作者:倚笑趁風涼@逢魔安全實驗室
公眾號:逢魔安全實驗室
起因:
掃描器不能滿足需求,phantomjs不支持html5標簽。所以自己查閱資料和api,寫了一個基于chrome headless xss掃描插件。
總體思路來自于:
fridayy的基于phantomjs的xss掃描http://www.bjnorthway.com/93/
以及豬豬俠的web2.0啟發式爬蟲實戰

判斷xss的方法為:
-
監聽頁面的彈窗事件
-
查看dom中的localName是否有存在我們自定義的標簽
-
查看dom中的nodeValue 是否含有我們輸入的payload
將其分為三個等級,分別為level 3 level 2 level 1 分別對應這xss的精確程度(由高到低)
了解chrome_headless
初步了解,可以看一下大佬的blog: https://thief.one/2018/03/06/1/
你可以通過它來做很多事情,但是這里不討論其他功能,只著眼于xss的判斷。
總體來說,headless chrome意思是無頭chrome瀏覽器,相對于傳統的chrome瀏覽器,這是一個可以在后臺用命令行操作瀏覽器的工具,對于爬蟲編寫以及web自動化測試都有很大的作用。相比較同類工具Phantomjs,其更加強大(主要因為其依賴的webkit更新)。
我認為核心的理解在于:
-
就是具有基于Chrome DevTools Protocol 的chrome遠程調試功能的無界面瀏覽器。
-
現在的python和nodejs對chrome headless進行操作的封包都是基于Chrome DevTools Protocol來實現的。
學習了一下:https://github.com/wilson9x1/ChromeHeadlessInterface 的項目后,決定自己使用webscoket和chrome進行通信。
原因有以下幾點:
-
有現成的部分代碼,但是不支持post,也不能監聽dom的更改。所以需要自己讀api去實現我們的功能。
-
比較直觀,可以通過本地遠程調試端口看頁面的變化。
與chrome通信的基本知識:
簡單說一下這套協議吧,這套協議通過 websocket 進行通信,發送和返回的內容都是 json 格式。發送的格式大概是這樣:
{
"id": id,
"method": command,
"params": params
}
換成一個實際的例子可能是這樣:
{"id": 1,
"method: "Page.enable",
"params": {}
}
{
"id": 2,
"method": "Page.navigate",
"params": {"url": "https://www.github.com"}
}
幾個關鍵的url:
http://localhost:9222/json/new
http://localhost:9222/json/close/tab_id
其中第一個 URL 是獲取當前所有打開的 TAB 頁,第二個是新建一個 TAB 頁,第三個是根據 TAB 頁的 id 關閉這個 TAB 頁。當我們請求第一個 URL 時,返回的內容大概如下:
[
{
"description": "",
"id": "c33a4799-13e0-4b6a-b636-fd717c32c941",
"title": "a.html",
"type": "page",
"url": "http://x.x.x.x/a.html"
},
{
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/1adf9b16-5cca-483e-874a-2a53f4b131ca",
"id": "1adf9b16-5cca-483e-874a-2a53f4b131ca",
"title": "about:blank",
"type": "page",
"url": "about:blank",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/1adf9b16-5cca-483e-874a-2a53f4b131ca"
}
]
這里面可以拿到每個 TAB 頁的詳細信息。
第二個新建 TAB 頁訪問之后,也會返回新 TAB 頁的信息。其中就有一個很重要的字段:webSocketDebuggerUrl,這個就是我們要拿的 websocket 的地址。
Page.navigate命令

