JSON Parsers 差异安全问题探索

  • A+
所属分类:安全文章

本文由团队大佬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还是解析错误,都是允许的。

obj = {"test": 1, "test": 2}


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

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


下面是一个优先级差异导致安全问题的场景,Cart SERVICE执行订单校验逻辑,校验通过后转发至Payment SERVICE进行支付相关逻辑:
JSON Parsers 差异安全问题探索
恶意payload,第二类商品包含了重复键qty:

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 {} @ ${}/unitn".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}

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,就会导致安全问题发生:

HTTP/1.1 200 OK...Content-Type: text/plain
Receipt:5x Product A @ $100/unit1x Product B @ $200/unit
Total Charged: $300


2.字符截断和注释


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



字符截断


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


{"test": 1, "test[raw x0d byte]": 2} {"test": 1, "testud800": 2}{"test": 1, "test"": 2}{"test": 1, "test": 2}


这类畸形字符串,对多轮解析和序列化/反序列化来说,结果是不稳定的。例如U+D800U+DFFF在UTF-16中是一个空段,即这些码点永久保留不映射到任何Unicode字符。当其被当做UTF-8解码时,会被认为是非法字符。
参考:Unicode编码解析


所有示例字符串都与第一节中的示例有相同的利用方式,但是,某些允许对非法Unicode进行编码和解码的环境(例如Python 2.x),在进行序列化和反序列化字符串时,可能容易受到复杂的攻击。


让我们从Python 2.x 中unicode编码/解码的行为开始:

➜  ~ pythonPython 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 darwinType "help", "copyright", "credits" or "license" for more information.>>> import json>>> import ujson#序列化非法字符>>> u"asdfud800".encode("utf-8")   'asdfxedxa0x80'>>> json.dumps({"test": "asdfxedxa0x80"})'{"test": "asdf\ud800"}'#尝试分别用标准库json及第三方库ujson对字符串进行反序列化>>> json.loads('{"test": 1, "test\ud800": 2}'){u'test': 1, u'testud800': 2}>>> ujson.loads('{"test": 1, "test\ud800": 2}'){u'test': 2}>>>

JSON Parsers 差异安全问题探索下面是针对该问题的利用场景,攻击者可以使用解析缺陷绕过权限检查。例如,创建一个superadminud888用户,该用户可能在进行权限检查时被认为是superadmin用户。前提是目标系统支持对非法的unicode字符编码/解码,并且数据库及系统不会抛出异常(比较困难)。


如下为一个多用户系统,其中组织管理员允许创建自定义的用户角色,此外,superadmin角色拥有跨组织访问权限
JSON Parsers 差异安全问题探索
首先,尝试创建一个superadmin权限的用户:


POST /user/create HTTP/1.1Content-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解析器,为了影响下游解析器,我们创建一个恶意角色:


POST /role/create HTTP/1.1...Content-Type: application/json
{ "name": "superadminud888"}

HTTP/1.1 200 OK...Content-type: application/json
{"result": "OK: Created role 'superadminud888'"}

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

POST /user/create HTTP/1.1...Content-Type: application/json
{ "user": "exampleUser", "roles": [ "superadminud888" ]}

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": [ "superadminud888" ]}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)
# "superadminud888" 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解释器环境中的无引号值和注释语法(例如:/* */),但这不是正式规范的一部分,支持此类功能的解析器可以处理如下字符串:


obj = {"test": valWithoutQuotes, keyWithoutQuotes: "test" /* 支持注释 */}


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

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



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

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 有如下输入及输出
输入:

obj = {"test": 1, "test": 2}

输出:

obj["test"] // 1obj.toString() // {"test": 2}

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



生成重复键值的字符串


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

obj = {"test": 1, "test": 2}

输出:

obj["test"// 2obj.toString() // {"test": 1, "test": 2}


4.浮点数及整数表示



大数解码不一致


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

999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999


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

9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999.999999999999999e951E+9609223372036854775807

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

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(非数字)。但是许多解析器都有自己的处理方式,并且可能导致多种不同结果:
输入:

{"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被认为是相同的:

<?phpecho 0 == 1.0e4096 ? "True": "False" . "n"; # Falseecho 0 == "Infinity" ? "True": "False" . "n"; # True?>


5.宽容解析与一次性bug



尾部污染


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

POST / HTTP/1.1...Content-Type: application/x-www-form-urlencoded
{"test": 1}=

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


拒绝服务


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


重要提醒!

团队现开了微信交流群团队语雀知识库(不定期知识分享)及知识星球(小范围精华内容传播及问答),欢迎加入(微信群通过公众号按钮“加入我们”获取联系方式)

团队公开知识库链接:

https://www.yuque.com/whitecatanquantuandui/xkx7k2

知识星球:

JSON Parsers 差异安全问题探索

往期经典


《从入门到秃头之PWN蛇皮走位》

图形验证码绕过新姿势之深度学习与burp结合

漏洞挖掘|条件竞争在漏洞挖掘中的妙用

移动安全-APP渗透进阶之AppCan本地文件解密

漏洞笔记|记一次与XXE漏洞的爱恨纠缠

内网渗透之从信息收集到横向独家姿势总结-linux篇

HVV前奏|最新版AWVS&Nessus破解及批量脚本分享

Android抓包总结-HTTPS单向认证&双向认证突破


JSON Parsers 差异安全问题探索

扫描二维码 |关注我们

             微信号 : WhITECat_007  |  名称:WhITECat安全团队

本文始发于微信公众号(WhITECat安全团队):JSON Parsers 差异安全问题探索

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: