最近看到小伙伴写的 XSS 扫描器,最后验证是否存在 XSS 时是调用浏览器打开可疑的 URL 再人工查看是否有弹窗。
感觉这样做不那么优雅,正好之前看到过一篇文章 XSS dynamic detection using PhantomJS,于是准备写个程序用 PhantomJS 来自动化检测网页是否有 alert 弹窗。(查资料的时候发现 PhantomJS 不支持 Flash,不过 SlimerJS 支持,可以参考 Flash Xss Dynamic Detection Using SlimerJS

根据官方文档,先写一个 alert.js

"use strict";
var page = require('webpage').create(),
    system = require('system'),
    t, address;

if (system.args.length === 1) {
    console.log('Usage: alert.js <some URL>');
    phantom.exit(1);
} else {
    t = Date.now();
    address = system.args[1];
    page.open(address, function (status) {
        if (status !== 'success') {
            console.log('FAIL to load the address');
        } else {
            t = Date.now() - t;
            console.log('Loading time ' + t + ' msec');
        }
        phantom.exit();
    });

    page.onAlert = function () {  
        console.log('Alert detected: ' + address);
    }
}

然后写个有 DOM 型 XSS 的测试页面 alert.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>alert test</title>
</head>
<body>
<script>    
<?php  
    echo $_GET['test'];  
?>
</script>
</body>
</html>

再执行命令试试:

F:\>phantomjs.exe alert.js http://demo.local/alert.php?test=alert(1)
Alert detected: http://demo.local/alert.php?test=alert(1)
Loading time 1644 msec

看起来不错,但是这样单进程比较麻烦也比较慢,于是准备使用 Python + Selenium + PhantomJS 搞个多进程检测。
根据之前看到过的一篇 PhantomJS 性能优化 的文章,改改代码:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import Queue
from selenium import webdriver
import threading
import time

class conphantomjs:
    phantomjs_max = 1  # 同时开启phantomjs个数
    jiange = 0.00001   # 开启phantomjs间隔
    timeout = 20       # 设置phantomjs超时时间
    path = "E:\Python27\Scripts\phantomjs.exe"  # phantomjs路径
    service_args = ['--load-images=no', '--disk-cache=yes']  # 参数设置

    def __init__(self):
        self.q_phantomjs = Queue.Queue()  # 存放phantomjs进程队列

    def check_alert(self, url):
        '''
        利用phantomjs检查是否有alert事件
        '''
        d = self.q_phantomjs.get()
        try:
            d.get(url)
            alert = d.switch_to.alert
            alert.accept()      # 坑就在这里
            print url
        except Exception as e:
            print e

    def open_phantomjs(self):
        '''
        多线程开启phantomjs进程
        '''
        def open_threading():
            d = webdriver.PhantomJS(conphantomjs.path, service_args = conphantomjs.service_args)
            d.implicitly_wait(conphantomjs.timeout)        # 设置超时时间
            d.set_page_load_timeout(conphantomjs.timeout)  # 设置超时时间

            self.q_phantomjs.put(d)  # 将phantomjs进程存入队列

        th = []
        for i in range(conphantomjs.phantomjs_max):
            t = threading.Thread(target = open_threading)
            th.append(t)
        for i in th:
            i.start()
        time.sleep(conphantomjs.jiange)  # 设置开启的时间间隔
        for i in th:
            i.join()

    def close_phantomjs(self):
        '''
        多线程关闭phantomjs对象
        '''
        th = []
        def close_threading():
            d = self.q_phantomjs.get()
            d.quit()

        for i in range(self.q_phantomjs.qsize()):
            t = threading.Thread(target = close_threading)
            th.append(t)
        for i in th:
            i.start()
        for i in th:
            i.join()

if __name__ == "__main__":
    '''
    用法:
    1. 实例化类
    2. 运行open_phantomjs 开启phantomjs进程
    3. 运行check_alert函数, 传入url
    4. 运行close_phantomjs 关闭phantomjs进程
    '''
    cur = conphantomjs()
    conphantomjs.phantomjs_max = 4
    cur.open_phantomjs()
    print "phantomjs num is ", cur.q_phantomjs.qsize()

    url_list = ["http://demo.local/alert.php?test=<img%20src=1%20id=123%20onerror=alert(1)>"] * 2
    th = []
    for i in url_list:
        t = threading.Thread(target = cur.check_alert, args = (i, ))
    th.append(t)
    for i in th:
        i.start()
    for i in th:
        i.join()
    cur.close_phantomjs()
    print "phantomjs num is ", cur.q_phantomjs.qsize()

虽然代码没问题但是一直报错:

Message: Invalid Command Method -
{"headers":{"Accept":"application/json","Accept-Encoding":"identity","Connection":"close","Content-Length":"53","Content-Type":"application/json;charset=UTF-8","Host":"127.0.0.1:13578","User-Agent":"Python-urllib/2.7"},"httpVersion":"1.1","method":"POST","post":"{"sessionId":
"503c0e40-0474-11e7-928f-812dd53b8c7b"}","url":"/accept_alert","urlParsed":{"anchor":"","query":"","file":"accept_alert","directory":"/","path":"/accept_alert","relative":"/accept_alert","port":"","host":"","password":"","user":"","userInfo":"","authority":"","protocol":"","source":"/accept_alert","queryKey":{},"chunks":["accept_alert"]},"urlOriginal":"/session/503c0e40-0474-11e7-928f-812dd53b8c7b/accept_alert"}

翻阅 Selenium 文档,用 print dir(alert) 查看第 31 行 alert 的属性:
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'accept', 'authenticate', 'dismiss', 'driver', 'send_keys', 'text']
发现 accept 确实是有的,纠结了很久之后,搜到一个类似的问题,里面有人说到:

**PhantomJS uses GhostDriver to implement the WebDriver Wire Protocol,
which is how it works as a headless browser within Selenium.**

**Unfortunately, GhostDriver does not currently support Alerts. Although
it looks like they would like help to implement the features:**

https://github.com/detro/ghostdriver/issues/20

You could possibly switch to the javascript version of PhantomJS or
use the Firefox driver within Selenium.

意思就是 PhantomJS 用来实现 WebDriver 协议的 GhostDriver 居然不支持 alert !
这就比较尴尬了,因为检查 XSS 的 Payload 都是用的 alert。
只好改一改 Payload 了,想了下决定插入一个 img 来判断:
eval(String.fromCharCode(115,61,110,101,119,32,73,109,97,103,101,40,41,59,115,46,115,101,116,65,116,116,114,105,98,117,116,101,40,39,105,100,39,44,39,120,115,115,116,101,115,116,39,41,59,100,111,99,117,109,101,110,116,46,98,111,100,121,46,97,112,112,101,110,100,67,104,105,108,100,40,115,41)),即向页面插入一个 id 为 xsstest 的 img ,如果含有该 id 的元素存在,这说明存在 XSS。(当然对于本例直接在网页返回包里查 id 也行,但对于更复杂的 DOM 操作或是有过滤的情况用 PhantomJS 会更有效些)

把 check_alert() 函数改一下就好:

def check_xss(self, url):
    '''
    利用phantomjs检查页面中是否存在注入的img
    '''
    d = self.q_phantomjs.get()
    try:
        d.get(url)
        xss = d.find_element_by_id('xsstest')
        print "Find XSS in: " + url
    except Exception as e:
        print e

参考: