PlaidCTF 2017 - SHA-4
Category: writeupsTags: plaidctf-2017 web
Web - 300 Points
I heard SHA-1 is broken, so I think it’s probably time we move to SHA-4.
Writeup
This challenge provided two forms, one which allowed to post comments and a textarea which parses an hex encoded, ans1 enveloped input.
Solution tl;dr
local file read, hash collision, template injection, race condition
Directory Traversal
It is trivial to find a directory traversal by using the file://
uri scheme instead of the intended http://
or https://
ones. Once we obtain file-reading access, we can find the directory of the python webapp /var/www/sha4
by getting the system version from file:///etc/issue
and the cmdline from file:///proc/self/cmdline
.
We can guess the existence of these 2 files: server.py sha4.py
By checking the code we can see a possible template injection here:
return render_template_string(commentt, comment=out_text.replace("\n","<br/>"))
However we can’t directly send an input containing characters such as {}
or /
because the function is_unsafe()
checks if the comments contain non-whitelisted chars in string.ascii_letters + string.digits + " ,.:()?!-_'+=[]\t\n<>"
before rendering it.
The code however seems a little strange for what it is intended to do. Let’s check the whole comments()
function:
So the code does the following:
- hex decode the comment POST parameter
- calculate the ‘sha4’ hash of the input
- set the path
/var/tmp/comments/<hash>.file
as output - ASN.1 ber decode the input
- write the decoded input to the file
- check if it contains unsafe characters
- if the previous check is false render the template
Template Injection
Since the webapp uses Flask, we can grab the code needed from Exploring SSTI in flask which hashes the following requirements to achieve Command Execution:
- upload a python reverse shell payload
- inject
{{ config.from_pyfile('/var/tmp/comments/<hash>.file') }}
in therender_template()
function
The first part is simple, we just need to upload the file using the upload form and calculate its path with the sha4
function.
The second part is the hard one.
If we take a closer look at the comments()
function we can notice there’s something a little unusual: why would the programmer write the input to disk, check if it’s safe and read it back? It is already in the out_text
variable!
This seems coded to purposely create a race condition situation and make the challenge solvable. So basically, to exploit the template injection we need to:
- find a
sha4
hash collision between two ASN.1 encoded strings: one containing{{ config.from_pyfile('/var/tmp/comments/<hash>.file') }}
and a safe one (which means without{}
and/
) - send both requests as fast as we can to have enough luck to exploit the race condition by overwriting the file
/var/tmp/comments/<collided hash>.txt
with the malicious payload while the script has passed theis_unsafe()
check on the safe payload
Collision!
We find out that finding a collision is pretty easy. For every char there is a colliding one for each position in the DES key (see sha4.py file).
We then calculate each colliding char for each forbidden char ({}/
) with this small snippet
We can use this script that:
- takes our “bad” payload, and replace each “bad” character with its relative collision
- sets dummy padding at the start and at the end of the payload, since the hash function was appending a padding block that needs to collide as well
- encodes both “good” and “bad” payloads as ASN.1
- hashes them!
Here are the 2 payloads, ASN.1 encoded
0459617b7b636f6e6669672e66726f6d5f707966696c652820222e2e2f2e2e2f746d702f636f6d6d656e74732f64643331623464633435346336656337653031343736653032663865656163342e66696c652229207d7d61616161
0459616b73636f6e6669672e66726f6d5f707966696c652820222e2e2e2e2e3f746d702e636f6d6d656e74730f64643331623464633435346336656337653031343736653032663865656163342e66696c652229203d5d61616161
Finally, the Flag
Once we have the colliding payloads we just have to send both requests as fast as we can, possibly using multiple threads; so from a server with a fast connection we can simply open multiple tmux sessions, running one of the following command in each
After a while the reverse shell should call back to our netcat listener: