在上一篇文章中,我们为读者解释了不同的JSON解析器的解析结果不一致的原因,以及JSON在互操作性方面存在的安全风险,在本文中,我们将继续与读者一起探索JOSN的互操作性安全漏洞以及防御措施!
(接上文)
3. JSON序列化的怪癖
到目前为止,我们只关注了JSON解码,但几乎所有的实现也都提供了JSON编码(也就是序列化)。下面,让我们来看几个具体的例子。
不一致的优先级:反序列化与序列化
传统的防御思路是避免重复键,对于内部服务来说,这很容易做到,但对于外部用户输入来说,事情就没有这么简单了。所以,并不是所有的解析器都会检查有重复键的行为。在下面的例子中,解析器(Java的JSON-iterator)将产生如下所示的输入和输出。
输入:
obj = {"test": 1, "test": 2}
输出:
obj["test"] // 1 obj.toString() // {"test": 2}
如上图所示,检索到的键的值,与序列化处理后得到的值是不一致的。我们看到,底层数据结构似乎保留了重复键的值;但是,序列化器和反序列化器之间的优先级是不一致的。
生成具有重复键的文档
根据规范,序列化重复的键是可以接受的,并且某些解析器(例如,C++的rapidjson)就是这样做的。
输入:
obj = {"test": 1, "test": 2}
输出:
obj["test"] // 2 obj.toString() // {"test": 1, "test": 2}
在这些情况下,重新序列化已解析的JSON对象并不能提供相应的安全保护。因为这些序列化行为允许攻击者跨过滤层走私各种值。正如我们之前所看到的,这可能会导致业务逻辑漏洞、注入漏洞或其他安全威胁。
4. 浮点数和整数的表示法
既然我们已经考察了重复键的诸多风险,接下来,我们将开始研究数字表示法带来的安全问题。首先,下面的内容摘自讨论数字互操作性的RFC文档:
由于实现IEEE 754 binary64(双精度)数字[IEEE754]的软件是普遍存在并被广泛使用的,因此,良好的互操作性可以通过下列方式来实现,即实现这些规范的时候,让期望实现的精度或范围不超过提供的精度或范围,也就是说,这些实现会在期望的精度范围内非常接近JSON数字。像1E400或3.141592653589793238462643383279这样的JSON数字非常适合用于演示潜在的互操作性问题,因为它表明创建这些数字的软件期望接收它们的软件在数值的大小和精度方面,比常见的软件具有更大的包容性。
请注意,当使用这样的软件时,只有大小介于[-(2**53)+1,(2**53)-1]之间整数才是可以互操作的,因为实现将精确地确定它们的数值。
下面,让我们先来看一下大数。
不一致的大数解码
如果解码不准确的话,大数可能被解码为MAX_INT或0(或MIN_INT,因为我们接近负无穷大)。对于不同的解析器来说,同一个大数的解码结果可能是不一致的,如:
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
可以解码为多种表示形式,包括:
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9.999999999999999e95 1E+96 0 9223372036854775807
下面,我们通过具体的例子进行说明。
例子: 不一致的大数解码
我们再来看看实验1。我们知道,Payments API中使用的第三方Golang jsonparser库会将大数解码为0,而Cart API会忠实地解码数字。我们可以利用这个不一致的地方来获得免费的商品。下面,让我们购买大量的电子礼品卡(id:8)。
请求:
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
业务逻辑层将忠实地解码整数,而支付处理层则将该大数解码为0。
在实验1的第2部分中,我们将通过lab1_alt_req.json中的请求尝试这种攻击方法。如实验中所述,jsonparser库可以通过正确的错误检查来检测此溢出漏洞。
具有无穷大的不一致类型表示
虽然官方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}
请注意从JSON数字到字符串的类型转换。在严格的比较中,这种类型转换可能不会引发不良后果,但在宽松的比较中,这种转换会导致类型篡改漏洞。例如,请考虑以下代码(注意:这里的字符串将被解释为0):
< ?php echo 0 == 1.0e4096 ? "True": "False" . "\n"; # False echo 0 == "Infinity" ? "True": "False" . "\n"; # True ? >
与前面的例子一样,业务逻辑层可能会错误地验证那些解码不一致的值。因此,我们最好在使用前进行严格的比较或执行类型验证。
5. 权限解析和One-off漏洞
有些解析器对文档中的杂散字符、替代引号字符和语法错误持包容态度,而其他解析器则严格执行RFC定义的语法。让我们来看看与重复键无关的权限解析的实例。
尾随垃圾(Trailing Garbage)
允许尾随垃圾是许多JSON解析器的一个众所周知的问题,多年来一直被滥用于进行跨站请求伪造(CSRF)攻击。为了绕过同源策略(SOP)“简单请求”的限制,可以在发布JSON文档时在尾部加上一个等号,以表示这是一个x-form-urlencoded文档,这在跨源请求中是允许的。例如:
POST / HTTP/1.1 ... Content-Type: application/x-www-form-urlencoded {"test": 1}=
忽略Content-Type并将所有请求作为JSON处理的服务将容易遭受这些类型的CSRF攻击。
拒绝服务:分段错误
我们发现,有两个解析库在遇到格式错误的JSON时都会发生崩溃。这两个实例都已报告给维护人员,当这两个库修复该漏洞后,我们将公布它们的名称。
也就是说,为了提高性能,许多解析器依赖于可能易受内存安全问题影响的低级例程。因此,我们一定要多留心依赖本机代码的解析器。
关于二进制JSON解析器
到目前为止,我已经简要测试了BSON、MessagePack、UBJSON和CBOR格式,以及它们各自的解析器的示例。这些解析器也存在许多同样的问题。尽管一些序列化器拒绝创建具有重复键的二进制表示,但我们可以通过字节交换手动创建恶意文档,下面是可用于MessagePack的例子:
json_doc = {u'test': 1, u'four': 2} # use an ABI-compatible string to replace (e.g., 'four') encoded = msgpack.packb(json_doc, use_bin_type=True) encoded = encoded.replace(b'four', b'test')
接下来,我们用Dot Net MessagePack反序列化器处理这个文档,并使用其内置的JSON序列化器。
Console.WriteLine(MessagePackSerializer.ConvertToJson(File.ReadAllBytes("msgpack.bin")));
我们将得到以下输出:
{"test":1,"test":2}
如果我们看一下该文档,就会发现许多规范都试图向后兼容JSON。也就是说,像BSON这样的规范确实试图更明确地说明如何处理重复键这样的情况。
当然,在这些二进制格式中还有更多的互操作性问题有待于我们去深入挖掘。
解析器的行为调查结果
本调查包括下列语言相关的49种解析器,同时涉及标准库解析器(如果有的话)和第三方解析器:
· C/C++
· C#
· Elixir/Erlang
· Go
· Java
· JavaScript
· PHP
· Python
· Ruby
· Rust
(关于解析器和版本号的完整列表,请参阅附录A。)
下面的结果定义了具有不常见行为的解析器。这些行为违背了相关规范和/或官方规范的规定。并且,其中一些行为是经过深思熟虑的设计选择,并由相应的超级规范(例如,JSON5)加以定义。
除了分段错误之外,这些行为在单个解析器的上下文中都是无害的,这可以防止它们被归类为特定解析器的漏洞。然而,正如我们所观察到的,这种行为可能会通过互操作性引入安全风险。因此,以下行为在未来可能会发生改变,也可能不会改变:
重复键的优先级:
· Go[jsonparser]
· Go[gojay]
· C++[rapidjson]
· Java[json-iterator]
· Elixir[Jason]
· Elixir[Poison]
· Erlang[jsone]
字符截断:
· 因不配对的代字符导致的碰撞
Python[ujson]
PHP [json5]
· 因反斜杠和回车字节(0x0d)导致的碰撞
Rust[json5]
PHP[json5]
· 由杂散引号引发的碰撞
Ruby[simdjson]
· 因杂散反斜杠 { "test": 2, "te\st": 1}引发的碰撞
C#[Jayrock.json]
Ruby[stdlib/ext]
Ruby[stdlib/pure]
JavaScript[json5]
Rust[json5]
· 拒绝服务(分段故障):
(Two instances) To be announced following remediation
· 注解截断:
Java[json-iterator]
Ruby[simdjson]
· 字符串化的重复键:
C++[rapidjson]
C#[MessagePack] (v2.2.85)
· 非预期的注释支持(非JSON5/HJSON解析器):
Ruby[stdlib/ext]
Ruby[stdlib/pure]
Ruby[oj]
Ruby[Yajl]
Go[jsonparser]
Go[gojay]
Java[GSON]
Java[Genson]
Java[fastjson]
C#[Newtonsoft.Json]
C#[Utf8Json]
C#[Jayrock.Json]
C#[Manatee.Json]
· 大数(转换为字符串或“Infinity”):
Python[stdlib/json] – In: 1.0e4096, Out: “Infinity”
Python[ujson] In: 1.0e4096, Out: “Inf”
Java[Jackson] – In: 1.0e4096, Out: “Infinity”
Java[Genson] – In: 1.0e4096, Out: “Infinity”
Java[Jodd] – In: 1.0e4096, Out: “+Infinity”
C#[Manatee] – In: 1.0e4096, Out: “Infinity”
C#[Newtonsoft.Json] – In: [9 repeated 96 times] Out: “[9 repeated 96 times]”
· 大数(四舍五入)
Ruby[oj] – In: 1.0e4096, Out: 3.0e14159265358979323846
C#[Utf8Json] – In: 1.0e4096, Out: 9.218868437227405E+18
PHP[jsonlint] – In: [9 repeated 96 times] Out:9223372036854775807
· 大数(返回0,不进行错误检查):
Go[jsonparser] – In: 1.0e4096, Out: 0,
总的来说,在所调查的49个JSON解析器中,每种语言都至少有一个解析器表现出一种潜在的危险的互操作性行为。标准库提供的解析器往往是最合规的,但它们往往速度较慢,而速度在微服务架构中已经越来越重要。这促使开发人员选择性能更强的第三方解析器。
补救措施:我们如何才能降低互操作性风险?
当JSON还只是ECMAScript标准的一部分时,很难想象它的应用会如此广泛。从这个RFC和上面讨论的互操作性漏洞中,我们可以学到一些很好的经验。我在下面概述了针对不同受众的补救措施和测试指南。
对于JSON解析器维护者来说:
· 让重复的键导致严重的解析错误。
· 不要执行字符截断。相反,用占位符替换无效的Unicode(例如,未配对的代用符应显示为Unicode替换字符U+FFFD)。截断可能会破坏多解析器应用程序的安全过滤程序。
· 避免偏离所选择的规范,或提供一个“严格”模式,使其遵守RFC 8259(或相关)定义。
· 在处理不能如实表示的整数或浮点数时产生错误。
对于软件工程师来说:
盘点整个体系结构中的现有的解析器。使用提供的测试用例、可用的文档或根据上面现有的行为列表确定解析器中的行为偏离程度。
对于安全人员来说:
这些都是微妙的攻击手法,而且不容易从外部识别出来。如果你可以访问源代码,请检查是否使用了具有已知怪癖的解析器。利用重复键进行安全测试,并使用Labs README中的建议来尝试诱发碰撞。
关于JSON模式验证器
JSON Schema规范可以帮助简化和加强类型安全和约束,但它不能帮助处理重复键。JSON Schema实现本身并不执行JSON解析,而只处理解析后的对象。为了证明这一点,我在本篇博文所附的漏洞实验中使用了JSON Schema。
例如,Java的JSON Schema实现需要使用org.json.JSONObject的输入,而Python的json-schema库则依赖于Python Dictionary对象,具体如下图所示:
import jsonschema schema = { "type": "object", "properties": { "test": { "type": "integer", "minimum": 0, "maximum": 100, } } } jsonschema.validate(instance={"test": 1}, schema=schema)
JSON Schema可以帮助缓解某些解析漏洞,比如类型检查和限制允许的整数范围。但不一致的解析将是JSON Schema的一个盲点。
什么是JSON LINTERS?
JSON linters用于处理字符串输入,通常用于IDE或编程环境。Linter只是一个解析器,通常不会返回一个解码的对象。理想情况下,我们希望解码解析器能够保持一致的解析结果。
小结
解析差异将一直是微服务安全的主题。虽然这些攻击的条件可能很难检测,但实验证明,即使是JSON这样看似良性的标准,也会引入意想不到的互操作性怪癖,从而在更复杂的系统中导致严重的安全问题。
最后,在设计协议或标准时,将行为限制在确定性的结果上,不仅可以提高互操作性,还更易于报告错误并改进我们的软件。通过定义以前未定义的行为来打破现有标准中的向后兼容性,可能会引起许多问题。但在微服务架构的现代背景下,互操作性变得越来越复杂,这可能是一个值得的选择。
最后,感谢InfoSec Twitter给我带来了另一个关于JSON解析怪癖的很棒的资源:http://seriot.ch/parsing_json.php (2018),作者Nicolas Seriot (@nst021)。建议大家一定要看看这个资料,它对于深入理解JSON解析非常有帮助。此外,它还包括本文未涉及的一些编程语言(如Perl、Lua、Swift等)的解析器怪癖。
附录A:版本号
本文翻译自:https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities如若转载,请注明原文地址: