前言
前段时间开了审计的坑,最近也在积极学习审计相关的知识。本来是想自己写漏洞环境的,但是想到自己写的代码很抽象,所以经过考虑还是利用大佬写好的代码进行审计。当然大家也可以自己动手去写。这里我们用的是java-sec-code这个项目,大家可以进行下载,自行搭建。
本系列是从初学者的角度去进行逐一分析,若有不正确之处还希望大佬斧正。
关于搭建环境:
redeme已经说的很清楚了,将源码下载后,用IDEA打开项目。然后导入对应的.sql文件,直接起飞!!!
其他注意事项:
用户名root使用不了的话,可以随便起别的名字,需要在Spring配置中同步。
端口默认8080,估计很多人burp也是这个端口。要么修改burp的监听端口,要么修改环境开放端口。
审计思路
关于思路,我目前只了解到了三种方法:
- 关注输入、输出、数据流。
- 关注可能存在漏洞的关键函数。
- 全篇通读
因为我平常比较习惯第一种关注输入、数据流、输出,这种审计方式。所以在之后的审计思路方面都是去通过接口传参追踪数据流走向,寻找漏洞。这种相对来说比较简单。而且相对合适,因为一般漏洞都是需要我们去输入数据后出发漏洞,所以我觉得这种方式也是比较不错的审计思路。
审计流程
寻找接口点
当我们在拿到源码的时候,发现有Swagger,我们直接打开进行查看。我们本篇文章主要以SQL注入为主,所以主要关注sqli
这个接口:
sqli接口具体信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| GET /sqli/jdbc/ps/vuln query=username
HEAD /sqli/jdbc/ps/vuln query=username
POST /sqli/jdbc/ps/vuln query=username
PUT /sqli/jdbc/ps/vuln query=username
DELETE /sqli/jdbc/ps/vuln query=username
OPTIONS /sqli/jdbc/ps/vuln query=username
PATCH /sqli/jdbc/ps/vuln query=username
GET /sqli/jdbc/sec query=username
HEAD /sqli/jdbc/sec query=username
POST /sqli/jdbc/sec query=username
PUT /sqli/jdbc/sec query=username
DELETE /sqli/jdbc/sec query=username
OPTIONS /sqli/jdbc/sec query=username
PATCH /sqli/jdbc/sec query=username
GET /sqli/jdbc/vuln query=username
HEAD /sqli/jdbc/vuln query=username
POST /sqli/jdbc/vuln query=username
PUT /sqli/jdbc/vuln query=username
DELETE /sqli/jdbc/vuln query=username
OPTIONS /sqli/jdbc/vuln query=username
PATCH /sqli/jdbc/vuln query=username
GET /sqli/mybatis/orderby/sec04 quety=sort
GET /sqli/mybatis/orderby/vuln03 quety=sort
GET /sqli/mybatis/sec01 query=username
GET /sqli/mybatis/sec02 query=id
GET /sqli/mybatis/sec03 No parameters
GET /sqli/mybatis/vuln01 query=username
GET /sqli/mybatis/vuln02 query=username
|
这么多我们肯定不可能一个一个看。可以将他们分为两类,JDBC和Mybatis。其他的只是提交参数的方式不同最终执行逻辑还是相同的。我们就以get为例来进行审计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| GET /sqli/jdbc/ps/vuln query=username
GET /sqli/jdbc/sec query=username
GET /sqli/jdbc/vuln query=username
GET /sqli/mybatis/orderby/sec04 quety=sort
GET /sqli/mybatis/orderby/vuln03 quety=sort
GET /sqli/mybatis/sec01 query=username
GET /sqli/mybatis/sec02 query=id
GET /sqli/mybatis/vuln01 query=username
GET /sqli/mybatis/vuln02 query=username
|
寻找路由
通过上述分析,我们已经成功找到了每个接口。但是因为我们做的是白盒,所以光知道接口是不行的。我们还需要知道每个接口对应后端的真正逻辑处理部分即service。
URL和逻辑类之间的对应关系可以用注解和web.xml两种方式进行配置或对应
注解方式
注解方式是在代码中直接使用注解来进行 URL 路径和逻辑类的对应关系配置。像 @WebServlet("/example")
、@RequestMapping("/example")
、@Action("/example-action")
,除了service、Filter和Listener都是可以通过这种方式进行映射。其中包括Spring、Struts2等一些框架大都是注释的方式进行映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Controller public class ExampleController { @RequestMapping("/example") public String exampleHandler() { return "exampleView"; } }
@Namespace("/example") @Action("/example-action") public class ExampleAction extends ActionSupport { }
|
web.xml
web.xml 是一个 XML 格式的配置文件,用于对 Web 应用程序进行全局配置。通过在 web.xml 文件中配置 <servlet>
、<servlet-mapping>
、<filter>
、<filter-mapping>
和 <listener>
元素,可以实现 URL 路径和逻辑类的对应关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 逻辑类 <servlet> <servlet-name>exampleServlet</servlet-name> <servlet-class>com.example.ExampleServlet</servlet-class> </servlet> 路由绑定 <servlet-mapping> <servlet-name>exampleServlet</servlet-name> <url-pattern>/example</url-pattern> </servlet-mapping>
<filter> <filter-name>exampleFilter</filter-name> <filter-class>com.example.ExampleFilter</filter-class> </filter> <filter-mapping> <filter-name>exampleFilter</filter-name> <url-pattern>/example/*</url-pattern> </filter-mapping>
<listener> <listener-class>com.example.ExampleListener</listener-class> </listener>
|
tips
需要注意的是,使用注解方式和 web.xml 配置方式是可以混合使用的,但是当二者同时存在时,注解方式的优先级会高于 web.xml 配置方式。
实际操作
IDEA双击shift
打开搜索界面,输入@RequestMapping
找到我们像要查看的路由。正常情况,若这一步没有查找到,可以检查源码是否拷贝完整或者考虑是否源码以web.xml方式进行配置。
定位目标
通过IDEA的搜索,我们找到了GET /sqli/jdbc/ps/vuln query=username
对应的逻辑类。我们先打上断点。
利用burp发包,成功触发断点。
漏洞分析
源码展示
源码如下,我会将每句的注解先标注上去,然后再进一步分析漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @RequestMapping("/jdbc/ps/vuln") public String jdbc_ps_vuln(@RequestParam("username") String username) { StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); if (!con.isClosed()) System.out.println("Connecting to Database successfully."); String sql = "select * from users where username = '" + username + "'"; PreparedStatement st = con.prepareStatement(sql); logger.info(st.toString()); ResultSet rs = st.executeQuery(); while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); String info = String.format("%s: %s\n", res_name, res_pwd); result.append(info); logger.info(info); } rs.close(); con.close();
} catch (ClassNotFoundException e) { logger.error("Sorry, can't find the Driver!"); e.printStackTrace(); } catch (SQLException e) { logger.error(e.toString()); } return result.toString(); }
|
源码分析
这个漏洞点一眼就出,很开门。直接将字符串与SQL进行拼接这是大忌。漏洞点我们虽然找到了,但是想要进一步利用,我们还得进一步分析。
因为在正常环境当中,service之前还有filter。因为存在有些没办法使用预编译的情况。例如:模糊查询(like)、order by等所以就需要在filter之中再做进一步的过滤,一般见到比较多的是写一个黑名单来进行检测。
当然这个环境比较简单,它当中并没有编写对应的sql-filter。虽然里面使用了java原生的sql预编译函数PreparedStatement()
但是在SQL语句中并没有用"?"
进行占位,且没有进行参数绑定。
漏洞复现
因为这个数据库导入之后,只有一张表,里面只有内置的两个用户。所以我们这里注入的语句就选择or 1=1,来将两个用户都展示出来。
漏洞修复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password);
if (!con.isClosed()) System.out.println("Connecting to Database successfully.");
String sql = "select * from users where username = ?"; PreparedStatement st = con.prepareStatement(sql); st.setString(1, username);
logger.info(st.toString()); ResultSet rs = st.executeQuery();
while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); String info = String.format("%s: %s\n", res_name, res_pwd); result.append(info); logger.info(info); }
rs.close(); con.close();
} catch (ClassNotFoundException e) { logger.error("Sorry, can't find the Driver!"); e.printStackTrace(); } catch (SQLException e) { logger.error(e.toString()); } return result.toString();
|
修复之后首先使用?进行占位,然后执行SQL,再将username进行绑定。这样就会解决SQL注入的问题。
tips:
预编译使用有误,没有调用 set 方法将变量与占位符进行对应。也会存在SQL注入漏洞
示例:
1 2 3
| String sql = "select * from test where id = ?"; PreparedStatement pstt = conn.prepareStatement(sql);
|
其他接口分析
源码展示
我们通过JDBC在熟悉SQL审计的简单流程之后,我们看下来分析一下另一种Mybatis的注入。
1
| GET /sqli/mybatis/vuln01 query=username
|
此步省略之前的路由寻找,直接查看源码:
1 2 3 4 5 6 7
| @GetMapping("/mybatis/vuln01") public List<User> mybatisVuln01(@RequestParam("username") String username) { return userMapper.findByUserNameVuln01(username); }
|
1 2 3 4 5
| @Select("select * from users where username = '${username}'") List<User> findByUserNameVuln01(@Param("username") String username);
|
源码分析
上述代码使用了 MyBatis 框架进行数据库查询操作。然而在其SQL语句中使用了${}进行变量替换。
在Mybatis中,#{}是预编译处理,${}是字符串替换,使用${}就可能导致SQL注入。
示例:Select * from news where title like '%#{title}%'
漏洞复现
漏洞修复
1 2 3 4
|
@Select("select * from users where username = '#{username}'") List<User> findByUserNameVuln01(@Param("username") String username);
|
修复之后使用#{}进行预编译操作,此时就不会产生SQL注入的问题。
tips:
mybatis有两种写法,一种是基于注解:
1 2 3 4 5
| @Mapper public interface CategoryMapper { @Select("select * from category_ where name= '${name}' ") public CategoryM getByName(@Param("name") String name); }
|
另一种是基于xml:
1 2 3 4 5 6 7 8 9 10
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.seaii.springboot.mapper.CategoryMapper"> <select id="get" resultType="cn.seaii.springboot.pojo.CategoryM"> select * from category_ where id= ${id} </select> </mapper>
|
注意在maven项目中,xml文件要放在resources目录中。效果都是一样的。
结尾:
总的来说,这个只是入门,在真实情况下,相对来说会比较复杂。还需要多关注filter中的流程和绕来绕去或者套的很深的源码。具体还是得多看多实践。
参考链接:
Mybatis框架下SQL注入漏洞面面观