[De1CTF 2019]SSRF Me
Foreword
I thought it was injected into the flask template, but after reading the writeup other master found a code audit process, it is reassuring audit the code.
Tip flag in the /flag.txt
After finishing the 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 we look at this route:
@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())
The first is to create a Task
class, action、sign
the value is determined by cookie obtained, and param
the value is passed directly through the GET method param
parameters to be worth, ip
is your ip地址
, then param参数
it will go through waf
, if passed waf
, this class is executed Exec .
Along the way, we traced back to waf
this method:
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
The waf waf is relatively simple, only requires param参数
not to gopher
and file
at the beginning will be able to over-waf, which is filtered by these two agreements, the agreement that we can not read the file.
The final Task
class of Exec
methods is the key to concluding the natural, we follow this:
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
If self.checkSign()
true, then we can be passed param参数
into the scan
method, the first follow-up scan
methods:
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
Here is the key, through our structure param参数
was found to achieve the results were read arbitrary files, so we need to do now is how to make self.checkSign()
true, follow:
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
As long as we pass in the cookie sign
== getSign ( cookie passed in Action , GET passed param ) will be able to returnTrue
Here we do not know secert_key
the value, and thus can not obtain getSign
the value returned, but found here:
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
Here can be getSign ( 'scan', param GET passed) value, which is the only thing we can take advantage of the place, here we GET the param参数
value is clear, is flag.txt , we can get the value of a sign of geneSign MD5 (SECRET_KEY + param
+ 'Scan'), but in the end we /De1ta?param=
value must be flag.txt, and must be met:
if "scan" in self.action
if "read" in self.action:
We can, in /geneSign
the param参数
value of flag.txtread, so we get a sign that md5 (secret_key + flag.txtreadscan), and the access /De1ta?param
transfer value flag.txt
, and by the action of incoming cookie value readscan
, so
getSign(self.action, self.param) == getSign(flag.txtreadscan)
== md5(secret_key+flag.txtreadscan)
And this sign we know, it can be successfully readflag.txt
Obtained sign=38713ea9d6fcf9affcbfe082fcb2fcea
;
transfer action
and obtained sign
, resulting In Flag!