有关python的ssrf
前言
- 环境:buuctf中[De1CTF 2019]SSRF Me
- 知识点:python代码审计,ssrf
做题
审计源码
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): #python类的构造函数
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
# 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", "")) \对参数Param进行url解码
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()) \打印出一个json对象
@ app.route('/') #显示源代码
def index():
return open("code.txt", "r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[: 50] #造成ssrf的漏洞,对param进行读取,返回前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() \strip()移除两边的空格
if check.startswith("gopher") or check.startswith("file"): #字符串开头不能是gopher和file
return True
else :return False
if __name__ == '__main__': app.debug = False app.run(host = '0.0.0.0', port = 80)
根据题目提示这里存在ssrf漏洞
我们找到可以读取文件的函数,urllib.urlopen(param).read
,可以看到没有对param进行过滤,明显存在ssrf漏洞
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[: 50] #造成ssrf的漏洞,对param进行读取,返回前50个字符
except:
return "Connection Timeout"
追踪scan,在类Task里定义了Exec函数,如果满足了checkSign这个函数并且action这个字符串里面包含了scan,就会调用scan函数
先追踪哪里使用了这个类,可以看到在De1ta这个路由内,定义了一个Task的对象task,并且调用了Exec方法,将其结果以json的形式打印出来,看来注入点就是这个路由
调用Exec函数前,waf函数会对param参数进行过滤,过滤了协议file和gopher
接下来我们要做的就是满足checkSign函数,调用了getSign函数
而getSian函数,可以看到我们不知道secert_key是什么,也就无法直接构造sign了
但是我们在geneSign这个路由里,看到会返回getSign这个值,所以我们就可以通过这个路由,来获取sign值
根据代码逻辑,这里为什么param是flag.txtread呢
首先我们要搞清楚为什么要加个read,调用scan函数后,会将我们读取的文件写进result.txt内,如果read也在action这个字符串内,那么就会读取result.txt的内容,但是明明有个print,为什么还要read呢?这里print并不会输出,原因我也不晓得。
其次我们要搞清楚read的位置,根据getSign函数,加密顺序是secert_key+param+action
,注意传进去是action在前
在geneSign
(scan,flag.txtread)=>flag.txtreadscan
那么在De1ta里
(readscan,flag.txt,sign)=>flag.txtreadscan
值得一试的是:通过urllib.urlopen(param).read(),读取内部文件不要file协议就可以读取,
而php的curl读取内部文件时,要通过file协议才可以,不然会报错