文章目录
一直知道反序列化漏洞的大概原理和利用方式,但从来没有真正动手调试过,拿来主义终究不可取,于是就有了这篇文章,这里就以fastjson为例,为自己好好捋一捋,沉淀一下。
文章有点过分的长,一次可能看不完。。。不过幸好有目录,有需要可以挑自己喜欢的地方看
环境
贴上我的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>JavaProject</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<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>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
可以搞一个marshalsec(不想搞的,忽略,后面会有替代代码),在github上下载,解压并在同目录下使用如下命令打包
mvn clean package -DskipTests
在target目录下可以找到marshalsec-0.0.3-SNAPSHOT-all.jar
fastjson介绍
首先创建一个简单Student类:
package fastjson;
public class Student {
private String name;
private int age;
public Student() {
System.out.println("Student构造函数");
}
public String getName() {
System.out.println("Student getName");
return name;
}
public void setName(String name) {
System.out.println("Student setName");
this.name = name;
}
public int getAge() {
System.out.println("Student getAge");
return age;
}
public void setAge(int age) {
System.out.println("Student setAge");
this.age = age;
}
}
TestFastJson.java,调用JSON.toJsonString()来序列化Student类对象 :
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class TestFastJson {
public static void main(String[] args){
Student student = new Student();
student.setName("5wimming");
student.setAge(18);
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString);
}
}
FastJson利用 toJSONString 方法来序列化对象,SerializerFeature.WriteClassName是JSON.toJSONString()中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法,而问题恰恰出现在了这个特性,我们可以配合一些存在问题的类,然后继续操作,造成RCE的问题。
输出如下:
# 设置了SerializerFeature.WriteClassName属性
Student构造函数
Student setName
Student setAge
Student getAge
Student getName
{
"@type":"fastjson.Student","age":18,"name":"5wimming"}
# 未设置SerializerFeature.WriteClassName属性
Student构造函数
Student setName
Student setAge
Student getAge
Student getName
{
"age":18,"name":"5wimming"}
而反序列化还原回 Object 的方法,主要的API有两个,分别是 JSON.parseObject 和 JSON.parse ,最主要的区别就是前者未指定目标类的前提下返回的是 JSONObject ,而后者返回的是实际类型的对象。
parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。
所以进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),因此在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class TestFastJsonDer {
public static void main(String[] args){
String jsonString ="{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"5wimming\"}";
System.out.println("通过parseObject方法进行反序列化,未指定class,返回一个JSONObject对象:");
JSONObject obj = JSON.parseObject(jsonString);
System.out.println(obj);
System.out.println(obj.getClass().getName());
System.out.println("通过parseObject方法进行反序列化,指定class,返回相应的类:");
Student obj02 = JSON.parseObject(jsonString, Student.class);
System.out.println(obj02);
System.out.println(obj02.getClass().getName());
System.out.println("通过parse方法进行反序列化,返回相应的类:");
Student obj03 = (Student)JSON.parse(jsonString);
System.out.println(obj03);
System.out.println(obj03.getClass().getName());
}
}
输出如下:
通过parseObject方法进行反序列化,未指定class,返回一个JSONObject对象:
Student构造函数
Student setAge
Student setName
Student getAge
Student getName
{"name":"5wimming","age":18}
com.alibaba.fastjson.JSONObject
通过parseObject方法进行反序列化,指定class,返回相应的类:
Student构造函数
Student setAge
Student setName
fastjson.Student@4f2410ac
fastjson.Student
通过parse方法进行反序列化,返回相应的类:
Student构造函数
Student setAge
Student setName
fastjson.Student@722c41f4
fastjson.Student
Feature.SupportNonPublicField的使用
fastjson默认情况下是不会反序列化私有属性的,如果需要怼私有属性进行反序列化,则需要在parseObject()函数添加一个属性Feature.SupportNonPublicField。
举一个栗子:
我们把student类改改,把私有属性age的set方法去掉(如果不去掉,fastjson依然是能反序列化成功的,因为你提供了这个接口),作为对比,我们把name属性设置为public,并且也注释掉set方法:
package fastjson;
public class Student {
public String name;
private int age;
public Student() {
System.out.println("Student构造函数");
}
public String getName() {
System.out.println("Student getName");
return name;
}
// public void setName(String name) {
// System.out.println("Student setName");
// this.name = name;
// }
public int getAge() {
System.out.println("Student getAge");
return age;
}
// public void setAge(int age) {
// System.out.println("Student setAge");
// this.age = age;
// }
}
然后反序列化:
String jsonString ="{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"5wimming\"}";
Student obj04 = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
System.out.println(obj04);
System.out.println(obj04.getClass().getName());
System.out.println(obj04.getName() + " " + obj04.getAge());
输出如下,发现不带Feature.SupportNonPublicField的age为0,即没有赋值成功,而同样没有set函数的name却可以赋值成功。
# 带有Feature.SupportNonPublicField
Student构造函数
fastjson.Student@5b80350b
fastjson.Student
Student getName
Student getAge
5wimming 18
# 不带Feature.SupportNonPublicField
Student构造函数
fastjson.Student@5b80350b
fastjson.Student
Student getName
Student getAge
5wimming 0
下面是Fastjson反序列化的类方法调用关系:
JSON:门面类,提供入口
DefaultJSONParser:主类
ParserConfig:配置相关类
JSONLexerBase:字符分析类
JavaBeanDeserializer:JavaBean反序列化类
Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:
- 函数名长度大于4且以set开头
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 函数名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
注意,除了getter方法和setter方法外,fastjson还有getIs和setIs方法,用于获取、设置布尔参数。
漏洞原理
从前文可知,fastjson在反序列化时,可能会将目标类的构造函数、getter方法、setter方法、is方法执行一遍,如果此时这四个方法中有危险操作,则会导致反序列化漏洞,也就是说攻击者传入的序列化数据中需要目标类的这些方法中要存在漏洞才能触发。
怎么样才能找到这种有漏洞函数的类呢
前文中,我们知道fastjson使用parseObject()/parse()进行反序列化的时候可以指定类型。有两种情况我们有可乘之机:
1、程序员自己实现的类中就包含了这种危险操作,那就可以直接利用了;
2、反序列化指定的类型太大,包含了很多子类,并且在不在反序列化的黑名单内,极端情况,如Object或JSONObject,像Object o = JSON.parseObject(poc,Object.class)就可以反序列化出来任意类,这种情况下,带有危险操作的类就相当可观了。
举个例子:
我们在setName()函数中加入了一个危险操作:
package fastjson;
import java.io.IOException;
public class Student {
public String name;
private int age;
public Student() {
System.out.println("Student构造函数");
}
public String getName() {
System.out.println("Student getName");
return name;
}
public void setName(String name) throws IOException {
System.out.println("Student setName");
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
this.name = name;
}
public int getAge() {
System.out.println("Student getAge");
return age;
}
// public void setAge(int age) {
// System.out.println("Student setAge");
// this.age = age;
// }
}
然后反序列化,便可弹出计算器了
package fastjson;
import com.alibaba.fastjson.JSON;