JSON互操作性漏洞探索

2021-02-27 09:15:25

TL; DR相同的JSON文档可以在微服务中使用不同的值进行解析,从而导致各种潜在的安全风险。如果您喜欢动手实践的方法,请尝试一下实验,当实验吓到您时,请返回并继续阅读。

JSON是Web应用程序通信的基础。 JSON的简单性通常被认为是理所当然的。我们通常不将JSON解析作为威胁模型的一部分。但是,在我们现代的多语言微服务体系结构中,我们的应用程序通常依赖于几个单独的JSON解析实现,每个实现都有自己的怪癖。

正如我们通过HTTP请求走私这样的攻击所看到的那样,解析器之间的差异与多阶段请求处理相结合会带来严重的漏洞。在这项研究中,我对49个JSON解析器进行了调查,对它们的怪癖进行了分类,并提出了各种攻击方案和Docker Compose实验以突出它们的风险。通过我们的付款处理和用户管理示例,我们将探索JSON解析不一致如何掩盖其他良性代码中严重的业务逻辑漏洞。

即使在最佳情况下实施,也不可避免地会出现与规格的微小,无意的偏差。但是,JSON解析器还有其他一些挑战。即使在正式的JSON RFC中,也存在一些主题的开放式指南,例如如何处理重复的键和表示数字。尽管此指南后面是关于互操作性的免责声明,但是大多数JSON解析器的用户并没有意识到这些警告。

IETF JSON RFC(8259及更低版本):这是官方的Internet工程任务组(IETF)规范。

ECMAScript标准:对JSON的更改与RFC版本同步发布,该标准参考RFC以获取有关JSON的指导。但是,JavaScript解释器提供的非规范便利(例如无引号的字符串和注释)启发了许多解析器。

JSON5:此超集规范通过显式添加便捷功能(例如,注释,替代引号,无引号的字符串,尾部逗号)来增强官方规范。

那么,为什么某些解析器会开始选择性地合并其他人忽略的功能,或者采用与解析器行为相矛盾的方法呢?

如以下各节所述,处理重复键和表示数字的决定通常是开放式的。我怀疑这可能是由于该规范在实施变得流行之后被发布了。也许设计委员会决定不破坏与预规范JSON解析器的向后兼容性,包括原始的JavaScript实现。

但是,这些决策将继续通过生态系统传播到JSON5和HJSON之类的超集规范中,甚至传播到BSON,MessagePack和CBOR之类的二进制变量中,我们将在后面讨论。

进一步的互操作性问题来自对数字和字符串编码的延迟指导。在规范的2017年修订版中,仅明确要求字符串编码为UTF-8。

信条说他们是不同的。信条和帕姆都是正确的,但这取决于你问谁。

我们中的许多人在我们的开发工作中都遇到了JSON的这个怪癖:如果您有重复的密钥,会发生什么?

obj [" test"]的值是1还是2,还是会产生错误?

根据官方规范,任何这些结果都是可以接受的。令人惊讶的是,我什至遇到了开发人员直接利用重复的键优先级来创建自文档化JSON的情况。这是一个示例:

//对于最后一个键优先的解析器,在解析过程中第一个“测试”键将被忽略。obj = {" test&#34 ;:"这是测试字段的描述&#34 ;," test&#34 ;:" Actual Value"}

您可能会惊讶地发现,该规范中的指导是描述性的而非规范性的。以下是IETF JSON RFC(8259)的最新版本:

从名称接收到的所有软件实现都将在名称-值映射上达成一致的意义上来说,名称都是唯一的对象是可以互操作的。如果对象中的名称不是唯一的,则接收到该对象的软件的行为是不可预测的。许多实现仅报告姓/值对。其他实现报告错误或无法解析对象,某些实现报告所有名称/值对,包括重复项。观察到JSON解析库在是否使调用软件可见对象成员的顺序方面存在差异。行为不取决于成员顺序的实现将可以互操作,因为它们不会受到这些差异的影响。

我怀疑规范不希望破坏与规范前解析器的向后兼容性。公平地说,注意到了互操作性问题。但是,如前所述,从实际意义上讲,有多少开发人员阅读JSON RFC,或考虑使用这种简单格式的互操作性问题?不用说,以上规范中的语言与RFC中常见的显式和直接指导有很大不同。

