Like every year, the Swiss security event Insomni’hack releases a “CTF teaser” two months prior the real CTF. This post is a write-up for three of the challenges: Vulnshop, Smart-Y, and Hax4Bitcoins. Unfortunately I learned about this CTF a bit late, so I didn’t get much time to play on it.
Vulnshop
We’re preparing a website for selling some important vulnerabilities in the future. You can browse some static pages on it, waiting for the official release.
We are presented with a webpage which looks pretty ugly.
The links on the left allow us to see the source of this webpage, and the PHP configuration used by the webserver. The code is the following (redacted for clarity).
If we browse to ?page=contact, then ?page=captcha, we see that a random number is generated, stored in a session variable named challenge, and printed.
When we browse to ?page=captcha-verify, the following code is executed:
Line 13 of this code is particularly interesting. Since we control the two variables $_REQUEST['method'] and $_REQUEST['answer'], we are able to call an arbitrary PHP function, which will take as a first parameter a file whose name is the same as the challenge session variable (in our case, that would be ./778763). By looking at the PHPInfo file we are provided with, we also notice a list of functions which are blacklisted.
Especially, the functions shell_exec, exec, passthru, and system are disabled.
Obtaining a write primitive
In the code snippet above, the most interesting part is line 8:
if(eval("return ".$_SESSION['challenge']." ;") === $response) return true;
If we manage to change the value of the challenge session variable, we’re able to get code execution. If we call the url ?page=captcha-verify&method=file_put_contents&answer=hello+world, the code:
$_REQUEST['method']("./".$_SESSION['challenge'], $_REQUEST['answer']);
will evaluate to:
file_put_contents("./".$_SESSION['challenge'], "hello world");
Similarly, by calling ?page=captcha-verify&method=copy&answer=/tmp/hello, we’ll get:
copy("./".$_SESSION['challenge'], "/tmp/hello")
These two things give us an arbitrary write primitive, as we’re able to first write any contents to ./[random number], then copy it to any file on the system.
Overwriting PHP session file
As we noticed earlier, we can easily get code execution if we manage to change the value of $_SESSION['challenge']. By default, PHP stores its sessions in a serialized format in the directory /var/lib/php/sessions, in a file named sess_[session ID].
To understand exactly how PHP would store it, we can run an interactive PHP shell on our local machine and use session_encode.
$ php -a Interactive mode enabled php > session_start(); php > $_SESSION['challenge'] = "pwned"; php > echo session_encode(); challenge|s:5:"pwned";
By writing this last string into /var/lib/php/sessions/sess_[session ID], we are therefore able to change the value of the challenge session variable to pwned. Here’s a Python script doing this:
Output:
<div class="content"> pwned </div>
Now that we proved we’re able to overwrite the challenge session variable, we can write PHP code to it, and have it executed by the verifyFromMath function.
Which gives us the flag!
<div class="content"> INS{4rb1tr4ry_func_c4ll_is_n0t_s0_fun} </div>
Smart-Y
Last year, a nerd destroyed the system of Robot City by using some evident flaws. It seems that the system has changed and is not as evident to break now.
If we click on the arrow on the right few times, we are redirected to the following page, where we can use the <<<DEBUG>>> link to see its PHP source.
It looks like the code is using the Smarty template engine. If we browse to /smarty, we see a directory listing of the Smarty files. By opening the change_log.txt file, we’re able to see that the Smarty version being used is the 3.1.31.
After a bit of googling, we find that Smarty before 3.1.32 is vulnerable to CVE-2017-1000480.
Smarty 3 before 3.1.32 is vulnerable to a PHP code injection when calling fetch() or display() functions on custom resources that does not sanitize template name.
Interestingly, no exploit has yet been released for this vulnerability. By searching a little bit more, we can find the commit which fixed the vulnerability.
As one can guess, the vulnerability is caused by the fact that $source->filepath is not validated (second diff), and is being printed in the template generated by Smarty (first diff). We see that it is printed inside a PHP comment:
$output .= "/* Smarty version [...] from \"" . $template->source->filepath . "\" */...";
If we set the filepath to */ echo 'pwned'; /*, the expression above evaluates to:
$output .= "/* Smarty version [...] from \"*/ echo 'pwned'; /*\" */...";
Once generated, the template will therefore contain the code we injected in it, resulting in arbitrary code execution.
As one can see from the Smarty source, the filepath variable is constructed as the concatenation of the type of the resource, and its name. In the code of the challenge’s console.php, we have:
$smarty->display('news:'.(isset($_GET['id']) ? $_GET['id'] : ''));
Here, news is the type of the resource, and the id URL variable (which we control) will be the name. Thus, we can easily control the vulnerable filepath variable.
Which gives us the flag.
INS{why_being_so_smart-y} The news system is in maintenance. Please wait a year. <<<DEBUG>>>
Hax4Bitcoins
I found this challenge interesting, because it wasn’t very technical but still required some thinking.
These crowdfunded hackers are hiding something! http://hax4bitcoins.teaser.insomnihack.ch/
(No need to pay any Bitcoin to solve)
The website contains several bitcoin addresses, all starting with 1Hax4B. In the footer, we can see the mention:
PS: we call dibs on all 1Hax4B vanity addresses ò_ó
If we inspect these addresses on blockchain.info, we see that one of them has an output transaction to the address 1PasteBinXXXXXdWzkucb1XXXXXXdY9fcu. Sure enough, that leads us to a pastebin at https://pastebin.com/dWzkucb1.
—–BEGIN BITCOIN SIGNED MESSAGE—–
Hi dude, I added a login form to our website so that we can remotely share sensitive files 🙂
It’s at /super_secret_Hax4B_admin_login.
—–BEGIN SIGNATURE—–
1Hax4B2j9FC3c73jHhfxrPQmv2zKuiSngv
HMBMSfQJiE68/9x0qeyIiEkf8T3N8Zt26d6vVBECVNZNJWcdIBk06sCcVNKkETaYDFcKwycnz6eDeAPwQyo9w0w=
—–END BITCOIN SIGNED MESSAGE—–
We can see that this message is signed with the bitcoin private key corresponding to the key 1Hax4B2j9FC3c73jHhfxrPQmv2zKuiSngv. If we browse to /super_secret_Hax4B_admin_login, we get a login form for an administrative interface.
The challenge is a hash which is unique for each session. If we look at the source of this page, we find what might be an hint.
We can decode the base64 payload and see what it gives us.
$ base64 -d hint.b64 > hint.bin $ file hint.bin hint.bin: PNG image data, 18 x 20, 8-bit/color RGBA, non-interlaced
We get a tiny image, with something that looks like a hooded man.
No metadata in it, nothing by changing the colors or brightness, no special string in the file, so I’m guessing it’s a false hint. Back to the login page – remember this sentence?
PS: we call dibs on all 1Hax4B vanity addresses ò_ó
And this message on the login page.
To access this content, prove that you are an Hax4B admin
Here, I’m going to guess that in order to access the admin interface, we’ll have to sign the challenge with a private key tied to a bitcoin address starting with 1Hax4B.
After a few seconds of googling, I discover that it is quite straightforward to generate a bitcoin address starting with a specific set of bytes. Vanitygen is a tool which does exactly this.
$ ./vanitygen 1Hax4B Difficulty: 259627881 Pattern: 1Hax4B Address: 1Hax4BVjKTad5QnmeMDHrUpQczAPcJMnxQ Privkey: 5JUz4nmgPiWfA6xFRnyqJnBfK9kRA9yprtVBCmohQuYkrFNiGr4
Here we go. Now, we can sign the challenge presented by the admin page using this private key, using an online tool such as this one.
We get the following.
-----BEGIN BITCOIN SIGNED MESSAGE----- fbdfff4a2667f3015bd800369022bb0ca09f29cda73520476f22a0df042e66ec -----BEGIN SIGNATURE----- 1Hax4BVjKTad5QnmeMDHrUpQczAPcJMnxQ HCnFVFPeyzoyzYGTyjhyZ3T7I2zRVhd91gCI9KmVDo0QM9cAWWjNE0Zq3T0T1H2i9uT9nYx9gL9b60hcMu5SlgA= -----END BITCOIN SIGNED MESSAGE-----
We can know input the signature (starting with HCnF...) in the login form, and we get:
Flag: INS{v4n1ty_k1ll3d_th3_c4t}
Conclusion
I wasn’t able to spend as much time as I wanted to on this CTF teaser, but the few challenges I solved were funny nevertheless.
Thanks for reading!
Liked this post? Show it by pushing the heart button below! You can also follow me on Twitter.
Hi Sir,
I really appreciated your writeup. However, I am curious about how the system know the signature was signed by address started with 1Hax4b
That’s a really good question! I’m guessing one can retrieve the public key from the signature (see here for instance).
thanks for your effort, I found that bitcoin library can recover it directly.
#!/usr/bin/env python3
# coding: utf-8
from bitcoin.core.key import CPubKey
from bitcoin.wallet import CBitcoinSecret, P2PKHBitcoinAddress
from bitcoin.signmessage import BitcoinMessage, VerifyMessage, SignMessage
import base64
”’
$ ./vanitygen 1 ──[Wed Jan 24 10:09]──┘
Difficulty: 1
Pattern: 1
Address: 1Bh2Z93HYZvNt69r9ZSaXUCmxBUSqdPWj3
Privkey: 5K4MLXfbSQ7to3xHmhfJZmEPKswMeDVcC7SfnT7zoNwuWk774mv
”’
def VerifyMessage(message, sig):
message = BitcoinMessage(message)
sig = base64.b64decode(sig)
hash = message.GetHash()
pubkey = CPubKey.recover_compact(hash, sig)
return str(P2PKHBitcoinAddress.from_pubkey(pubkey))
msg=”hello bitcoin msg”
sig=”G5jW0iPr6ByOs16C4UqVraijrFBaDqT/8VIhUfSfmg8wMsE0iBdzJUvjE9FY2GnHcxyu6uzCB+VJkpkYR+7U0x0=”
print(VerifyMessage(msg, sig))