[De1CTF 2019]SSRF Me
今天做的这道题比较偏审计,当时拿道题目的确发慌,也是看着大佬的wp一步步做出来的,这题就是需要你扎实的基本功,要有耐心,一步步跟着回溯,就可以做出来
题目
拿到题目代码很乱,可放在pycharm里Ctrl+Alt+L将代码格式化一下
1 #! /usr/bin/env python
2 #encoding=utf-8
3 from flask import Flask
4 from flask import request
5 import socket
6 import hashlib
7 import urllib
8 import sys
9 import os
10 import json
11 reload(sys)
12 sys.setdefaultencoding('latin1')
13
14 app = Flask(__name__)
15
16 secert_key = os.urandom(16)
17
18
19 class Task:
20 def __init__(self, action, param, sign, ip):
21 self.action = action
22 self.param = param
23 self.sign = sign
24 self.sandbox = md5(ip)
25 if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
26 os.mkdir(self.sandbox)
27
28 def Exec(self):
29 result = {}
30 result['code'] = 500
31 if (self.checkSign()):
32 if "scan" in self.action:
33 tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
34 resp = scan(self.param)
35 if (resp == "Connection Timeout"):
36 result['data'] = resp
37 else:
38 print resp
39 tmpfile.write(resp)
40 tmpfile.close()
41 result['code'] = 200
42 if "read" in self.action:
43 f = open("./%s/result.txt" % self.sandbox, 'r')
44 result['code'] = 200
45 result['data'] = f.read()
46 if result['code'] == 500:
47 result['data'] = "Action Error"
48 else:
49 result['code'] = 500
50 result['msg'] = "Sign Error"
51 return result
52
53 def checkSign(self):
54 if (getSign(self.action, self.param) == self.sign):
55 return True
56 else:
57 return False
58
59
60 #generate Sign For Action Scan.
61 @app.route("/geneSign", methods=['GET', 'POST'])
62 def geneSign():
63 param = urllib.unquote(request.args.get("param", ""))
64 action = "scan"
65 return getSign(action, param)
66
67
68 @app.route('/De1ta',methods=['GET','POST'])
69 def challenge():
70 action = urllib.unquote(request.cookies.get("action"))
71 param = urllib.unquote(request.args.get("param", ""))
72 sign = urllib.unquote(request.cookies.get("sign"))
73 ip = request.remote_addr
74 if(waf(param)):
75 return "No Hacker!!!!"
76 task = Task(action, param, sign, ip)
77 return json.dumps(task.Exec())
78 @app.route('/')
79 def index():
80 return open("code.txt","r").read()
81
82
83 def scan(param):
84 socket.setdefaulttimeout(1)
85 try:
86 return urllib.urlopen(param).read()[:50]
87 except:
88 return "Connection Timeout"
89
90
91
92 def getSign(action, param):
93 return hashlib.md5(secert_key + param + action).hexdigest()
94
95
96 def md5(content):
97 return hashlib.md5(content).hexdigest()
98
99
100 def waf(param):
101 check=param.strip().lower()
102 if check.startswith("gopher") or check.startswith("file"):
103 return True
104 else:
105 return False
106
107
108 if __name__ == '__main__':
109 app.debug = False
110 app.run(host='0.0.0.0')
分析
先从路由入手,本题一共有3个路由
我们先看/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"))
可以看到在/De1ta页面我们get方法传入param参数值,在cookie里面传递action和sign的值
然后将传递的param通过waf这个函数。
if(waf(param)):
return "No Hacker!!!!"
于是我们先去看waf函数
waf函数找到以gopher或者file开头的,所以在这里过滤了这两个协议,使我们不能通过协议读取文件
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
接着在challenge里面,用我们传进去的参数构造一个Task类对象,并且执行它的Exec方法
我们接着去看Exec方法
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
这是Exec方法
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
先通过checkSign方法检测登录。
到checkSign方法里面去看看
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
当我们传入的参数action和param经过getSign这个函数之后与sign相等,就返回true
返回true之后则进入if语句里面
我们来追踪一下getSign这个函数,它主要是三个东西拼接然后进行md5
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
我发现/geneSign这个路由可以生成我们需要的md5(其实它也是调用了getSign这个函数)
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
但是它的action只有"scan",我们回到之前 if (self.checkSign()):中,当它为真会执行它下面的两条if语句
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
代码也很好懂,就是action中必须要有"scan"和"read"两个才能读取flag
试着访问了一下 /geneSign?param=flag.txt ,给出了一个 md5 4699ef157bee078779c3b263dd895341 ,但是只有 scan 的功能,想加入 read 功能就要另想办法了
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
看了一下逻辑,在 getSign 处很有意思,这个字符串拼接的就很有意思了
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
不妨假设 secert_key 是 xxx ,那么在开始访问 /geneSign?param=flag.txt 的时候,返回的 md5 就是 md5('xxx' + 'flag.txt' + 'scan') ,在 python 里面上述表达式就相当于 md5(xxxflag.txtscan) ,这就很有意思了。
直接构造访问 /geneSign?param=flag.txtread ,拿到的 md5 就是 md5('xxx' + 'flag.txtread' + 'scan') ,等价于 md5('xxxflag.txtreadscan') ,这就达到了目标。
直接访问 /De1ta?param=flag.txt 构造 Cookie: sign=7a2a235fcc9218dfe21e4eb400a11b5e;action=readscan 即可