fastjson 是 java 中常用的一个用来序列化/反序列化 JSON 数据的库。因其优异的性能表现,在 java web 开放中应用比较广泛。这两天花 3 分钟入门了 JAVA 安全,恰巧最近需要写一个 fastjson 的检测插件,稍微研究了一下后,感觉有一个比较不错的检测方法,在这里和大家分享下。
在文章开始之前,我想说明一点,这里介绍的是检测方法,而不是利用方法。这是两个不同的目标,实现这两个目标需要考虑的细节也是不同的。在做漏洞检测时,尤其是自动化检测时,关注的往往有以下几点:
- 利用入口点是什么
- 如何确认漏洞存在
- 如何高效检测
- 如何无损检测
围绕着这几点,我这个从未接触 java 安全的弟弟打开了 idea 开始了 fastjson 反序列化 debug 之路。
漏洞成因
我刚接触的时候,感觉很多文章都在说 @type
,但@type
是什么,为什么需要 @type
大家好像都没有提及,而且既然 @type
这么多问题,官方为何不去掉这个用法。带着这些疑问,我写了一个简单的 case,在 1.2.24 版本运行一下:
|
|
结果为:
|
|
仔细观察这个这个 case,它主要说明了三点:一是如果没有指定类型,得到的是 fastjson 的内置类型 JSONObject
,这个模式下没有类型信息,使用起来和 python dict 比较像;二是如果用某种方式制定了类型,那么会调用初始化函数和相关属性的 setter 等。这里说的某种方式可以通过 @type
在 JSON 中指定,也可以在反序列化时手动指定 class 类;三是反序列化指定类时,类的 setter
和 getter
会被调用。
我们来试着回答下上面的三个问题: @type
用于指定本次序列化所使用的类,方便直接操作想要的类型,例子中的后两种情况我们可以直接通过类型转换将原始的 JSONObject
转为 User
,第一种却不行,因为后两种真正的类型就是 User
,用过 go 的 interface{}
的同学应该比较容易理解这句话;至于为什么需要以及为什么不去掉,我猜想的是一方面帮 Java 开发者偷懒了,一方面可能也是不得不。Java 是一门静态类型语言,在静态语言中操作动态类型是比较难受和不安全的方式,虽然可以通过手动指定class 的方式做反序列化,但这种写法不够通用,在写中间件之类的代码时,结合各种反射特性可以把东西写的很精巧,这时候就不得不用一些比较投机的方式了。
回到话题上,现在我们可以概括一下这个漏洞的成因: 反序列化 @type
指定的类时,指定类的 setter
或 getter
被调用导致的命令执行。
检测方案
上面说到漏洞触发和 setter
与 getter
有关,那么利用方式就是找那些在 setter
和 getter
中有敏感方法的类。从各位大佬们的分析文章来看,主流方式有三种(以 1.2.24 版本为例):
JNDI 注入
|
|
原理是 com.sun.rowset.JdbcRowSetImpl
这个类在设置 autoCommit
的 setter 时会调用 connect 方法去连接 dataSourceName
指定的 jdbc 服务。 JNDI 常用的有 RMI 和 LDAP 服务,这里我使用的 RMI 服务,因为实现比较简单,这个后面会说。
bytesCode
|
|
原理是把这个类会把中的方法会实例化 _bytescodes
中指定的类,我们可以写一个自定义类并在类的初始化函数中加入利用代码。
DNS log
|
|
原理是 java.net.InetAddress
这个类在实例化时会尝试做对 example.com
做域名解析,这时候可以通过 dns log 的方式得知漏洞是否存在了。
上面的三种方式综合考量下,第一种是最合适的。第二种有个致命的限制,需要类似 JSON.parseObject(z, Feature.SupportNonPublicField)
的用法来启用对私有成员的设置,这个选项默认关闭,所以直接不考虑;第三种虽然简单,但用户部署起来很复杂,需要一个能够自行控制 dns 的域名才可以,而且内网的情况更加棘手。
查阅资料后发现,为了防止 JNDI 注入,Java 本身也做了很多努力,比如 java.rmi.server.useCodebaseOnly
和 com.sun.jndi.rmi.object.trustURLCodebase
这两个都是用于防止 rmi server 远程加载恶意类的。但这些限制对漏洞检测而言是无效的,检测讲究点到为止,我们只要能确定漏洞存在就可以结束检测流程。对 JNDI 注入而言,我们认为 JNDI server 收到了 socket 连接就是漏洞存在。
确定 payload
上面敲定了使用 JNDI 注入的方式来做检测,还有个关键问题需要解决,就是检测过程使用的 payload。有个简单的方式是把各个版本爆出的 poc 都打一遍,可以但有些粗暴。回看最开始说的漏洞检测的几个点,现在要思考的是如何高效检测。
从 2017 年到现在(2019.12),fastjson 先后约有 5 次左右的反序列漏洞的产生、修复和绕过,在这曲折的打怪升级过程中,这其中有两个关键性的版本,一个是 1.2.24,一个是 1.2.47。前者是官方主动说该版本有反序列化漏洞,开启了 fastjson 反序列化研究的道路,后者是护网期间诞生的一个梦幻般的绕过。1.2.24 及之前没有任何限制,从该版本后逐渐增加了黑名单限制、默认关闭 AutoType 等,安全更新大都因为黑名单被绕过,直到 1.2.47 版本左右,有人发现了一种利用 cache 绕过限制的方法,而且这种方法可以向前通杀很多版本,但是 1.2.24 版本却不能用,究竟可以杀到那个版本,我自己调了一下代码,结论如下:
- 1.2.33 - 1.2.47 无条件利用
- 1.2.25 - 1.2.32 未开启 AutoType 可以利用,开启反而不能 (默认关闭)
- 1.2.24 无条件利用
cache 机制是从 1.2.25 添加的,我当时很好奇为何这个开启了 AutoType 反而不能用了,发现原因是这两行代码:
|
|
这段代码只在开启了 AutoType 时会执行到,但 25 版本少了一个判断,导致 cache 的利用机制失效了。综合来看 47 这个版本的 poc 基本是通杀的,但 25~32 几个版本手动开了 AutoType 就检查不到了,只能发一个别的 payload 来检测,我曾花费很多力气来尝试把两个 payload 合二为一,但后来发现做的是无用功,因为这两个关键版本的 payload 本质上是互斥的。
没有办法只能求次发两个包解决,其中 payload1 是”通杀“ payload,payload2 是 1.2.24 ~ 1.2.41 在启用 AutoType 时可用的 payload,这两个结合就覆盖了所有的 case。 细心的同学会发现每个数据都套了一层随机数,这么做的原因是我发现 Java Web 中可以通过 annotation 来做类型绑定,大意是可以指定 /user
的数据类型是 User,如果 Server 收到的数据是这样的 {"@type": "com.sun.rowset.JdbcRowSetImpl"}
,数据指定的类型和 User 不匹配时会报错,这是我在测试 vulhub 靶站时发现的。通过这样一个小的优化可以提高 payload 的命中率。
|
|
自动化实现
检测方式和 payload 都确定了,就可以开始写代码了。有个问题摆在了眼前,如何利用 RMI 服务来做自动化检测。 回想一下漏洞检测常用的方式:
- 有回显的检测
- 布尔/时间盲检测
- 反连平台检测
fastjson 的这个问题明显属于第三种,它需要一个外部服务来告诉我们漏洞有没有触发,我们称这种服务为反连平台。白帽子们最常用的 xss 平台就是一个 http 服务的反连平台,检测 ssrf 漏洞时也常用反连平台来作为辅助平台,那么我们能不能设法实现一个基于 rmi 服务的反连平台?
一些图省事的同学可能会说直接用 java 启动一个 rmi 服务就可以了,这样做的问题是比较多的,一方面 xray 是用 go 写的,再套一个 java 会很奇怪。而且就算可以用 java,我们也需要为每个检测目标启动不同的服务,因为在同时扫描多个网站时,需要鉴别漏洞请求来源于哪个网站。这其实牵涉到反连平台的一个关键问题:如果做请求关联,就是需要知道这条反连的请求是扫描那个目标时触发的。
有个简单的方案是根据端口来区分,rmi 本质上是一个 socket 服务,我们可以在发送 payload 前启动一个随机的 socket 服务,然后将这个 socket 服务的端口填入 payload 中,内部只需要维持一个 map{“port” -> “request”} 即可。理论上是可行的,但这样需要启动大量的 socket 服务来监听端口,听着就很脏,有没有更好的方法呢?
我们上面输入的 dataSourceName
中输入的是 rmi://127.0.0.1:1099/aaa
,/aaa
这一部分像极了 http 的 path,我们设法取到这个值理论上就和 http 服务的反连平台基本一致了。不妨来看看 RMI 服务的协议,https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html#overview, 发现这个协议还挺简单的,我用 wireshark 调了一下,大致流程是:
- client -> server dial tcp and send
1 2
4a 52 4d 49 00 02 4b J R M I Version Protocol(StreamProtocol)
- server -> client, repsond with client infos其中 127.0.0.1:54169 对于 server 来讲就是 socket.RemoteAddr
1 2
4e 0009 3132372e302e302e31 0000 d399 ProtocolACK Length 127.0.0.1 54169
- client -> server, call
1 2
50 xxxxxxxxxxxxxxx Call SerializationData
这里的 SerializationData 其实就是 String 的序列化数据,这里面必然包含这我们想要的那个 path, 我在实现时并没有按照 java 序列化数据的格式去乖乖读取,而是用了一个简单的办法,我发现 String 的序列化数据的真正内容都在最后面,那么我其实从后往前读取就可以找到想要的 path,具体方法可以从后往前读固定的长度,也可以给path 设置一个标记符,读到就结束,我用的是后者。
至此,我们把上面讨论的内容用代码串起来就可以做到 fastjson 的高效自动化检测了,该插件现已加入 xray 高级版,欢迎体验。我取了4个版本相对关键的 fastjson 版本验证了一下,效果图如下:
一点想法
上面的实现还有个我觉得不够完美的点,由于我自行实现的 RMI 只实现了握手部分,取得 path 后就关掉连接了,这其实会导致服务端有一个异常信息。其实有时间的话完全可以把剩下的协议部分实现以下,返回一个最简单的结果就可以,这个留给大家去发挥吧。
在研究这个漏洞时,发现大家的研究点都集中在漏洞利用上,然而发现漏洞其实是利用漏洞的起点,而如何高效、自动化的检测漏洞也是非常值得我们去思考和研究的。由于我之前没接触过 Java,很多都是花三分钟现学的,虽然文中结论我大都自己调试过,但精力有限,如有错误,欢迎与我联系改正。