代码审计_SQL

前言

前段时间开了审计的坑,最近也在积极学习审计相关的知识。本来是想自己写漏洞环境的,但是想到自己写的代码很抽象,所以经过考虑还是利用大佬写好的代码进行审计。当然大家也可以自己动手去写。这里我们用的是java-sec-code这个项目,大家可以进行下载,自行搭建。

本系列是从初学者的角度去进行逐一分析,若有不正确之处还希望大佬斧正。

关于搭建环境:

redeme已经说的很清楚了,将源码下载后,用IDEA打开项目。然后导入对应的.sql文件,直接起飞!!!

其他注意事项:

用户名root使用不了的话,可以随便起别的名字,需要在Spring配置中同步。

端口默认8080,估计很多人burp也是这个端口。要么修改burp的监听端口,要么修改环境开放端口。

审计思路

关于思路,我目前只了解到了三种方法:

  • 关注输入、输出、数据流。
  • 关注可能存在漏洞的关键函数。
  • 全篇通读

因为我平常比较习惯第一种关注输入、数据流、输出,这种审计方式。所以在之后的审计思路方面都是去通过接口传参追踪数据流走向,寻找漏洞。这种相对来说比较简单。而且相对合适,因为一般漏洞都是需要我们去输入数据后出发漏洞,所以我觉得这种方式也是比较不错的审计思路。

审计流程

寻找接口点

当我们在拿到源码的时候,发现有Swagger,我们直接打开进行查看。我们本篇文章主要以SQL注入为主,所以主要关注sqli这个接口:

image-20240201111810978

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
//Spring MVC
@Controller
public class ExampleController {
@RequestMapping("/example")
public String exampleHandler() {
// 控制器方法代码
return "exampleView";
}
}

//Struts2
@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方式进行配置。

image-20240201145212008

定位目标

通过IDEA的搜索,我们找到了GET /sqli/jdbc/ps/vuln query=username对应的逻辑类。我们先打上断点。

image-20240201145909135

利用burp发包,成功触发断点。

image-20240201151521636

image-20240201151630753

漏洞分析

源码展示

源码如下,我会将每句的注解先标注上去,然后再进一步分析漏洞。

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")
//接收参数username
public String jdbc_ps_vuln(@RequestParam("username") String username) {
//创建result保存查询结果
StringBuilder result = new StringBuilder();
try {
//加载指定的数据库驱动
Class.forName(driver);
//建立数据库连接,并将连接对象赋值给 con。
Connection con = DriverManager.getConnection(url, user, password);
//检查数据库是否成功连接,成功则打印successfully。
if (!con.isClosed())
System.out.println("Connecting to Database successfully.");
//查询users中的username(之间拼接参数)
String sql = "select * from users where username = '" + username + "'";
//创建一个预编译sql对象,并赋值给st
PreparedStatement st = con.prepareStatement(sql);
//打印预编译SQL语句,并记录日志
logger.info(st.toString());
//执行sql
ResultSet rs = st.executeQuery();
//对结果进行遍历
while (rs.next()) {
//获取username、password
String res_name = rs.getString("username");
String res_pwd = rs.getString("password");
//将获取到的username、password格式化为字符串,添加到result中,并记录日志。
String info = String.format("%s: %s\n", res_name, res_pwd);
result.append(info);
logger.info(info);
}
//关闭rs结果集对象和数据库连接
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,来将两个用户都展示出来。

image-20240201161036515

image-20240201161219810

漏洞修复

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);
// pstt.setObject(1,id);

其他接口分析

源码展示

我们通过JDBC在熟悉SQL审计的简单流程之后,我们看下来分析一下另一种Mybatis的注入。

1
GET  /sqli/mybatis/vuln01   	 	 	 query=username

此步省略之前的路由寻找,直接查看源码:

1
2
3
4
5
6
7
//SQLI.java
@GetMapping("/mybatis/vuln01")
//接收参数username
public List<User> mybatisVuln01(@RequestParam("username") String username) {
//调用userMapper查询username
return userMapper.findByUserNameVuln01(username);
}
1
2
3
4
5
//UserMapper.java
//查询传入参数的sql
@Select("select * from users where username = '${username}'")
//将username与sql语句中的username进行关联,返回一个结果list
List<User> findByUserNameVuln01(@Param("username") String username);

源码分析

上述代码使用了 MyBatis 框架进行数据库查询操作。然而在其SQL语句中使用了${}进行变量替换。

在Mybatis中,#{}是预编译处理,${}是字符串替换,使用${}就可能导致SQL注入。
示例:Select * from news where title like '%#{title}%'

漏洞复现

image-20240201164839073

漏洞修复

1
2
3
4
//修复后的代码片段
//UserMapper.java
@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注入漏洞面面观


代码审计_SQL
http://example.com/2024/02/01/代码审计-SQL/
作者
Yuanyi
发布于
2024年2月1日
许可协议