BUUCTF[De1CTF 2019]SSRF Me 1
考点:
1:python代码审计
2:SSRF理解
已知:flag在./flag.txt中
进入靶场:
为一串Python代码,整理得到
#! /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 import importlib,sys importlib.reload(sys) sys.setdefaultencoding('latin1') #编码转换 app = Flask(__name__) #Flask框架 secert_key = os.urandom(16) #返回一个有n个byte那么长的一个string,然后很适合用于加密。 class Task: def __init__(self, action, param, sign, ip):#python的构造方法 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.path.exists判断括号里的文件是否存在的意思,括号内的可以是文件路径。 os.mkdir(self.sandbox) #用于以数字权限模式创建目录。 def Exec(self):# 定义的命令执行函数。 result = {} result['code'] = 500 if (self.checkSign()): if "scan" in self.action:#action的值包括scan 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:#action的值包括read 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):#判断 #hashlib.md5(secert_key + param + action).hexdigest() 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", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理, 当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form action = "scan" return getSign(action, param) #hashlib.md5(secert_key + param + action).hexdigest() #即: hashlib.md5(secert_key + param + 'scan').hexdigest() @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()) #将一个Python数据结构转换为JSON @app.route('/') # 根目录路由,就是显示源代码得地方 def index(): return open("code.txt","r").read() def scan(param):# 这是用来扫目录的函数 socket.setdefaulttimeout(1)#代表经过t秒后,如果还未下载成功,自动跳入下一次操作,此次下载失败。 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):#wef,需要绕过 check=param.strip().lower() #用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。 if check.startswith("gopher") or check.startswith("file"):#.startswith用于检查字符串是否是以指定子字符串开头, return True else: return False if __name__ == '__main__': app.debug = False app.run(host='0.0.0.0')
发现代码中中有两处地方可以传参(/geneSign 和 /De1ta):
@app.route("/geneSign", methods=['GET', 'POST'])#此路由用于测试 def geneSign(): param = urllib.unquote(request.args.get("param", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理, 当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form action = "scan" return getSign(action, param) #hashlib.md5(secert_key + param + action).hexdigest() #即: hashlib.md5(secert_key + param + 'scan').hexdigest() @app.route('/De1ta',methods=['GET','POST'])#此路由用于注入 def challenge(): action = urllib.unquote(request.cookies.get("action"))#用cookie传入action param = urllib.unquote(request.args.get("param", ""))#用get传入param sign = urllib.unquote(request.cookies.get("sign"))#用cookie传入sign ip = request.remote_addr if(waf(param)): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec()) #将一个Python数据结构转换为JSON
先看一下调用函数Task的函数(/De1ta的函数)
@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()) #将一个Python数据结构转换为JSON
可以传入可控的action,param,sign三个函数
先经过wef判断
param中不能有'gopher'和'file',然后最后return json.dumps(task.Exec())
而task.Exec()最后return result,所以我们判断,最后应该要将return的值等于flag,即return flag
看一下Exec()的构成
def Exec(self):# 定义的命令执行函数。 result = {} result['code'] = 500 if (self.checkSign()):
溯源checkSign()
def checkSign(self): if (getSign(self.action, self.param) == self.sign):#判断 #hashlib.md5(secert_key + param + action).hexdigest() return True else: return False
hashlib.md5(secert_key + param + action).hexdigest() == self.sign
即让(secert_key + param + action)的md5值等于sign
但secert_key的值不知道,所以要用到第一个路由(/geneSign ):
@app.route("/geneSign", methods=['GET', 'POST'])#此路由用于测试 def geneSign(): param = urllib.unquote(request.args.get("param", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理, 当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form action = "scan" return getSign(action, param) #hashlib.md5(secert_key + param + action).hexdigest() #即: hashlib.md5(secert_key + param + 'scan').hexdigest()
action的值已经给定,我们只能控制param的值。然后得到secert_key + param + action的md5值。
所以我们只要让/De1ta中的secert_key + param + action的值等于/geneSign 中的secert_key + param + 'scan'就可以使secert_key + param + action == sign
if "scan" in self.action:#action的值包括scan 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:#action的值包括read 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要求传入的action中含有scan,然后调用scan函数从文件中查找param这个文件,将文件名给resp,然后通过tmpfile函数写入result.txt中,且令它连通。即(code=200);第二个if要求传入的action中含有read,然后从result.txt中查找连通的文件作为f,并让result['data']=f.read得到它的内容。最后return result输出内容。
所以此处的action必须为含read和scan。
但/geneSign路由中规定的action必须为scan。但可以控制param
@app.route("/geneSign", methods=['GET', 'POST'])#此路由用于测试 def geneSign(): param = urllib.unquote(request.args.get("param", "")) #urllib.unquote字符串被当作url提交时会被自动进行url编码处理, 当需要获取前端页面表单传过来的id值的时候,我们就需要用request.args.get,而不能用request.form action = "scan" return getSign(action, param)
但/De1ta路由中两个值都可以控制。
且两者最后的secert_key + param + action相等。我们已知flag在flag.txt中,而且最终要通过/De1ta中的param来得到,所以/De1ta中的param=flag.txt
所以
/geneSign路由中,我们令param=flag.txtread,此时action=scan
/De1ta路由中,我们令param=flag.txt,action=readscan
这样,最后secert_key + param + action的值是相同的。/De1ta中的param=flag.txt
1.用/geneSign路由get传入param=flag.txtread得到一个值,让它等于sign
即sign=e8157ddb99ef0fb8d5513fd4a2e7b3a6
2.用/De1ta路由get传入param=flag.txt,cookie传入action=readscan,sign=e8157ddb99ef0fb8d5513fd4a2e7b3a6
得到flag