TokyoWesterns CTF 2019 - phpnote
04 September 2019 by alfink and Ben
The main purpose of the service was to store and display serialized notes. However, the exploit requires more than a typical deserialisation exploit.
This is, because the serialized note is signed using a HMAC and the deserialization is only triggered if the signature is valid. The HMACs secret is not known to the user, but stored in the session instead.
Guess the Flag
During the CTF we looked for php issues and tried to bruteforce because SALT
and PEPPER
in the gen_secret
function looked phishy (in php, SALT
and PEPPER
may default to the strings "SALT"
and "PEPPER"
if the constants are undefined). Also, it was possible to use an empty seed.
However, none of those ideas worked and the CTF passed by. :(
Some OSINT
After the CTF, we found out that the challenge author was icchy. For WCTF 2019, he already created a hard, but really creative challenge. To solve Gyotaku The Flag, it was required to leak the flag by using Windows Defender as a side channel (similar to the XSS-Auditor side channel). Unfortunately, there was an unintended solution that almost everyone exploited. Knowing that, it was only a matter of time for a similar challenge to appear.
Thanks to all the people in the #twctf IRC for hinting on this after the CTF.
Leaking the secret
After creating an account, realname
, secret
and nickname
are stored in the session. The session data itself is stored in a file and looks like this:
Because the server is running Windows, Windows Defender inspects all files, including the ones that store the session data. So, if we store “malware” in the session data, Windows Defender detects it and the login will fail. One such string is var miner=new CoinHive.User();miner.start()
.
Another useful fact about Windows Defender is that it includes a Javascript Engine that executes all the Javascript code it finds. This should protect the users of obfuscated malware. However, this is also the part we will exploit to leak the secret.
As example, the first payload will be blocked by Windows Defender while the second will not:
So, to leak the secret, we have to create a Javascript payload that reads a character of the secret, and only assembles our “malicious” string if it matches our guess. We can then use a successful login as our side channel output.
Reading the secret in Javascript
To read the secret in Javascript via document.body.innerHTML
, it is necessary to put <body>
tags around the secret. During registration realname
and nickname
are added to the session before secret
, so we don’t control anything after the data to leak.
But, we can execute this code again after the login. So, if no nickname is provided in the first request, only realname
and secret
are stored in the session. In all following login-requests, the realname
and secret
will be updated and a nickname
is added to the end of the session file. This means, if we put a <body>
into realname
and </body>
into nickname
, the secret is a substring of document.body.innerHTML
.
So, our nickname
will be </body>
and the realname
our script ({offset}
is the position of the char to leak and {char_to_check}
the char to compare against):
Leaking it the simple way
The corresponding python code is the following:
I had to modify the javascript slightly to get rid of false positives, because Windows Defender seems to be case-insensitive.
After some time, it returns the leaked part of the session:
BONUS: The fast and elegant way
While I made my exploit faster by spending a few cents on a VPS in Tokyo, Ben implemented an exploit that used binary search to leak the secret more efficiently.
His Javascript generates CoinHive
if test
was larger or equal to the character at offset offset
. Otherwise it will assemble Coinundefinedive
, which will not be detected. This approach has the advantage, that does not rely on case-sensitiveness.
Ben’s python script uses the JS template for a binary search:
Request the flag
Finally, we can use the leaked secret to sign a note with isadmin
set:
Setting the cookies and accessing http://phpnote.chal.ctf.westerns.tokyo/?action=getflag reveals the flag: TWCTF{h0pefully_I_haven't_made_a_m1stake_again}