SQL注入漏洞审计
靶场:Hello-Java-Sec
java中由于数据库连接的方式有多种 所以它们对应的漏洞利用方式也是不同的
原生JDBC
JDBC有两种⽅法执⾏SQL语句,分别为PrepareStatement和Statement。两个⽅法的区别在PrepareStatement会对SQL语句进⾏预编译,⽽Statement⽅法在每次执⾏时都需要编译,会增⼤系统开销
Statement注入
点击靶场对应的代码
发现是存在注入的
现在问题来了?
我们该如何找到个这个接口对应在后端的代码呢?
毕竟它不像php和java原生的项目 ,有对应的目录 ,直接按照对应url的目录去找就可以
看IDEA的端点 这个也是最常用最有效的方法
也可以右键项目 在项目中搜索
或者根据习惯 根据MVC 一般业务的逻辑都在controller中
找到对应的SQLI一级目录 再去找二级三级目录
需要注意的点!!!!
SQLI/jdbc 虽然url是这样的 但是SQLI和jdbc它俩其实是没有连在一起的 去分析依赖or文件中查找 连在一起查找的话有可能是查不到的
其实 SQLI相当于是一级目录 声明在某一个class 中 而jdbc只是当前这个class中的一个方法
既然找到了对应的java代码 那我们就看看它是怎么写的吧。
@RequestMapping("/jdbc") public String sql_1(String id) { StringBuilder result = new StringBuilder(); try { /* * 注册 JDBC 驱动 * com.mysql.jdbc.Driver 对应版本 5 * com.mysql.cj.jdbc.Driver 对应高版本 */ Class.forName("com.mysql.cj.jdbc.Driver"); // 建立连接 Connection conn = DriverManager.getConnection(db_url, db_user, db_pass); // 执行查询 Statement stmt = conn.createStatement(); String sql = "select * from users where id = '" + id + "'"; System.out.println("[*] 执行SQL语句:" + sql); ResultSet rs = stmt.executeQuery(sql); // 获取查询结果 while (rs.next()) { String res_name = rs.getString("user"); String res_pass = rs.getString("pass"); String info = String.format("查询结果 %s: %s", res_name, res_pass); result.append(info); } rs.close(); conn.close(); } catch (Exception e) { // 输出错误,用于报错注入。。。 return e.toString(); } return result.toString(); }这是直接做的拼接 肯定是有问题的
构造执行了语句 进行了报错注入
[*] 执行SQL语句:select * from users where id = '1' and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)--当然也可以构造其他的语句
PrepareStatement注⼊
PrepareStatement支持进行预编译 正常的写法肯定是没有sql注入的问题的 但是不安全的写法同样会导致sql注入产生啊
比如:
String sql = "select * from user where id ="+req.getParameter("id"); PrintWriter out = resp.getWriter(); out.println("prepareStatement Demo"); out.println("SQL: "+sql); try { PreparedStatement pst = conn.prepareStatement(sql); ResultSet rs = pst.executeQuery(); while (rs.next()){ out.println("<br>Result: "+ rs.getObject("name")); } } catch (SQLException throwables) { throwables.printStackTrace(); }看似用了prepareStatement 实则还是拼接的的方式 没有进行预编译
正确的写法应该是如何呢?看靶场的代码
@RequestMapping("/jdbc/pre") public String sql_pre(String id) { StringBuilder result = new StringBuilder(); try { Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection(db_url, db_user, db_pass); String sql = "select * from users where id = ?"; PreparedStatement st = conn.prepareStatement(sql); st.setString(1, id); System.out.println("[*] 执行SQL语句:" + st); ResultSet rs = st.executeQuery(); while (rs.next()) { String res_name = rs.getString("user"); String res_pass = rs.getString("pass"); String info = String.format("查询结果%n %s: %s%n", res_name, res_pass); result.append(info); } rs.close(); conn.close(); } catch (Exception e) { return e.toString(); } return result.toString(); }st.setString(1, id);的含义是指定第一个位置的参数为id
使用了预编译的方式 有效防御了sql注入的产生
MyBatis
MyBatis 是⼀个流⾏的持久层框架,它提供了⼀种优雅⽽简单的⽅式来管理数据库访问。通常,框架底层已经实现了对SQL注⼊的防御,但在研发⼈员未能恰当使⽤框架的情况下,仍然可能存在SQL注⼊的⻛险。
在 MyBatis 中,动态 SQL 拼接字符串可能导致 SQL 注⼊漏洞。
直接拼接
<select id="QueryByName" parameterType="String" resultType="com.demo.bean.U ser"> select * from user where name = ${name} </select>orderby注入
点击oderBy函数查看函数详情
这是传统的XML形式的配置 我们去根据oderBy这个接口名找到对应的xml写法
${field}和${sort}确实存在问题 并且这俩参数都是String类型 存在sql注入的漏洞
正确的写法查看
MyBatis3提供了新的基于注解的配置,通过注解不在需要配置繁杂的xml文件
看写法还是错的啊 为什么就会避免了sql注入呢?
因为这里的id是int类型 输入字符串的话会被过滤掉 隐式转换
在看一下其他的地方有没有问题呢?我看到了除了这俩还有不少的函数
1.queryById1存在sql注入
错误写法+字符串类型值
2.orderBySafe安全
他就没有去拼接用户的输入 它的order by后面的语句都是写好的
XSS
直接在 html ⻚⾯展示“⽤户可控数据”,将直接导致跨站脚本威胁
解决办法:
1.进⾏ html escape 转义
2.在给⽤户设置认证 COOKIE 时,加⼊ HTTPONLY
命令注入
命令注⼊是注⼊操作系统命令并执⾏命令
学习的重点不仅仅只是看出来有命令注入漏洞 还要会利用会验证 会构造参数命令
ProcessBuilder
java.lang.ProcessBuilder 中 start() ⽅法可以执⾏系统命令
代码示例:
// String dir = "xxxx"; String[] cmdList = new String[]{"sh", "-c", "ls -lh " + dir}; // ; | & `` $() ProcessBuilder builder = new ProcessBuilder(cmdList); builder.redirectErrorStream(true); Process process = builder.start(); printOutput(process.getInputStream());看靶场
为啥是空白啊
看运行控制台有报错
提示找不到sh
因为sh是linux的命令行 win的是cmd.exe
去看代码 cmd.exe/c
果然是这样
那我们把它改一下吧 改成win的指令
改完后记得重新部署 要不然不会生效 filepath参数用户可控
产生了命令注入
Runtime
java.lang.Runtime 中 exec() 函数同样可以执⾏系统命令 本质上还是调用ProcessBuilder 的问题
⽽直接传⼊ String 时,会先经过 StringTokenizer 的分隔处理,然后在使⽤ ProcessBuilder,所以要知道
StringTokenizer 是如何分割字符串命令的,这样才可以去构造参数进行漏洞验证。
经过测试 sh -c ls;id 形式的可以
对于 sh -c curl example.com 来说 会把example.com作为前面的sh -c的参数执行 这肯定是不可以执行的
看靶场
空白页面 看运行控制台的报错
应该是把id当做系统的可运行文件执行了
@RequestMapping("/runtime") public static String cmd2(String cmd) { StringBuilder sb = new StringBuilder(); String line; try { // 执行命令 Process proc = Runtime.getRuntime().exec(cmd); InputStream fis = proc.getInputStream(); InputStreamReader isr = new InputStreamReader(fis); BufferedReader br = new BufferedReader(isr); while ((line = br.readLine()) != null) { System.out.println(line); sb.append(line); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); }嗯 符合猜想
直接把传入的参数当做可执行的程序了
效果相当于在运行窗口输入id 这肯定是有问题的啊
这种情况下输入win命令行的命令也不行 因为没有名叫dir的可执行文件
但是calc应该是可以的 因为它是有可执行的文件的
那我如果是想执行dir 等命令行的命令应该怎么做?
要自己去构造命令 实现:1.运行cmd.exe 2.把dir作为参数传入cmd.exe
这也是我们前面要分析参数命令格式的原因
参考格式sh -c ls;id 试一下这个
cmd.exe /c dirSSRF
SSRF(Server-Side Request Forgery)服务端请求伪造 是⼀种由攻击者构造形成由服务端发起请求的⼀个安全漏洞。
很多web应⽤都提供了从其他的服务器上获取数据的功能。使⽤ 户指定的URL,web应⽤可以获取图⽚,下载⽂件,读取⽂件内容等。这个功能如果被恶意使⽤,可以利⽤存在缺陷的web应⽤作为代理攻击远程和本地的服务器。
URLConnection方式
找不到路径是因为我们用的win部署的 没有file:///etc/password 换成win存在的目录即可
代码如下:
@GetMapping("/URLConnection") public String URLConnection(String url) { return Http.URLConnection(url); }// URLConnection类 public static String URLConnection(String url) { try { URL u = new URL(url); URLConnection conn = u.openConnection(); // 通过getInputStream() 读取 URL 所引用的资源数据 BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String content; StringBuffer html = new StringBuffer(); while ((content = reader.readLine()) != null) { html.append(content); } reader.close(); return html.toString(); } catch (Exception e) { return e.getMessage(); } }bypass
查看代码:
@GetMapping("/HTTPURLConnection/safe") public String HTTPURLConnection(String url) { if (!Security.is_http(url)){ return "不允许非http/https协议!!!"; }else if (Security.is_intranet(url)) { return "不允许访问内网!!!"; }else{ return Http.HTTPURLConnection(url); } }有对url参数的内容做检查
看下Security.is_http函数的内容
public static boolean is_http(String url) { return url.startsWith("http://") || url.startsWith("https://"); }检查url的开头是不是http或者https开头 如果是其他协议的话返回false 这样就限定了url要访问的话只能是http
协议
再看下Security.is_intranet函数的内容:
public static boolean is_intranet(String url) { Pattern reg = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)|(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|(192\\.168\\.\\d{1,3}\\.\\d{1,3})$"); Matcher match = reg.matcher(url); Boolean a = match.find(); return a; }代码执行流程
编译正则表达式:Pattern.compile() 创建匹配模式
创建匹配器:reg.matcher(url) 对输入URL进行匹配
查找匹配:match.find() 查找是否有匹配的部分
返回结果:返回匹配结果(true/false)
作用其实就是对url的ip做些过滤 过滤掉127 10 172 192 等内网的ip地址
访问网址的话当然是可以的
可以把内网地址转换成短网址传入去进行绕过
要防御就需要去做限制 不允许重定向
信息泄露
Actuator
Spring Boot Actuator 模块提供了健康检查,审计,指标收集,HTTP 跟踪等,是帮助我们监控和管理Spring Boot 应⽤的模块。如果没有正确使⽤Actuator,可能造成信息泄露等严重的安全隐患(外部⼈员⾮授权访问Actuator端点)。其中heapdump作为Actuator组件最为危险的Web端点,heapdump因未授权访问被恶意⼈员获取后进⾏分析,可进⼀步获取敏感信息。
Actuator的使用:1.加⼊相关的 maven dependency 2.开启了所有端点未授权访问的配置
查看有哪些actuator的端点开放了