导语:Firefox Accounts API服务器对unicode字符的一些错误处理可能允许攻击者向其后端数据存储发出任意请求。这就会引起一个漏洞:HTTP请求路径中的unicode字符损坏。
Firefox Accounts API服务器对unicode字符的一些错误处理可能允许攻击者向其后端数据存储发出任意请求。这就会引起一个漏洞:HTTP请求路径中的unicode字符损坏。
漏洞的发现过程
此漏洞还是在我调试一个不相关的unicode处理问题时发现的,该漏洞最终导致了Node.js`http`模块的错误。当使用含有 '/café🐶, the server receives /café=6路径的`http.get` 请求时,就会出现此错误。
换句话说,我要求Node.js向特定路径发出HTTP请求,但是请求发出后,路径却发生了变化!在深入研究这个细节后,我发现这个问题是这样引起的:当HTTP请求写入进入路径的unicode字符时,Node.js会将unicode字符损坏。
虽然使用`http`模块的用户通常将请求路径指定为字符串,但最终Node.js还是必须将这些请求转化为原始字节输出。 JavaScript具有unicode字符串,因此将它们转换为字节,就意味着要选择并应用适当的unicode编码。对于不包含主体的请求,Node.js会默认使用“latin1”,这是一种单字节编码形式,不能编码复杂的unicode字符,例如表情符号。但是,这些复杂的字符,可以被分解成其内部JavaScript表示的单字节形式。
> v = "/caf\u{E9}\u{01F436}" '/café🐶' > Buffer.from(v, 'latin1').toString('latin1') '/café=6'
在处理用户输入时,数据损坏常常是潜在安全问题的危险信号,比如,在本文的案例中,通过我所用的firefox的代码库发出的HTTP请求,我就知道包含用户输入的路径可能会出现风险。所以我立即联系了安全团队,然后根据对方提供的unicode字符串寻找我可能构建了URL的位置。
通过拆分请求来实现的SSRF攻击漏洞
经过分析,我发现这种漏洞是一种称为拆分请求的攻击,而基于文本的协议(如HTTP)通常容易受到这样的攻击。如果有一个服务器,它接受一些用户输入的内容,并将其包含在通过HTTP公开的内部服务的请求中,那整个过程就如下所示。
GET /private-api?q=<user-input-here> HTTP/1.1 Authorization: server-secret-key
如果服务器未正确验证用户的输入,则攻击者可能会将协议控制字符直接注入到发送请求中。假设通过这种方式,服务器接受了用户的以下输入。
"x HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n"
当发出请求时,服务器可能会直接将其写入路径,如下所示:
GET /private-api?q=x HTTP/1.1 DELETE /private-api Authorization: server-secret-key
此时,接收端会将此此请求解释为两个独立的HTTP请求:一个`GET`请求和一个`DELETE`请求,这样接受端就无法知道调用者的真实意图。
实际上,这种精心设计的用户输入将诱使服务器发出额外的出站请求,这种情况就被称为服务器端请求伪造或 “SSRF”。此时,服务器可能具有攻击者不具有的权限,例如访问内部网络或秘密API密钥,这会增加问题的严重性。
高质量的HTTP库通常包括防止这种危险行为的缓解措施,Node.js也不例外。如果你尝试使用路径中的控制字符发出一个出站HTTP请求,则它们将在被写入之前进行percent escape。
> http.get('http://example.com/\r\n/test').output [ 'GET /%0D%0A/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
不幸的是,上述处理unicode字符的错误则意味着可以规避这些防护措施。比如如下的URL,其中包含一些带有附加符号的unicode字符。
> 'http://example.com/\u{010D}\u{010A}/test' http://example.com/čĊ/test
当Node.js版本8或更低版本对此URL发出`GET`请求时,这些请求是不会进行percent escape的,因为它们不是HTTP控制字符。
> http.get('http://example.com/\u010D\u010A/test').output [ 'GET /čĊ/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
但是当这些字符串被编码为latin1以将其写入路径时,这些字符将分别被拆分为 "\r"和 "\n"。
> Buffer.from('http://example.com/\u{010D}\u{010A}/test', 'latin1').toString() 'http://example.com/\r\n/test'
因此,通过在请求路径中加入精心选择的unicode字符,攻击者可以欺骗Node.js将HTTP协议控制字符写入路径。
不过最新的Node.js 10版本修复了此漏洞,如果请求路径包含非ascii字符,则会弹出一个错误警报。但是对于Node.js版本8及其以下的版本,任何发出HTTP请求的服务器都可能受到SSRF的攻击,其中就包括:
1.接受来自用户输入的unicode数据;
2.将输入的unicode数据包含在传出HTTP请求的请求路径中;
3.长度为零的请求(例如GET或DELETE);
伪造对FxA数据存储的请求
我审核了FxA服务器堆栈,以查找在请求路径中使用长度为零的内容和用户提供的数据发出HTTP请求的位置,于是我发现可以触发上述错误的三个位置。
第一个是我们对WebPush的支持,登录的客户端可以提供一个https URI,在该URI处接收帐户状态更改的通知,服务器将通过发出零长度的“PUT”请求来传递该通知。幸运的是,在这种情况下,服务器发出的请求不具有任何特殊权限或包含任何API令牌。这样,你就可以利用这个漏洞诱使FxA服务器向webpush通知主机发出的意外请求,但该请求不会比攻击者直接发出的请求更强大。
第二个是检查BrowserID证书的真实性,其中FxA服务器会从用户提供的JSON blob中解析主机名,然后通过发出'GET`请求来获取该主机的签名密钥,如:
GET /.well-known/browserid?domain=<hostname>
我可以利用此漏洞来诱使服务器对任意主机名发出任意请求,幸运的是,在Firefox的运行环境中,这些请求都是通过squid缓存代理发送的,该代理配置了严格的验证规则来阻止任何意外的传出请求,这就防止了这个漏洞在此种情况中被利用。
第三个是向后端数据存储发出HTTP请求,而正是在这个位置,我发现了一个真正可利用的漏洞。
你需要了解的是,Firefox的帐户运行服务器分为面向Web的API服务器和与MySQL数据库通信的独立内部数据存储区服务器,如下所示:
+--------+ +--------+ +-----------+ +----------+ | Client | HTTP | API | HTTP | DataStore | SQL | MySQL | | |<------>| Server |<------>| Service |<----->| Database | +--------+ +--------+ +-----------+ +----------+
API服务器通过普通的老式HTTP与数据存储区服务进行通信,结果发现只有一个地方,可以让来自用户输入的unicode数据进入其中一个请求的路径。
由于我的许多数据存储请求都是通过电子邮件地址输入的,且电子邮件地址包含unicode字符。为了避免两个服务之间的unicode编码和解码问题,我的数据存储API中的大多数与电子邮件相关的操作都会将该电子邮件以十六进制编码的utf8字符串接受。例如,API服务器会通过向数据存储发出如下HTTP请求来获取电子邮件“[email protected]”的帐户记录。
GET /email/74657374406578616d706c652e636f6d
通过查阅历史记录,我发现有一个操作把电子邮件地址作为原始字符串进行了接收,通过ID删除“xyz”帐户中的电子邮件是通过以下请求完成的。
DELETE /account/xyz/emails/[email protected]
由于我会仔细验证进入系统的所有用户输入,因此理论上,此电子邮件地址不会包含任何HTTP控制字符,即使它确实存在由`http`模块进行的percent escape,也不会发生这种情况。但事实却是,电子邮件地址包含了unicode字符。
在测试环境中,我能够创建一个帐户,并向其中添加以下奇怪但有效的电子邮件地址。
[email protected]̠ňƆƆɐį1̮1č̊č̊ɆͅƆ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňƆƆɐį1̮1č̊č̊.cc
这里的非ascii字符是经过我精心选择的,因此,当在latin1中进行小写和编码时,它们将为各种HTTP控制字符生成原始字节。
> v = '[email protected]̠ňƆƆɐį1̮1č̊č̊ɆͅƆ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňƆƆɐį1̮1č̊č̊.cc' > Buffer.from(v.toLowerCase(), "latin1").toString() '[email protected] HTTP/1.1\r\n\r\nGET /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/sessions HTTP/1.1\r\n\r\n.cc'
通过将此电子邮件地址添加到帐户并删除它,我可以使API服务器向数据存储区发出如下所示的HTTP请求。
DELETE /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/email/[email protected]̠ňɔɔɐį1̮1č̊č̊ɇͅɔ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňɔɔɐį1̮1č̊č̊.cc
借助上述Node.js中的漏洞,此HTTP请求将被写入以下内容。
> console.log(Buffer.from('DELETE /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/email/[email protected]̠ňɔɔɐį1̮1č̊č̊ɇͅɔ̠įaccountįf9f9eebb05ef4b819b0467cc5ddd3b4aįsessions̠ňɔɔɐį1̮1č̊č̊.cc', 'latin1').toString()) DELETE /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/email/[email protected] HTTP/1.1 GET /account/f9f9eebb05ef4b819b0467cc5ddd3b4a/sessions HTTP/1.1 .cc
这是一个SSRF,导致API服务器产生一个额外的“GET”请求,而这并不是它想要的。
虽然,这个特定的“GET”请求是无害的,但它足以让我相信这个漏洞是可利用的,比如,它可能会被用来诱导API服务器对数据存储API发出各种各样的欺诈性请求,比如创建一个用户无法控制的电子邮件地址,或重置其他用户帐户的密码,或者只是在Firefox帐户对电子邮件地址强加的255-unicode字符长度限制内进行任何操作。
幸运的是,目前还没有任何证据表明这个漏洞被利用。
还需要注意的是,攻击者无法利用此漏洞访问用户的Firefox同步数据。Firefox同步数据使用强大的客户端加密功能,只有知道帐户密码的人才能访问你的同步数据。
总结
一发现此漏洞,我就将Node.js漏洞报告给了Firefox团队,而且在今年4月发布的Node.js 10中,此漏洞已经被修复。
Node.js 10.0.0是自 Node.js Foundation开展以来的第七个主要版本,并将在2018年10月成为下一个LTS分支。
完整更新内容请查阅发行说明: