PHP I/O functions support a handful of
When playing Insomni'hack teaser 2018 I discovered yet another trick, which surprised me to the extent that I couldn't believe my eyes that it actually worked. But to explain the trick, I'm actually going to have the explain the task.
Cool Storage Service
The task itself had two stages, first of which was an HTML/CSS-injection and exfiltration of an anti-XSRF token on a Content Security Policy protected website. At core this wasn't that different from the 34C3 CTF 2017 urlstorage task (see e.g. this write-up by l4w) - it's a pretty amazing attack, but out of scope for today's post. Sufficient to say that at the end of this stage you have modified admin's e-mail address and reset his password to the one of your liking. Therefore you begin the next stage with an admin account.While the first stage is a client-side web challenge, the second one moved to server-side. It was known from the task description that the flag is in the /flag file on the file system, but you had to find a way to leak it.
At your disposal were the following features/bugs (starting from the bottom):
- You could upload an image file (it was verified whether it's an actual image in some way), which would then be stored with a random file name (but same extension) in /uploads/random directory name/file name.ext. PHP-related extensions were blacklisted, and you couldn't access these files via HTTP, but...
- (Admin-only option) You could provide a URL with the image and it would be downloaded, verified and stored by the service. Both the directory and name were random (as in previous case) and the extension was hardcoded to .png. While some URL schemas were blacklisted, as was 127.0.0.1 as the host, it quickly turned out that the php:// family still works.
I failed at the second part. I tried .php, .php{3-7} and .phtml, and nothing. I tried .php.xyz, but only the last extension prevailed. So I started looking for other ideas.
In case you're wondering - yes, I didn't think about testing .pht, even though I'm sure I saw it before. And that was the correct, planned solution.
After reading some PHP source-code around the php://filter functionality, I've stumbled on the iconv conversion filter (under the name of convert.iconv.*), which wasn't really mentioned in the PHP documentation (well, in all fairness, it's mentioned in a heavily downvoted comment in one place, some bugs, and the example output of phpinfo() function in yet another comment). This filter basically allows you to use iconv to convert all processed data from charset A to charset B, where both character sets can be chosen from a surprisingly long list of supported encodings - there are 1173 (sic) entries on that list, some being aliases for other encodings though:
$ iconv -l
The following list contains all the coded character sets known. This does
not necessarily mean that all combinations of these names can be used for
the FROM and TO command line parameters. One coded character set can be
listed with several different names (aliases).
437, 500, 500V1, 850, 851, 852, 855, 856, 857, 860, 861, 862, 863, 864, 865,
866, 866NAV, 869, 874, 904, 1026, 1046, 1047, 8859_1, 8859_2, 8859_3, 8859_4,
8859_5, 8859_6, 8859_7, 8859_8, 8859_9, 10646-1:1993, 10646-1:1993/UCS4,
...
WINDOWS-31J, WINDOWS-874, WINDOWS-936, WINDOWS-1250, WINDOWS-1251,
WINDOWS-1252, WINDOWS-1253, WINDOWS-1254, WINDOWS-1255, WINDOWS-1256,
WINDOWS-1257, WINDOWS-1258, WINSAMI2, WS2, YU
To give you an example, the following code changes a UTF-8 encoded string into hackers' favorite UTF-7:
<?php
$url = "php://filter/convert.iconv.UTF-8%2fUTF-7/resource=data:,some<>text";
echo file_get_contents($url);
// Output:
// some+ADwAPg-text
At that point a seemingly crazy idea popped into my head: perhaps there is such a charset pair, that converting the /flag content between these encodings would actually result in a valid image file?
Unlikely.
But the basic PHP's image verification routines are, well, pretty basic (see these three posts: 1, 2, 3). On top of that it was almost 3am, I was almost out of ideas and it would take at most 15 minutes to code it. So whatever, I just went with it.
The script that I've written would select two charsets at random, form a php://filter/convert.iconv.CHARSET1%2fCHARSET2/resource=/flag URL and submit it to the service. If the service would answer with "Not an image", it would start over. And in the unlikely event of the service replying with "View here...", signaling successful processing, it would exit printing out both the URL of the result file and the charset pair.
I've run the script in the background and went to discuss other potential ideas with my teammates. We were thinking if we could use mcrypt/mdecrypt filter (I don't think you can invoke it through this interface though) with a selected IV, which would perhaps allow us to trick the image verification into accepting the flag as an image (in a somewhat similar manner to what I described in this post - look for "Bug 3 (unexploitable)" located at the very end).
To my great surprise, the script exited 5 minutes later outputting the following two words:
IBM1154 UTF-32BE
Wait. What?
I've quickly downloaded the "image" file and wrote a reverse-conversion script:
<?php
$d = file_get_contents(
"php://filter/convert.iconv.UTF-32BE%2fIBM1154/resource=".
"data:image/png;base64,AAAEWAAAACsAAARbAAAAIwAABFsAAAA/AA".
"AAKAAAAC8AAAA+AAAAYAAABCoAAAQFAAAEAwAABAYAAAAlAAAALwAABD".
"AAAACtAAAEUwAAAC8AAAA+AAAEDgAABAMAAAQFAAAEBwAAAD8AAAA/AA".
"AEBAAABAYAAAA/AAAEDAAAAGAAAAA/AAAEDwAAACcAAACO");
echo $d;
And then executed it, both expecting it to fail for some reason and hoping it wouldn't:
$ php wtf.php
INS{SoManyWebflawsCantbegoodforyou}
It actually worked! And we even got "first blood" on the task :)
The actual URL to get the flag, in case you're wondering:
php://filter/convert.iconv.IBM1154%2fUTF-32BE/resource=/flag
I've chatted with clZ (the task author) about this and he was as surprised as I was with the solution - looks like it wasn't the intended one.
And that's it. A funny story of a crazy 3am idea that actually worked, and yet another PHP quirk that might be useful on another CTF some day.
P.S. It seems that IBM1154 is actually "EBCDIC Cyrillic, Multilingual with euro" (charset).
P.S.2. When the "flag-image" is passed to PHP's getimagesize() we get the following output:
array(5) {
[0]=>
int(4)
[1]=>
int(88)
[2]=>
int(15)
[3]=>
string(21) "width="4" height="88""
["mime"]=>
string(18) "image/vnd.wap.wbmp"
}
So, image/vnd.wap.wbmp, is it? Well then. Whatever works ;)
P.S.3. Turns out p4 team had an equally crazy solution.
Comments:
php://filter/read=string.toupper|convert.base64-encode|convert.base64-encode|string.tolower|string.rot13|convert.base64-encode|string.toupper|convert.base64-decode/resource=/flag
This, being passed INS{hehe}, produced data starting with the null byte. Then we were able to download the real data from server, and brute force, char by char, the actual flag. I did it semi-manually so I'm not entirely sure, but I think the encoding was not unambiguous - but since flag thankfully was meaningful words, we found it anyway. Unfortunately, 20 minutes too late to grab third place :/
Add a comment: