flask框架的SSRF
来自
[De1CTF 2019]SSRF Me
打开就是flask源码。
cv到编辑器审计一下:
#! /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')
捏麻麻的还挺长。
看名字SSRF应该就是服务器端请求伪造,但是切入口有点难找,很考验代码审计能力。
遇到flask框架首先找路由@app.route(),这里有三个:
@app.route("/geneSign", methods=['GET', 'POST']) @app.route('/De1ta',methods=['GET','POST']) @app.route('/')
/Delta有点复杂,但是应该最重要,先放一边,从这个/geneSign分析:
意思就是这个方法可以get传一个param的参,action的值是字符串scan,然后给到了getSign函数。
再看getSign函数:
把一个secret_key与传进来的参数param和值为scan的action合起来并进行MD5加密然后返回。
接下来看看这个/De1ta:
这里吧action赋值为cookie里的action参数,param是get传的param参数,sign也是cookie里的sign参数,ip是本地地址,这里有个waf防火墙,看看:
意思就是开头不能用gopher和file读了,算是ban掉了SSRF的常规方法。
接着分析/De1ta,经过防火墙后是丢进了task对象里,然后用json格式dump出来它的Exec函数返回值。
看看Exec:
开始直接调用了checkSign函数,这里就用到了前面getSign函数的MD5返回值,必须与对象里丢进来的那个cookie的sign相等。
接着是两个判断条件
绕进去了后scan,read都必须在这个对象丢进来的action里,我们要用第一个判断里的resp=scan(self.param),要用第二个判断里的f.read(),那么这个action呼之欲出了,要么是scanread,要么是readscan。
接下来就会执行scan函数,赋值给resp。
提示说了flag在./flag.txt,那么一定是这个param在De1ta路由下传flag.txt。
现在首要目的就是绕开这个MD5加密的玩意,但是我们不知道这个密钥。
然而这个getSign就有用武之地了,直接拿来弄出我们想要的MD5,然后我们就在cookie伪造这个MD5传sign上去,就绕过了这个checkSign的相等判断。
param值可控,我们就直接传flag.txtread进行构造,这样在getSign函数里后面加上那个scan刚好凑成readscan,后面再传action的时候就匹配上了!
所以我们在cookie里应该传action=readscan,借用这个flask带密钥的MD5梭一下:
这个就是我们sign的cookie值。
bp一把梭了: