2019De1CTF—SSRF Me
题目描述
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 109 110
|
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)
|
代码审计能力还有待提高! 看了很久才把逻辑搞清楚(主要还是对框架,一些函数不太熟悉
三个路由:
- / 显示源码
- /geneSign 生成md5,sign = md5(secert_key + param + action ),其中param为url传参,action固定为scan
- /De1ta 挑战,获取cooike中的action参数和sign参数,获取url中的param参数,param参数不能用gopher和file协议。在执行函数Exec中,计算acion和param的Sign,和传入的sign进行比较,相等则继续执行两个判断:action中含有“scan”则会调用urllib.urlopen(param).read()[:50] ,即从url指定地址中创建一个表示远程url的类文件对象(也可以读取本地文件),即可以读取文件内容存入result.txt中;action中含有read可以将result的内容读出来输出前端。
解法一:md5长度扩展攻击
其中Input Signature是md5(secert_key + ‘flag.txt’+ ‘scan’)的值,secert_key看源码可知是一个长度为16的字符串。
Input Data 是message,即’scan’
Input Key Length 是 secert_key+’flag.txt’长度24(相当于固定的一个key的长度),message是scan,为了扩展message
Input Data to Add: read(message要扩展的内容)
最终的目标则是获取 md5(secert_key + 'flag.txt' + 'xxx字符串')
的值(其中xxx字符串中包含了scan和read)
最终我们把\x换成%(url编码)构造cookies提交即可得到flag
解法二:字符串拼接
由题意我们只需要得到md5(secert_key+’flag.txt’+’xxx’)的值即可,其中xxx包含了scan 和read。
我们可以直接在geneSign中传参param=flag.txtread。
那么此时我们可以得到md5(secert_key+’flag.txt’+’readscan’)的值,正好符合了要求。
于是在De1ta处传参param=flag.txt, action = readscan ,sign=geneSign得到的值即可
解法三:local_file
关于 local_file
:
参考:https://bugs.python.org/issue35907——CVE-2019-9948——Python 2.x版本至2.7.16版本中的urllib存在安全漏洞,该漏洞源于程序支持local_file: scheme。远程攻击者可利用该漏洞绕过保护机制
这里是使用的 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
的错误