之前写过一篇文章:《使用 PhantomJS 来实现 CTF 中的 XSS 题目》,后来被群里的某大佬翻了出来并被吐槽了一番:
好吧,现在是 8102 年了,该升级一下技术了。于是今天下班后没去打太鼓,转而去折腾 puppeteer。
所需技术
- XSS 基础:相信点进来的同学都了解。
- Promise:相信搞过前端开发的同学都知道。
- async / await:相信大家都会把 Promise 转成这东西。
- Puppeteer:Chrome Headless 的 Node.JS 库,相信大家都听说过。
PhantomJS 版本的不足
先提一下上一个版本的几个问题吧,主要是 Checker 的问题,其它问题暂且忽略:
- 全程用的 ES5,没办法,我当时以为 Node.JS 没法直接运行 ES6……
- 每次请求都很慢,因为我当时比较蠢,每次
check
都会重新开一个 PhantomJS 进程…… - 直接拼接了字符串,因为 PhantomJS 的
evaluateJavaScript
方法只能接受字符串。
其中的 1 非常好解决,3 的话 Puppeteer 的 API 同时支持传入字符串和函数,因此只有 2 是需要重点关注的地方。
Puppeteer 版本的 Checker 思路
首先,Puppeteer 它本质上是个 Chromium 浏览器,因此可以先用它打开一个空白页,然后通过 page.evaluate
API 执行我们自己的代码。自己代码的思路与 PhantomJS 版本相同,也是劫持 alert
、onerror
之类的函数。
// 自己的代码,运行在 Puppeteer 页面内部,用于检测用户输入
function script(problem, input) {
let outputObj = '';
const output = obj => outputObj = obj;
window.onerror = a => output({ error: a.toString() });
window.alert = a => {
if (a === 1) output({ success: 1 });
else if (a == 1) output({ error: 'You should alert *NUMBER* 1.' });
else output({ error: 'Server check failed, you need to alert 1.' });
};
// now we have a `check` function
eval(problem);
try {
check(input);
} catch (e) {
output({ error: e.toString().split('\\n')[0] });
} finally {
return outputObj;
}
}
在程序运行过程中,只需要启动一次 Puppeteer,然后始终用这个进程即可。解决的方法就是将存放 Puppeteer 实例的变量放到函数的外面,一开始为 null
,然后每次 check
的时候,如果发现是 null
,就启动一个进程并赋值给这个变量。
let browser = null;
async function check(input, id, res) {
if (!browser) {
browser = await puppeteer.launch();
}
const page = await browser.newPage();
// several lines hidden
}
由于在我们自己的代码中已经劫持了 onerror
,因此如果用户的输入导致了 Puppeteer 的页面内部运行时错误,Puppeteer 会把错误以普通输出的形式返回给 Node.JS 代码。但如果 Puppeteer 进程自身出现了错误就比较尴尬了,因此需要在外面套一层 try-catch
。
如果用户恶意提交耗时操作(例如死循环),需要及时停止。Puppeteer 提供了一个 API 叫 page.waitForFunction
,但看文档好像不是为了解决这个问题的。于是我打算用 Promise.race
来控制超时,如果超时就将浏览器关掉,并将实例变量赋值为 null
。
const racedPromise = Promise.race([
new Promise(resolve => setTimeout(() => resolve(false), 5000)),
page.evaluate(script, problem, input)
]);
try {
const output = await racedPromise;
if (!output) {
await browser.close();
browser = null;
res.write('Timeout');
} else {
const result = output;
if (result.success) {
res.write('Check passed, flag: ' + global.flag[index]);
} else {
res.write(result.error);
}
}
} catch (e) {
console.log(e); // eslint-disable-line no-console
res.write('Puppeteer error');
}
依旧有缺陷
当然,这个版本还是有一些缺陷,例如当同时有很多人恶意提交的时候,就会出现 Puppeteer 进程不够用的情况,解决的方法有很多,可以开一大堆 browser
进程搞个进程池,也可以先阻塞请求,至于用哪个方法,看开发者的心情吧。
全部代码在 这里,have fun!
如果发现了 BUG,欢迎随时来 diss 我……