其socket返回包為
{"id":2,"result":{"frameId":"33320.1"}}{"method":"Page.frameNavigated","params":{"frame":
{"id":"33320.1","loaderId":"33320.2","url":"http://x.x.x.x/a.html","securityOrigin":"http://x.x.x.x","mimeType":"text/html"}}}
{"method":"Page.javascriptDialogOpening","params":
{"message":"9527","type":"alert"}}
{"method":"Page.javascriptDialogClosed","params":
{"result":true}}
{"method":"Page.loadEventFired","params":{"timestamp":131319.852874}}
{"method":"Page.frameStoppedLoading","params":
{"frameId":"33320.1"}}
{"method":"Page.domContentEventFired","params":{"timestamp":131319.853225}
從內容可以看出來是頁面渲染時瀏覽器通知客戶端瀏覽器發生的事件。
漏洞判別標準及如何實現
1、 監聽頁面的彈窗事件:
通過循環監聽Page.javascriptDialogOpening的結果,判斷頁面是否存在彈窗事件。
其socket回包是:
{"method":"Page.javascriptDialogOpening","params":
{"url":"http://xss.php","message":"1","type":"alert","hasBrowserHandler":false,"defaultPrompt":""}
}
2、 查看dom中的localName是否有存在我們自定義的標簽
通過循環監聽DOM.getDocument的return來判斷我們自定義的標簽是否被解析。其數據包如下:
{"id":2324,"result":{"root":{"nodeId":30453,"backendNodeId":6,"nodeType":9,"nodeName":"#document","localName":"","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30454,"parentId":30453,"backendNodeId":7,"nodeType":1,"nodeName":"HTML","localName":"html","nodeValue":"","childNodeCount":2,"children":[{"nodeId":30455,"parentId":30454,"backendNodeId":8,"nodeType":1,"nodeName":"HEAD","localName":"head","nodeValue":"","childNodeCount":0,"children":[],"attributes":[]},{"nodeId":30456,"parentId":30454,"backendNodeId":9,"nodeType":1,"nodeName":"BODY","localName":"body","nodeValue":"","childNodeCount":4,"children":[{"nodeId":30457,"parentId":30456,"backendNodeId":10,"nodeType":1,"nodeName":"TABLE","localName":"table","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30458,"parentId":30457,"backendNodeId":11,"nodeType":1,"nodeName":"TBODY","localName":"tbody","nodeValue":"","childNodeCount":2,"children":[{"nodeId":30459,"parentId":30458,"backendNodeId":12,"nodeType":1,"nodeName":"TR","localName":"tr","nodeValue":"","childNodeCount":2,"children":[{"nodeId":30460,"parentId":30459,"backendNodeId":13,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30461,"parentId":30460,"backendNodeId":14,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"id"}],"attributes":[]},{"nodeId":30462,"parentId":30459,"backendNodeId":15,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30463,"parentId":30462,"backendNodeId":16,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"username"}],"attributes":[]}],"attributes":[]},{"nodeId":30464,"parentId":30458,"backendNodeId":17,"nodeType":1,"nodeName":"TR","localName":"tr","nodeValue":"","childNodeCount":2,"children":[{"nodeId":30465,"parentId":30464,"backendNodeId":18,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":0,"children":[],"attributes":[]},{"nodeId":30466,"parentId":30464,"backendNodeId":19,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30467,"parentId":30466,"backendNodeId":20,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"jim"}],"attributes":[]}],"attributes":[]}],"attributes":[]}],"attributes":["class","itable","border","1","cellspacing","0","width","300px","height","150"]},{"nodeId":30468,"parentId":30456,"backendNodeId":21,"nodeType":1,"nodeName":"TABLE","localName":"table","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30469,"parentId":30468,"backendNodeId":22,"nodeType":1,"nodeName":"TBODY","localName":"tbody","nodeValue":"","childNodeCount":2,"children":[{"nodeId":30470,"parentId":30469,"backendNodeId":23,"nodeType":1,"nodeName":"TR","localName":"tr","nodeValue":"","childNodeCount":2,"children":[{"nodeId":30471,"parentId":30470,"backendNodeId":24,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30472,"parentId":30471,"backendNodeId":25,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"id"}],"attributes":[]},{"nodeId":30473,"parentId":30470,"backendNodeId":26,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":1,"children":[{"nodeId":30474,"parentId":30473,"backendNodeId":27,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"username"}],"attributes":[]}],"attributes":[]},{"nodeId":30475,"parentId":30469,"backendNodeId":28,"nodeType":1,"nodeName":"TR","localName":"tr","nodeValue":"","childNodeCount":2,"children":[{"nodeId":30476,"parentId":30475,"backendNodeId":29,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":0,"children":[],"attributes":[]},{"nodeId":30477,"parentId":30475,"backendNodeId":30,"nodeType":1,"nodeName":"TD","localName":"td","nodeValue":"","childNodeCount":0,"children":[],"attributes":[]}],"attributes":[]}],"attributes":[]}],"attributes":["class","itable","border","1","cellspacing","0","width","300px","height","150"]},{"nodeId":30478,"parentId":30456,"backendNodeId":31,"nodeType":3,"nodeName":"#text","localName":"","nodeValue":"select * from users where id = 1select * from users where id = 2"},{"nodeId":30479,"parentId":30456,"backendNodeId":32,"nodeType":1,"nodeName":"WEBSCAN","localName":"webscan","nodeValue":"","childNodeCount":0,"children":[],"attributes":[]}],"attributes":[]}],"attributes":[],"frameId":"374820F555469428D6636693E4F63022"}],"documentURL":"http://xss.php%3Cwebscan%3E%3C/webscan%3E","baseURL":"http://xss.php%3Cwebscan%3E%3C/webscan%3E","xmlVersion":""}}}
3、 通過解析DOM.getDocument的return里的 nodeValue來判斷payload是否存在于最后渲染的頁面里。
一些細節:
1、 如何觸發事件的彈窗,通過遍歷dom樹觸發事件來觸發onerror=alert之類的彈窗