让我们考虑一个电子商务应用程序,在该应用程序中,我们具有执行业务逻辑的购物车服务,将该请求转发到付款服务以进行付款处理,并执行订单履行。让我们尝试免费获得一些东西。本示例将使用以下描述的设计:

假设购物车服务收到这样的请求(请注意购物车中第二个项目的重复数量键):

POST /购物车/结帐HTTP / 1.1 ...内容类型:application / json {" orderId&#34 ;: 10," paymentInfo&#34 ;: {// ...},&#34 ; shippingInfo&#34 ;: {// ...}," cart&#34 ;: [{" id&#34 ;: 0," qty&#34 ;: 5},{& #34; id&#34 ;: 1," qty&#34 ;: -1," qty&#34 ;: 1}]}

如下所示,购物车服务在将订单发送到支付服务之前强制执行业务逻辑。该API是使用Python Flask编写的,并使用Python标准库JSON解析器,该解析器对重复的键使用last-key优先级(对于id:1,数量= 1);

@ app.route(' / cart / checkout&#39 ;, methods = [" POST"])def checkout():#1a:使用Python stdlib解析器解析JSON正文。数据= request.get_json(force = True)#1b:使用jsonschema验证约束:id:0< = x< = 10和qty:> = 1#请参阅架构jsonschema.validate(instance)的完整源代码= data,schema = schema)#2:处理付款分别= request.request(method =" POST&#34 ;, url =" http:// payments:8000 / process",data = request.get_data(),)#3:将回执打印为响应,或者如果resp.status_code == 200:则产生通用错误消息:回执="回执:\ n"数据[&cart"]中的项目:收据+ =" {} x {} @ $ {} /单位\ n" .format(item [" qty&#34 ;],productDB [item [" id"]。get(" name"),productDB [item [" id"]]。get(&#34 ; price"))收据+ =" \ n总收费:$ {} \ n" .format(resp.json()[" total"])返回收据return&# 34;付款处理过程中出现错误"

JSON主体将成功验证,因为忽略了重复的键,并且所有解析的值都满足约束。现在,JSON被认为是安全的,原始的JSON字符串(request.get_data())被转发到Payments服务。从开发人员的角度来看,为什么要在字符串输入易于使用时通过重新序列化刚刚解析并验证的JSON对象来浪费计算量?这个假设应该是正确的。

接下来,在付款服务处收到请求。此Golang服务使用高性能的第三方JSON解析器(buger / jsonparser)。但是,此JSON解析器使用第一键优先级(对于id:1,数量= -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){//检索重复键的第一个实例,包括qty = -1 id,_:= jsonparser.GetInt(value," id")qty,_:= jsonparser。 GetInt(value," qty")total =总数+ productDB [id] [" price"]。(int64)* qty;}," cart")/ / ...处理付款金额“总额” //返回总值'到购物车服务以生成收据。 io.WriteString(w,fmt.Sprintf(" {\" total \&#34 ;:%d}" total))}

购物车服务接收从支付服务收取的总费用,并生成响应收据。我们查看了购物车服务中的收据,但看到错误。我们将向您运送价值700美元的六种产品,但我们仅收取了300美元的费用:

但是,在此示例中,经过验证的JSON文档在解析后并未重新进行字符串化。而是使用原始请求中的JSON字符串。在第3节JSON序列化怪癖中,我们将探讨仍传播风险怪癖的重新字符串化对象的示例。

还可以通过字符截断和注释来引发键冲突,从而增加受重复键优先级影响的解析器的数量。

有些解析器在字符串中出现特定字符时会截断它们,而其他解析器则不会。这可能导致将不同的密钥解释为解析器子集中的重复项。例如,以下文档在某些最后关键字优先级解析器中似乎具有重复的关键字,而在其他文档中则没有:

{" test":1," test \ [原始\ x0d字节]":2} {" test":1," test \ ud800& #34 ;: 2} {" test&#34 ;: 1," test"&#34 ;: 2} {" test&#34 ;: 1," te \ st":2}

这些字符串表示形式通常对于多轮反序列化和反序列化是不稳定的。例如,通过U + DFFF的Unicode代码点U + D800是不成对的UTF-16替代,并且可以将未成对的替代编码为UTF-8字节字符串,但它被认为是非法的Unicode。

所有这些示例都可以与之前的示例和实验1相似的方式使用。但是,允许对非法Unicode进行编码和解码的环境(例如Python 2.x)可能容易受到需要存储(序列化)的复杂攻击的影响。并检索(反序列化)这些值。

$ python2>>>导入json>>>将ujson#序列化导入非法unicode。 >>> u&asdf \ ud800" .encode(" utf-8")' asdf \ xed \ xa0 \ x80'#重新序列化非法Unicode>>> json.dumps({" test&#34 ;:" asdf \ xed \ xa0 \ x80"})' {" test&#34 ;:" asdf \ \ ud800"}'让我们观察第三方解析器ujson的截断行为及其如何创建重复密钥。 ujson.loads(' {" test&#34 ;: 1," test \\ ud800&#34 ;: 2}'){u' test&#39 ;: 2 }

如我们在下一个示例中将看到的,攻击者可以使用此功能绕过清理检查,例如,创建并存储一个名为superadmin \ ud888的角色,该角色可以检索并解析为superadmin。但是,此技术需要支持对非法Unicode代码点进行编码和解码(不是那么难),以及具有不会抛出异常的类型系统的数据库(更难)。

在以下实验中,我们将以二进制模式使用Python 2.7和MySQL,以使我们能够专注于存储非法Unicode的风险及其对不一致的JSON解析的影响。

让我们考虑一个多租户应用程序,组织管理员可以在该应用程序中创建自定义用户角色。此外,我们知道具有跨组织访问权限的用户被分配了内部角色超级管理员。让我们尝试提升特权。本示例将使用如下所示的设计:

POST / user / create HTTP / 1.1 ... Content-Type:application / json {" user&#34 ;:" exampleUser&#34 ;," roles&#34 ;: [&#34 ;超级管理员]} HTTP / 1.1 401未经授权...内容类型:application / json {" Error&#34 ;:"内部角色分配' superadmin'被禁止"}

如上所示,用户API具有服务器端安全控件,以阻止用户创建具有superadmin角色的新用户。角色API共享此控件,以避免覆盖现有的用户定义角色和系统角色。在这里,我们将假设User API上的/ user /和/ role /端点使用行为良好的兼容解析器。

相反,为了影响下游解析器,我们将创建一个角色,其名称在整个解析器中都是不稳定的,即superadmin \ ud888:

POST / user / create HTTP / 1.1 ... Content-Type:application / json {" user&#34 ;:" exampleUser&#34 ;," roles&#34 ;: [&#34 ; superadmin \ ud888" ]} HTTP / 1.1 200 OK ...内容类型:application / json {" result&#34 ;:" OK:创建的用户' exampleUser'"}

用户API将用户存储到数据库中。到目前为止,所有解析器都将用户定义的角色(superadmin \ ud888)视为与内部角色superadmin不同的名称。

但是,当稍后访问跨组织的/ admin端点时,服务器从权限API请求用户权限。 Permissions API忠实地编码角色,如下所示:

但是这里出了问题:管理API使用了第三方ujson分析器。如前所述,该解析器会截断包含非法代码点的所有字节:

@ app.route(' / admin')def admin():用户名= request.cookies.get("用户名")如果不是用户名:返回{" Error&#34 ;:"在Cookie中指定用户名"}用户名= urllib.quote(os.path.basename(用户名))url =" http://权限:5000 / permissions / {}&#34 ; .format(用户名)resp = requests.request(方法=" GET&#34 ;, url = url)#" superadmin \ ud888"将简化为" superadmin" ret = ujson.loads(resp.text)如果resp.status_code == 200:如果" superadmin"在ret [" roles"]中:返回{" OK&#34 ;:"授予超级管理员访问权限"}否则:e = u"访问被拒绝。用户具有以下角色:{}" .format(ret [" roles"])返回{" Error&#34 ;: e},401其他:return {" Error& #34;:ret [" Error"]},500

如上所示,我们的用户定义角色将被截断为超级管理员,从而​​授予对特权API的访问权限。

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

给定两个支持无引号的字符串的解析器,但是只有一个可以识别注释的解析器,我们可以走私重复的键。考虑下面的示例:

到目前为止,我们只关注JSON解码,但是几乎所有实现都还提供JSON编码(也称为序列化)。让我们看几个例子。

传统的观点是避免重复的键,这对于内部服务很容易实现,但是无法通过外部用户输入来保证。因此,并非所有解析器都使用重复键来探索行为。在一个实例中,解析器(Java的JSON-iterator)产生了以下输入和输出:

如上所示,密钥检索和序列化的值不同。底层的数据结构似乎保留了重复键的值。但是,序列化程序和反序列化程序之间的优先级不一致。

按照规范,序列化重复的密钥是可以接受的,某些解析器(例如C ++的Rapidjson)可以做到这一点:

在这些情况下,重新序列化已解析的JSON对象不会提供保护。这些序列化行为使攻击者可以跨消毒层走私价值。如我们先前所见,这可能导致业务逻辑缺陷,注入漏洞或其他安全影响。

现在,我们已经观察到重复密钥的许多风险,我们将研究数字表示。首先,让我们看一下讨论数字互操作性的RFC摘录:

由于实现IEEE 754二进制64(双精度)数字[IEEE754]的软件通常可用并得到广泛使用,因此,期望实现的精度或范围不超过提供的精度或范围的实现可以实现良好的互操作性,在这种意义上,实现将在其中近似JSON数。预期的精度。诸如1E400或3.141592653589793238462643383279之类的JSON数字可能表示潜在的互操作性问题,因为它表明创建它的软件期望接收软件具有比广泛使用的更大的数字幅度和精度功能。请注意,使用此类软件时,整数表示并且在[-(2 ** 53)+1,(2 ** 53)-1]范围内的数字是可互操作的,因为实现将在数字上完全一致价值观。

如果解码不正确,则大数可能会解码为MAX_INT或0(或MIN_INT,因为我们接近负无穷大)。在多个解析器中,我们将发现大量的解析器,例如:

让我们重温实验1。我们知道,Payments API中使用的第三方Golang jsonparser库会将大数字解码为0,而Cart API将忠实地解码数字。我们可以利用这种不一致来获取免费物品。让我们购买大量的电子礼品卡(ID:8):

POST /购物车/结帐HTTP / 1.1 ...内容类型:application / json {" orderId&#34 ;: 10," paymentInfo&#34 ;: {// ...},&#34 ; shippingInfo&#34 ;: {// ...}," cart&#34 ;: [{" id&#34 ;: 8," qty&#34 ;: 9999999999999999999999999999999999999999999999999999999999999999

业务逻辑层忠实地解码该整数,而支付处理层默认为0表示大数。

在Lab 1:第2部分中使用lab1_alt_req.json中的请求尝试这种攻击。如实验室中所述,jsonparser库可以通过适当的错误检查来检测到此溢出。

正式RFC不支持正和负无穷大以及NaN(不是数字)。但是许多解析器都选择了解决方法。反序列化和/或重新序列化值可以导致多种结果,例如:

请注意从JSON数字到字符串的类型转换。严格比较中的类型转换可能是良性的,但在宽松比较中可能导致类型变戏法的漏洞。考虑以下代码(注意:字符串被解释为0):

<?phpecho 0 == 1.0e4096? "真&#34 ;:假#" 。 " \ n&#34 ;; #Falseecho 0 ==" Infinity" ? "真&#34 ;:假#" 。 " \ n&#34 ;; #正确吗?>

如在先前的示例中,业务逻辑层可能会错误地验证不一致解码的值。建议在使用前进行严格比较或执行类型验证。

一些解析器允许杂散字符,替代引号字符和文档中的语法错误,而其他解析器则严格执行RFC定义的语法。让我们看一下与重复键无关的许可解析实例。

对于许多JSON解析器来说,允许尾随垃圾是一个众所周知的问题,多年来一直被滥用以进行跨站点请求伪造(CSRF)攻击。为了绕过同源策略(SOP)的``简单请求''限制,可以在JSON文档中添加尾随等号来建议x形式的通用编码文档,这在跨域请求中是允许的。例如:

忽略Content-Type并以JSON处理所有请求的服务将受到这些CSRF攻击。

两个解析库在格式错误的JSON上崩溃。这两种情况都已报告给

......