跳到主要内容

工程实践:嵌套 JSON 参数的自动化测试

· 阅读需 9 分钟
Yak ProjectYak Project

JSON( JavaScript Object Notation )是一种轻量级的数据交换格式,常用于 Web 应用程序中的数据传输。在 HTTP 数据包信息传递时,JSON 扮演着非常正常的角色,因为它是一种通用的数据格式,可以被多种编程语言和应用程序所支持。

当客户端向服务器发送 HTTP 请求时,请求头中可以指定请求体的数据格式为 JSON 。服务器在接收到请求后,可以解析 JSON 数据并进行相应的处理。同样地,当服务器向客户端返回 HTTP 响应时,响应头中可以指定响应体的数据格式为 JSON 。客户端可以解析 JSON 数据并进行相应的处理。

背景

我们平时遇到的 JSON 接口非常多,但是 JSON 中的各种字段测试以及不同深度的 JSON 测试其实非常难自动化测试。在安全服务测试和漏洞挖掘中,遇到这种情况,大多数可能只能手动测试,或者只测试少数几层。当然这个问题其实由来已久,在一些场景下这种情况大多数通过堆人力来解决,我们如何解决这个问题?或者我们有没有一些更优秀的解决方案?

我们以一个很经典的实际案例来说明这种情况,如果一个数据包长这个样子:

GET / HTTP/1.1
Host: www.example.com
Content-Type: application/json; charset=UTF-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

{
"abc": 123,
"foo": "bar",
"obj": {
"deep1": 1,
"deep2": "deepStr",
"depth3": {
"ccc": 1,
"ddd": 1
}
}
}

向右滑动查看完整代码

那么我们如何对这个数据包进行测试呢?比如说我们的 payload 是{{payload}} 那么,我们希望获得如下结果:

{
"abc": {{payload}},
"foo": "bar",
"obj": {
"deep1": 1,
"deep2": "deepStr",
"depth3": {
"ccc": 1,
"ddd": 1
}
}
}

甚至

{
"abc": 123,
"foo": "bar",
"obj": {
"deep1": {{payload}},
"deep2": "deepStr",
"depth3": {
"ccc": 1,
"ddd": 1
}
}
}

基础方法

先说结论吧,我们在 Yaklang 的 fuzz 模块中实现了这样的变换,可以递归深度遍历 JSON 的 Key/Value,并且同时实现替换的功能。很简单地,我们可以编写一段 Yaklang 代码很简单地实现这个功能:

熟悉 Yaklang Fuzz 模块的同学对上述的结果其实并不陌生,实际上这个问题已经得到了很好的解决。但是往往渗透测试中遇到的 JSON 可并不是这么简单。

难度升级:如果JOSN内容是在GET/POST参数中呢?

如果本身参数是a=123&&b=123&&key=value1&&obj={"abc": 123, "keyInQuery": "ccc"} 这种情况呢?我们再来看另一个数据包

GET /file.php?a=123&&b=123&&key=value1&&obj=%7B%22abc%22%3A+123%2C+%22keyInQuery%22%3A+%22ccc%22%7D HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/83.0.4103.116

向右滑动查看完整代码

那么这种情况,其实就非常令人恐惧了。我们这个数据包中,包含 4 个 GET 参数,虽然表面上包含四个参数,但是参数中有一个obj 其实非常复杂,他是被编码的,并且还包含了abckeyInQuery这两个隐藏参数。

也就是说,上面这个数据包,实际包含了 6 个参数:

  • GET 参数:a
  • GET 参数:b
  • GET 参数:key
  • GET 参数:obj
  • GET 参数嵌套 JSON 参数:obj.abc
  • GET 参数嵌套 JSON 参数:obj.keyInQuery

一般来说,我们可能只能测试到 a, b, key, obj这四个参数,并不能测试到obj.abcobj.keyInQuery 参数。如果遇到这种情况,原有的方法可能就无法生效了,那么我们如何解决呢?

Show Me the CODE!

freq = fuzz.HTTPRequest(`GET /file.php?a=123&&b=123&&key=value1&&obj=%7B%22abc%22%3A+123%2C+%22keyInQuery%22%3A+%22ccc%22%7D HTTP/1.1
Host: www.example.com
Content-Type: application/json; charset=UTF-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
`)~

for param in freq.GetGetQueryParams() {
req = param.Fuzz("___________").GetFirstFuzzHTTPRequest()~
println(codec.DecodeUrl(req.GetRequestURI())~)
}

/*
/file.php?a=123&b=123&key=value1&obj={"abc":"___________","keyInQuery":"ccc"}
/file.php?a=123&b=123&key=value1&obj={"abc":123,"keyInQuery":"___________"}
/file.php?a=123&b=123&key=value1&obj=___________
/file.php?a=___________&b=123&key=value1&obj={"abc": 123, "keyInQuery": "ccc"}
/file.php?a=123&b=___________&key=value1&obj={"abc": 123, "keyInQuery": "ccc"}
/file.php?a=123&b=123&key=___________&obj={"abc": 123, "keyInQuery": "ccc"}
*/

向右滑动查看完整代码

我们按上面描述的内容,如果可以进行 payload 替换的话,我们应该有 6 个参数需要替换,通过 fuzz 模块中 HTTPRequest 构造一个模糊测试模版,然后通过内置的获取 GetQueryParams 方法。使用获取到的参数调用 Fuzz 方法,在每次 Fuzzing 后,使用 GetFirstFuzzHTTPRequest 方法获取第一个 Fuzzing 后的 HTTP 请求,并使用 DecodeUrl 方法解码请求 URI。最终,使用 println 方法将解码后的请求 URI 打印出来。

最后我们获取到的结果非常明显:

这两个参数已经可以成功被我们手动覆盖了,因此我们可以尝试对这类的所有数据包进行很精密的测试了。

仿真测试

我们首先手造了一个 /expr/injection 的路由,其中有三个参数,我们把 b 参数中的内容作为 JSON 进行反序列化,并且把 JSON 后对象的 "a" 参数取出来。然后把 “a” 的内容作为一个沙箱表达式进行执行。

我们对刚刚编写的靶场进行简单的测试:

发现只要有 b-json 参数的内容中的 a 为数值表达式的时候,它的结果为运算结果。这种漏洞如何进行自动发现呢?

全自动化测试

我们编写一个表达式注入的通用测试脚本:

freq = fuzz.HTTPRequest(`
GET /expr/injection?b={"a":1} HTTP/1.1
Host: 127.0.0.1:8787

`)~

for param in freq.GetGetQueryParams() {
try {
exprParams = fuzz.FuzzCalcExpr()
result := param.Fuzz(exprParams.expr).ExecFirst()~
if exprParams.result in string(result.ResponseRaw) {
println("----------------------------------")
println("----------------------------------")
println("--------------表达式执行------------")
println("----------------------------------")
println("----------------------------------")
}
} catch err {
dump(err)
}
}

向右滑动查看完整代码

我们在这个脚本中,需要测试表达式,通过 fuzz.FuzzCalcExpr 生成一个减法(加性)表达式,为了让表达式更加简单,我们认为它是一个“日期表达式”,类似 2012-12-21 这样的减法,这样它的计算结果为 1979。如果表达式执行了,页面应该会有 1979 的字样。

我们执行上述内容:

[INFO] 2023-05-15 14:25:56 [http_pool:612] start to send to http://127.0.0.1:8787/expr/injection?b=%7B%22a%22%3A%222011-03-9%22%7D(:0) (packet mode)
----------------------------------
----------------------------------
--------------表达式执行------------
----------------------------------
----------------------------------
HTTP/1.1 200 OK
Date: Mon, 15 May 2023 06:25:56 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 282

-----------------ORIGIN PACKET---------------
GET /expr/injection?b=%7B%22a%22%3A%222011-03-9%22%7D HTTP/1.1
Host: 127.0.0.1:8787

-----------------Handled---------------
a[]: last Stack Value is nil/undefined

b[{"a":"2011-03-9"}]: 1999

c[]: last Stack Value is nil/undefined

[INFO] 2023-05-15 14:25:56 [http_pool:612] start to send to http://127.0.0.1:8787/expr/injection?b=2018-05-9(:0) (packet mode)

向右滑动查看完整代码

确实,我们发送的数据包参数部分为 b=%7B%22a%22%3A%222011-03-9%22%7D解码后为 b={"a":"2011-03-9"},并且数据包中也有 “1999” 作为测试结果,这很符合我们的测试要求。

我们的代码最精彩的部分在于,没有写明测试的路径,仅仅是写明了测试的原始数据包,当然这个原始数据包的来源可以是任何地方,比如 Yakit MITM 模块中。

当然选取了一个 JSON 中的表达式注入作为测试案例,这个案例其实是非常具有代表性的,它很难被正常的扫描器,甚至启发式扫描算法检测到,并且甚至作为手动测试的时候,如果测试者忽略了这个小点也会漏掉;甚至很多通用框架型的漏洞也具有这个特征。

核心原理

这个算法看起来非常 amazing!但是他的核心原理其实并不复杂:代码部分开源在 https://github.com/yaklang/yaklang 仓库中的如下位置:

  • common/mutate/..
  • common/jsonpath/..

我们先用 JSON 对象的递归方法构建出它每个字段的 JSONPATH,然后我们使用构造出的一组 JSONPATH 对数据进行 Replace。因为递归构建参数的原因,并且 JSON 本身的数据是不存在环结构的,因此我们只要递归结束,就可以构建出所有的字段的 JSONPath 标记位置。

当每一个位置都可以被精确定位,只需要每个位置依次替换,就可以对任何位置的 JSON 进行替换了。

JSONPath 是一种用于在 JSON 数据中进行数据查询的语言。它类似于 XPath ,但是针对 JSON 格式的数据。使用 JSONPath 可以方便地从 JSON 数据中提取出所需要的数据,非常适合用于 API 开发和数据分析等场景。例如,可以使用 JSONPath 从 JSON 数据中提取出特定字段的值,或者根据条件过滤出符合要求的数据。JSONPath 语法简洁易懂,可以通过点号和方括号来访问 JSON 对象和数组中的元素。

fuzz.HTTPRequest 我们设计了一套链式 API 以达到模糊测试的目的,这种模糊测试可以自动提取所有的参数,我们寻找到 GET/POST 中参数的时候,可以检查它参数中的值是否是 JSON,如果是 JSON 的话,可以采用上面提到的 JSONPath 标记法生成对应的可模糊测试的模版对象。这样就接入了我们已有的基础设施中。

我们把上面提到的所有技术实现,开放在 YakVM 中,就可以在 Yaklang 中直接使用到这一套组合算法,并且它可以完美融合进 MITM 的测试过程和任何爬虫过程。


本文首发于 Yak Project 公众号,阅读原文