R·ex / Zeng


音游狗、安全狗、攻城狮、业余设计师、段子手、苦学日语的少年。

使用 Puppeteer 编写 CTF XSS Checker

之前写过一篇文章:《使用 PhantomJS 来实现 CTF 中的 XSS 题目》,后来被群里的某大佬翻了出来并被吐槽了一番:

0

好吧,现在是 8102 年了,该升级一下技术了。于是今天下班后没去打太鼓,转而去折腾 puppeteer。

所需技术

  • XSS 基础:相信点进来的同学都了解。
  • Promise:相信搞过前端开发的同学都知道。
  • async / await:相信大家都会把 Promise 转成这东西。
  • Puppeteer:Chrome Headless 的 Node.JS 库,相信大家都听说过。

PhantomJS 版本的不足

先提一下上一个版本的几个问题吧,主要是 Checker 的问题,其它问题暂且忽略:

  1. 全程用的 ES5,没办法,我当时以为 Node.JS 没法直接运行 ES6……
  2. 每次请求都很慢,因为我当时比较蠢,每次 check 都会重新开一个 PhantomJS 进程……
  3. 直接拼接了字符串,因为 PhantomJS 的 evaluateJavaScript 方法只能接受字符串。

其中的 1 非常好解决,3 的话 Puppeteer 的 API 同时支持传入字符串和函数,因此只有 2 是需要重点关注的地方。

Puppeteer 版本的 Checker 思路

首先,Puppeteer 它本质上是个 Chromium 浏览器,因此可以先用它打开一个空白页,然后通过 page.evaluate API 执行我们自己的代码。自己代码的思路与 PhantomJS 版本相同,也是劫持 alertonerror 之类的函数。

// 自己的代码,运行在 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 我……

版权声明:除文章开头有特殊声明的情况外,所有文章均可在遵从 CC BY 4.0 协议的情况下转载。

这是我们共同度过的

第 1114 天