0x00 前言
由于乌云知识库暂时不能看了,把我之前投稿的文章在博客上发一下。 关于java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections
这个库,造成的反序列化问题。然而,在下载老外的ysoserial工具并仔细看看后,我发现了许多值得学习的知识。 至少能学到如下内容:
- 不同反序列化
payload
玩法 - 灵活运用了反射机制和动态代理机制构造POC
java反序列化不仅是有Apache Commons Collections
这样一种玩法。还有如下payload玩法:
CommonsBeanutilsCollectionsLogging1
所需第三方库文件: commons-beanutils:1.9.2,commons-collections:3.1,commons-logging:1.2CommonsCollections1
所需第三方库文件: commons-collections:3.1CommonsCollections2
所需第三方库文件: commons-collections4:4.0CommonsCollections3
所需第三方库文件: commons-collections:3.1(CommonsCollections1
的变种)CommonsCollections4
所需第三方库文件: commons-collections4:4.0(CommonsCollections2
的变种)Groovy1
所需第三方库文件: org.codehaus.groovy:groovy:2.3.9Jdk7u21
所需第三方库文件: 只需JRE版本 <= 1.7u21Spring1
所需第三方库文件: spring框架所含spring-core:4.1.4.RELEASE,spring-beans:4.1.4.RELEASE
上面标注了payload使用情况下所依赖的包,诸位可以在源码中看到,根据实际情况选择。 通过对该攻击代码的分析,可以学习java的一些有意思的知识。而且,里面写的java代码也很值得学习,巧妙运用了反射机制去解决问题。老外写的POC还是很精妙的。
0x01 准备工作
- 在github上下载ysoserial工具。
- 使用maven进行编译成Eclipse项目文件,
mvn eclipse:eclipse
。要你联网下载依赖包,请耐心等待。如果卡住了,停止后再次执行该命令。
导入后,可以看到里面有8个payload。其中ObjectPayload
是定义的接口,所有的Payload需要实现这个接口的getObject
方法。下面就开始对这些payload进行简要的分析。
0x02 payload分析
1. CommonsBeanutilsCollectionsLogging1
该payload的要求依赖包挺多的,可能碰到的情况不会太多,但用到的技术是极好的。对这个payload执行的分析,请阅读参考资源第一个的分析文章。 这里谈谈我的理解。先直接看代码:
1 | public Object getObject(final String command) throws Exception { |
第一行代码final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
创建了TemplatesImpl
类的对象,里面封装了我们需要的命令执行代码。而且是使用字节码的形式存储在对象属性中。
下面就具体分析下这个对象的产生过程。
(1) 利用TemplatesImpl类存储危险的字节码
在产生字节码时,用到了JDK中javassist
类。具体了解可以参考这篇博客http://www.cnblogs.com/hucn/p/3636912.html。
下面是我编写的一个简单的样例程序,便于理解:
1 | @Test |
上述代码首先获取到class定义的容器ClassPool
,并找到了我自定义的Point
类,由此生成了cc
对象。这样就可以开始对类进行修改的任意操作了。而且这个操作是直接写字节码。这样可以绕过许多安全机制,正像工具中注释说的:
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
后面的操作便是利用我自定义的模板类Point
,生成新的类名,并使用insertAfter
方法插入了恶意java代码,执行命令。有兴趣的可以再详细了解这个类的用法。这里不再赘述。 这段代码运行后,会在当前目录生成字节码(class文件)。使用java
反编译器可看到源码,在原始模板类中插入了恶意静态代码,而且以字节码的形式直接存储。命令行直接运行,可以执行弹出计算器的命令: 现在看看老外工具中,生成字节码的代码为:
1 | public static TemplatesImpl createTemplatesImpl(final String command) throws Exception { |
根据以上样例分析,可以清楚看见:前面几行代码,即生成了我们需要的插入了恶意java代码的字节码数据。该字节码其实可以看做是一个类(.class)文件。final byte[] classBytes = clazz.toBytecode();
将其转成了二进制数据进行存储。 Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes,ClassFiles.classAsBytes(Foo.class)});
这里又来到了一个有趣知识,那就是java反射机制的强大。ysoserial
工具封装了使用反射机制对对象的一些操作,可以直接借鉴。 具体可以看看其源码,这里在工具中经常使用的Reflections.setFieldValue(final Object obj, final String fieldName, final Object value);
方法,便是使用反射机制,将obj
对象的fieldName
属性赋值为value
。反射机制的强大之处在于:
- 可以动态对对象的私有属性进行改变赋值,即:
private
修饰的属性。 - 动态生成任意类对象。
于是,我们便将com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类生成的对象templates
中的_bytecodes
属性,_name
属性,_tfactory
属性赋值成我们希望的值。 重点在于_bytecodes
属性,里面存储了我们的恶意java代码。现在的问题便是:如何触发加载我们的恶意java字节码?
(2) 触发TemplatesImpl类加载_bytecodes属性中的字节码
在TemplatesImpl类中存在执行链:
1 | TemplatesImpl.getOutputProperties() |
这在ysoserial工具中的注释中是可以看到的。在源码中,我们从TemplatesImpl.getOutputProperties()
开始跟踪,不难发现上面的执行链。最终会在getTransletInstance
方法中看到如下触发加载自定义ja字节码部分的代码:
1 | private Translet getTransletInstance() |
在defineTransletClasses()
方法中,会加载我们之前存储在_bytecodes
属性中的字节码(可以看做类文件),进而返回类的Class
对象,存储在_class
数组中。下面是调试时候的截图: 可以看到在defineTransletClasses()
后,得到类的Class
对象。然后会执行newInstance()
操作,新建一个实例,这样便触发了我们插入的静态恶意java代码。如果接着单步执行,便会弹出计算器。 通过以上分析,可以看到:
- 只要能够自动触发
TemplatesImpl.getOutputProperties()
方法执行,我们就能达到目的了。
(3) 利用BeanComparator比较器触发执行
我们接着看payload
的代码:
1 | final BeanComparator comparator = new BeanComparator("lowestSetBit"); |
很简单,将PriorityQueue
(优先级队列)插入两个元素,而且需要一个实现了Comparator
接口的比较器,对元素进行比较,并对元素进行排队处理。具体可以看看PriorityQueue
类的readObject()
方法。
1 | private void readObject(java.io.ObjectInputStream s) |
从对象反序列化过程原理,可以知道会首先调用该对象readObject()
。当然在序列化过程中会首先调用该对象的writeObject()
方法。这两个方法可以对比着看,方便理解。 首先,在序列化PriorityQueue
类实例时,会依次读取队列中的对象,并放到数组中进行存储。queue[i] = s.readObject();
然后,进行排序操作heapify();
。最终会到达这里,调用比较器的compare()
方法,对元素间进行比较。
1 | private void siftDownUsingComparator(int k, E x) { |
这里传进去的,便是BeanComparator
比较器:位于commons-beanutils
包。
于是,看看比较器的compare
方法。
1 | public int compare( T o1, T o2 ) { |
o1
,o2
便是要比较的两个对象,property
即我们需要比较对象中的属性(可控)。一开始property
赋值为lowestSetBit
,后来改成真正需要的outputProperties
属性。 PropertyUtils.getProperty( o1, property )
顾名思义,便是取出o1
对象中property
属性的值。而实际上会去调用o1.getProperty()
方法得到property
属性值。 到这里,可以画上完美的一个圈了。我们只需将前面构造好的TemplatesImpl
对象添加到PriorityQueue
(优先级队列)中,然后设置比较器为BeanComparator("outputProperties")
即可。
那么,在反序列化过程中,会自动调用TemplatesImpl.getOutputProperties()
方法。执行命令了。 个人总结观点:
- 只需要想办法:自动调用
TemplatesImpl
的getOutputProperties
方法。或者TemplatesImpl.newTransformer()
即能自动加载字节码,触发恶意代码。这也在其他payload
中经常用到。 - 触发原理:提供会自动调用比较器的容器。如:将
PriorityQueue
换成TreeSet
容器,也是可以的。
为了在生成payload时,能够正常运行。在代码中,先象征性地加入了两个BigInteger
对象。
后面使用反射机制,将comparator
中的属性和queue
容器存储的对象都改成我们需要的属性和对象。
否则,在生成payload
时,便会弹出计算器,抛出异常,无法正常执行了。测试如下:
2. Jdk7u21
该payload
其实是JAVA SE
的一个漏洞,ysoserial工具注释中有链接:https://gist.github.com/frohoff/24af7913611f8406eaf3。该payload
不需要使用任何第三方库文件,只需官方提供的JDK
即可,这个很方便啊。 不知Jdk7u21
以后怎么补的,先来看看它的实现。 在介绍完上面这个payload
后,再来看这个可以发现:CommonsBeanutilsCollectionsLogging1
借鉴了Jdk7u21
的利用方法。 同样,Jdk7u21
开始便创建了一个存储了恶意java字节码数据的TemplatesImpl
类对象。接下来就是怎么触发的问题了:如何自动触发TemplatesImpl
的getOutputProperties
方法。 这里首先就有一个有趣的hash碰撞问题了。
(1) “f5a5a608”的hash值为0
类的hashCode
方法是返回一个独一无二的hash值(int型),去代表这个唯一对象。如果类没有重写hashCode
方法,会调用原始Object
类中的hashCode
方法返回一个hash值。String
类的hashCode
方法是这么实现的。
1 | public int hashCode() { |
于是,就有了有趣的值:
1 | String zeroHashCodeStr = "f5a5a608"; |
可以看到”f5a5a608”字符串,通过hashCode
方法生成的hash值为0。这在之后的触发过程中会用到。
(2) 利用动态代理机制触发执行
Jdk7u21
中使用了HashSet
容器进行触发。添加了两个对象,一个是存储了恶意java字节码数据的TemplatesImpl
类对象templates
,一个是代理了Templates
接口的proxy
对象,使用了动态代理机制。 如下是Jdk7u21
生成payload时的主要代码:
1 | ...... |
HashSet
容器,就可以当做是一个HashMap<key,new Object()>
,key
便是我们存储进去的数据,对应的value
都只是静态的Object
对象。 同样,来看看HashSet
容器中的readObject
方法。
1 | private void readObject(java.io.ObjectInputStream s) |
实际上,这里map
可以看做是HashMap
类生成的对象。接着追踪源码就到了关键的地方:
1 | public V K , V value) { |
通过以上分析下可以知道:在反序列化HashSet
过程中,会依次将templates
和proxy
对象添加到map
中。 接着我们需要触发代码去执行key.equals(k)
这条语句。
由于短路机制的原因,必须使templates.hashCode()
与proxy.hashCode()
计算值相等。 proxy
使用了动态代理机制,代理了Templates
接口。具体请参考其他分析老外LazyMap
触发Apache Commons Collections
第三库序列化问题的文章,如:参考资料2。 这里又到了熟悉的sun.reflect.annotation.AnnotationInvocationHandler
类。
简而言之,我理解为将对象proxy
所有的方法调用,都改成调用sun.reflect.annotation.AnnotationInvocationHandler
类的invoke()
方法。 当我们调用proxy.hashCode()
方法时,自然就会执行到了如下代码:
1 | public Object invoke(Object proxy, Method method, Object[] args) { |
这里的memberValues
就是payload
代码一开始传进去的map("f5a5a608",templates)
。简要画图说明为: 因此,通过动态代理机制加上"f5a5a608".hashCode()=0
的特殊性,使e.hash == hash
成立。
这样便可以执行key.equals(k)
,即:proxy.equals(templates)
语句。 接着查看源码便知:proxy.equals(templates)
操作会遍历Templates
接口的所有方法,并调用。如此,即可触发调用templates
的getOutputProperties
方法。
1 | if (member.equals("equals") && paramTypes.length == 1 && |
如此,Jdk7u21
的payload
便也完美触发了。 同样,为了正常生成payload不抛出异常。先暂时存储map.put(zeroHashCodeStr, "foo");
,后面替换为真正我们所需的对象:map.put(zeroHashCodeStr, templates); // swap in real object
总结一下:
- 技术关键在于巧妙的利用了”f5a5a608”hash值为0。实现了hash碰撞成立。
AnnotationInvocationHandler
对于equal
方法的处理,可以使我们调用目标方法getOutputProperties
。
计算hash值部分的内容还挺有意思。有兴趣可以到参考链接中github上看看我的测试代码。
3. Groovy1
这个payload
和最近Xstream
反序列化漏洞的POC原理有相似性。请参考:http://drops.wooyun.org/papers/13243。 下面谈谈这个payload不一样的地方。 payload
使用了Groovy
库中ConvertedClosure
类。该类实现了InvocationHandler
和Serializable
接口,同样可以用作动态代理并且可以序列化传输。代码也只有几行:
1 | final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet"); |
当反序列化handler时,会调用map.entrySet
方法。于是,就调用代理类ConvertedClosure
的invoke
方法了。最终,来到了:
1 | public Object invokeCustom(Object proxy, Method method, Object[] args) |
然后和XStream
一样,调用MethodClosure.doCall()
方法。即:Groovy语法中"command".execute()
,顺利执行命令。 个人总结:
- 可以看到动态代理机制的强大作用。
4. Spring1
Spring1
这个payload
执行链有些复杂。按照常规步骤来分析下:
- 反序列化对象的readObject()方法为入口点进行跟踪。这里是
org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider
。1
2
3
4
5private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
Method method = ReflectionUtils.findMethod(this.provider.getType().getClass(), this.methodName);
this.result = ReflectionUtils.invokeMethod(method, this.provider.getType());
}
很明显的嗅到了感兴趣的”味道”:ReflectionUtils.invokeMethod
。接下来联系payload
源码跟进下,或者单步调试。
- 由于流程可能比较错综复杂,画个简单的图表示下几个对象之间的关系:
- 在执行
ReflectionUtils.invokeMethod(method, this.provider.getType())
语句时,整个执行流程如下:1
2
3ReflectionUtils.invokeMethod()
Method.invoke(typeTemplatesProxy对象)
//Method为Templates(Proxy).newTransformer()
这是明显的一部分调用,在执行Templates(Proxy).newTransformer()
时,会有余下过程发生:
1 | typeTemplatesProxy对象.invoke() |
这里面是对象之间的调用,还有动态代理机制,容易绕晕,就说到这里。有兴趣可以单步调试看看。 个人总结:
Spring1
为了强行代理Type
接口,进行对象赋值。运用了多个动态代理机制实现,还是很巧妙的。
5. CommonsCollections
对CommonsCollections
类,ysoserial
工具中存在四种利用方法。所用的方法都是与上面几个payload
类似。
CommonsCollections1
自然是使用了LazyMap
和动态代理机制进行触发调用Transformer
执行链,请参考链接2。CommonsCollections2
和CommonsBeanutilsCollectionsLogging1
一样也使用了比较器去触发TemplatesImpl
的newTransformer
方法执行命令。
这里用到的比较器为TransformingComparator
,直接看其compare
方法:1
2
3
4
5public 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);
}
很直接调用了transformer.transform(obj1)
,这里的obj1
就是payload
中的templates
对象。
主要代码为:
1 | // mock method name until armed |
根据熟悉的InvokerTransformer
作用,最终会调用templates.newTransformer()
执行恶意java代码。
CommonsCollections3
是CommonsCollections1
的变种,将执行链换了下:1
2
3
4
5
6
7
8TemplatesImpl templatesImpl = Gadgets.createTemplatesImpl(command);
.............
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};
查看InstantiateTransformer
的transform
方法,可以看到关键代码:
1 | Constructor con = ((Class) input).getConstructor(iParamTypes); //input为TrAXFilter.class |
即:transformer
执行链会执行new TrAXFilter(templatesImpl)
。正好,TrAXFilter
类构造函数中调用了templates.newTransformer()
方法。都是套路啊。
1 | public TrAXFilter(Templates templates) throws |
CommonsCollections4
是CommonsCollections2
的变种。同样使用InstantiateTransformer
触发templates.newTransformer()
代替了之前的执行链。1
2
3
4
5
6
7
8
9
10
11TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
...............
// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
..............
// swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;
...................
照例生成PriorityQueue<Object> queue
后,使用反射机制对其属性进行修改。保证成功生成payload。 个人总结:payload分析完了,里面涉及的方法很巧妙。也有许多共同的利用特性,值得学习~~
0x03 参考资料
http://blog.knownsec.com/2016/03/java-deserialization-commonsbeanutils-pop-chains-analysis/ http://www.iswin.org/2015/11/13/Apache-CommonsCollections-Deserialized-Vulnerability/ https://github.com/angelwhu/ysoserial-test/