2、 如何支持post請求:

chrome遠程調試的配置:
chrome-canary --remote-debugging-port=9222 --headless -remote-debugging-address=0.0.0.0 --disable-xss-auditor --no-sandbox --disable-web-security
這里關閉了xss-auditor 和安全相關的一些參數。所以事實上如果不對參數進行處理部署在內網可能會導致ssrf的情況。
三種不同的判斷邏輯的結果: scan_result結果:
# level 3 代表觸發了Page.javascriptDialogOpening事件
{'url': u'http://xss.php', 'vul': 'xss', 'post': '', 'method': u'GET', 'level': '3'}
# level 2 代表dom樹的節點包含了我們自定義的<webscan></webscan>標簽
{'url': u'http://xss.php', 'vul': 'xss', 'post': '', 'method': u'GET', 'level': '2'}
# level 1 代表渲染后的nodeValue包含我們的payload
{'url': u'http://xss.php', 'vul': 'xss', 'post': u'id1=1&id2=2test_test', 'method': u'POST', 'level': '1'}
源碼及使用方法
Mac os 安裝 chrome-canary:
brew install Caskroom/versions/google-chrome-canary
啟動chrome遠程調試:
chrome-canary --remote-debugging-port=9222 --headless -remote-debugging-address=0.0.0.0 --disable-xss-auditor --no-sandbox --disable-web-security
centos7:
安裝chrome
$ vi /etc/yum.repos.d/google-chrome.repo
寫入如下內容:
[google-chrome]
name=google-chrome
baseurl=http://dl.google.com/linux/chrome/rpm/stable/$basearch
enabled=1
gpgcheck=1
gpgkey=https://dl.google.com/linux/linux_signing_key.pub
然后
$ sudo yum install google-chrome-stable
后臺啟動chrome-stable
nohup google-chrome-stable --disable-gpu --remote-debugging-port=9222 --headless -remote-debugging-address=0.0.0.0 --disable-xss-auditor --no-sandbox --disable-web-security > chromeheadless.out 2>&1 &
chrome_headless_xss
# tmp_url為添加payload的url,如果是post請求則為原始url
chrome_headless_drive = ChromeHeadLess(url=tmp_url,
ip="127.0.0.1",
port="9222",
cookie="",
post="",
auth="",
payloads= payload)
scan_result = chrome_headless_drive.run()
scan_result結果:
# level 3 代表觸發了Page.javascriptDialogOpening事件
{'url': u'http://xss.php', 'vul': 'xss', 'post': '', 'method': u'GET', 'level': '3'}
# level 2 代表dom樹的節點包含了我們自定義的<webscan></webscan>標簽
{'url': u'http://xss.php', 'vul': 'xss', 'post': '', 'method': u'GET', 'level': '2'}
# level 1 代表渲染后的nodeValue包含我們的payload
{'url': u'http://xss.php', 'vul': 'xss', 'post': u'id1=1&id2=2test_test', 'method': u'POST', 'level': '1'}
源碼鏈接:
https://github.com/neverlovelynn/chrome_headless_xss/
總結及思考
-
其實使用websocket和chrome進行通信整個過程是異步的,使用異步的方法可以解決粗暴的通過超時來控制循環監聽的問題,同時也能提高掃描效率。
-
在關閉了同源策略的情況下,可能會導致內網ssrf,所以要對傳入參數進行處理。可以嘗試用其他方法實現post請求,如在Network.requestWillBeSent時修改請求參數。
-
由于企業內部對qps有限制,我們掃描的payload數量會被限制的很少。不能進行fuzz,如果需要fuzz模塊可以參考 https://github.com/bsmali4/xssfork 的fuzz模塊進行payload的fuzz。另外我有一個想法就是既然能得到最后的dom,是否能通過對指紋上下文進行分析自動生成精準的payload。但是想了很久也沒想到優雅的實現方式。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/641/
暫無評論