作者:LoRexxar'@知道創宇404實驗室
時間:2021年4月16日
4月12號,@cursered在starlabs上公開了一篇文章《You Talking To Me?》,里面分享了關于Webdriver的一些機制以及安全問題,通過一串攻擊鏈,成功實現了對Webdriver的RCE,我們就順著文章的思路來一起看看~
什么是Webdriver?
WebDriver是W3C的一個標準,由Selenium主持。具體的協議標準可以從http://code.google.com/p/selenium/wiki/JsonWireProtocol#Command_Reference查看。
通俗的講,WebDriver就是一個閹割版的瀏覽器,他提供了用于自動化控制瀏覽器的協議和接口。
你可以通過https://chromedriver.chromium.org/downloads來下載chrome版本的Webdriver,其中chrome還提供了headless模式以供沒有桌面系統的服務器運行。
一般來說,Webdriver應用于爬蟲等需要大范圍Web請求掃描的場景,在安全領域,掃描器一般都需要通過selenium來控制webdriver完成前置掃描。在CTF當中,我們也能常常見到通過控制Webdriver來訪問XSS挑戰的XSS Bot.
這里我借用一張原博的圖來描述一下Webdriver是如何工作的。

在整個流程當中,Selenium端點通過向Webdriver端口相應的seesion接口發送請求控制webdriver,webdriver通過預定的調試接口以及相應的協議來和瀏覽器交互(如Chrome通過Chrome DevTools Protocol來交互)。
由于不同的瀏覽器廠商都定義了自己的driver,因此不同的瀏覽器和driver之間使用的協議可能會有所不同。比如Chrome就是用hrome DevTools Protocol。

當然,需要注意的是,這里提到的端口為啟動webdriver時的默認端口,一般來說,我們通過selenium操作的Webdriver將會啟動在隨機端口上。
總之,在正常通過Selenium開啟的webdriver的主機上,將會開放兩個端口,一個是提供selenium操作webdriver的REST API服務,一個則是通過某種協議操作瀏覽器的服務端口。
這里我們用一個普通的python3腳本來啟動一個webdriver來確認這個結論。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import selenium
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverException
import os
chromedriver = "./chromedriver_win32.exe"
browser = webdriver.Chrome(executable_path=chromedriver)
url = "https://lorexxar.cn"
browser.get(url)
# browser.quit()
在腳本執行后顯示的日志中的端口為CDP端口

通過查看進程其中命令可以確認webdriver的端口

Chrome Webdriver 攻擊與利用
在了解了Webdriver基礎之后,我們一起來探討一些整個流程中到底有什么樣得安全隱患。
任意文件讀?
如果對Chrome DevTools Protocol有一些簡單的了解的話,不難發現他本身提供了一些接口來允許你自動化的操作webdriver。通過訪問/json/list可以獲取到所有的瀏覽器實例接口。

通過這里的webSocketDebuggerUrl得到相應的接口路徑,然后我們可以通過websocket來和這個接口進行交互實現CDP的所有功能。例如我們可以通過Page.navigate訪問相應的url,包括file協議

甚至,我們可以通過Runtime.evaluate來執行任意js

如果你對CDP的api感興趣,可以參考https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate
但是問題也來了,我們如何才能從http://127.0.0.1:<CDP Port>/json/list讀取相應的webSocketDebuggerUrl 呢?至少我們沒辦法使用任何非0day來輕易的繞過同源策略的限制,那么我們就需要繼續探索~
通過REST API來RCE
前面提到,selenuim需要通過Webdriver開放的REST API來操作Webdriver。具體API可以參考webdriver協議或源碼https://source.chromium.org/chromium/chromium/src/+/master:chrome/test/chromedriver/server/http_handler.cc。
這里我們主要關注幾個接口
-
GET /sessions從這個端點我們可以獲取到所有目前活躍webdriver 進程的session,并且獲取相應的session id.
-
GET /session/{sessionid}/source如果我們獲取到Session id,那么我們就可以獲取到對應session的各種數據,比如頁面內容。

相應的api可以參考https://www.w3.org/TR/webdriver/#endpoints
POST /session通過POST數據我們可以發起一個新的會話,并且其中允許我們通過POST參數來配置新會話。
https://www.w3.org/TR/webdriver/#dfn-new-sessions
我們甚至可以直接通過設置新會話的bin路徑來啟動其他的應用程序
而相關的配置參數,我們可以直接參考selenium操作配置chrome的文檔https://chromedriver.chromium.org/capabilities

這里我們可以展示通過post來啟動其他應用程序。并且我們可以通過配置args來配置參數。(要注意的是這個api對json的校驗非常嚴格,有任何不符合要求的請求都會報錯)
看到這里,我們有了一個大膽的想法,我們是不是可以通過fetch來發送post請求,即便我們無法獲取返回,我們也可以觸發操作。

理想很豐滿,可惜現實很骨感~
當我們從其他域發起請求時,js請求會自動帶上Origin頭以展示請求來源。服務端會檢查來源,并返回Host header or origin header is specified and is not whitelisted or localhost.
我們可以從chromium種相應的代碼窺得相應的限制。
到目前為止,我們仍然沒有找到任何可以遠程利用的方式,無論是通過webdriver的REST API 來執行命令,
這里我認為比較重要的是,這個校驗來源是std::string origin_header = info.GetHeaderValue("origin");,也就是說,是當發送請求頭中帶Origin時,才會導致這個校驗,眾所周知,只有當使用js發送POST請求時,才會自動帶上這個頭,換言之,這里的校驗并不會影響我們發送GET請求。

跟著源碼,我們可以大致總結這部分的校驗內容

除開上半部分中關于POST請求的校驗以外,下半部分的校驗更加直白,只要allow_remote為假,就一定回進入判斷,也就一定會經過net::IsLocalhost的校驗,而這里的allow_remote默認為假,只有當開啟allow-ips的時候才會為真。所以結論和原文相同。
- 如果chromedriver沒有
--allowed-ips參數- 無論任何類型的請求HOST都需要經過
net::IsLocalhost校驗 - 如果帶有Origin頭,那么Origin頭數據也需要經過
net::IsLocalhost校驗
- 無論任何類型的請求HOST都需要經過
- 如果chromedriver帶有
--allowed-ips參數- GET請求不會檢查HOST
- POST請求:
- 如果帶有Origin頭,那么Origin頭數據需要經過
net::IsLocalhost校驗。 - 如果不帶有Origin頭,那么沒有額外的校驗。(如何用js完成沒有Origin的post請求呢?)
- 如果HOST為ip:port格式,那么ip需要在whitelist中。
- 如果帶有Origin頭,那么Origin頭數據需要經過
綜合前面的所有條件,我們能比較清楚的弄明白,只有在開啟--allowed-ips參數時,我們可以通過綁定域名來發起GET請求對應的API。否則我們就必須讓HOST通過檢查,但可惜的是,僅有ip和localhost能通過net::IsLocalhost校驗。我們可以簡單驗證這一點。

那么問題來了,如果我們可以通過綁定域名來發送GET請求,那么是不是可以通過DNS Rebinding來讀取頁面內容呢?
配合DNS Rebinding來讀取GET返回
我們這里通過模擬一次DNS重綁定來探測,這里用一段簡單的代碼來做check
var i = 0;
var sessionid;
function waitdata(){
fetch("http://r.d73ha3.ceye.io:22827/sessions", {
method: "GET",
mode: "no-cors"
}).then(res => res.json()).then(res => function () {
if(res.value){
sessionid = res.value[0].id;
}
}());
stopwait();
}
function stopwait(){
if(sessionid!=undefined){
console.log(sessionid);
clearInterval(t1);
}
}
t1 = setInterval('i +=1;console.log("wait dns rebinding...test "+i);waitdata()',1000);

可以看到經過63次請求,dns cache失效并成功獲取到了127.0.0.1對應的seesionid。
attack chain!
總結前后的幾個利用點,我們現在可以嘗試把他們串聯起來。
- 受害者使用webdriver訪問exp.com/a.html,a.html掃描127.0.0.1對應webdriver端口。
- 跳轉到
exp.com:<webdriver port>,開始執行JS+DNS Rebinding。 - 通過構造JS+DNS Rebinding,我們可以讀取webdriver端口GET請求的返回,并通過
GET /sessions獲取對應Session的debug端口以及session id。 - 通過Session id,我們可以使用
GET /session/{sessionid}/source獲取對應窗口的頁面內容。 - 通過Session對應的debug端口,我們可以使瀏覽器訪問
http://127.0.0.1:<CDP Port>/json/list,并且通過GET /session/{sessionid}/source獲取返回對應瀏覽器窗口的webSocketDebuggerUrl。 - 通過webSocketDebuggerUrl與瀏覽器窗口會話交互,使用
Runtime.evaluate方法執行JS代碼。 - 構造JS代碼
POST /session執行命令。
這里借用原文當中的一張圖片來展示整個exp利用過程。

寫在最后
在前文中提到過,不同的瀏覽器會采用專屬自己的瀏覽器協議,但其中差異比較大的是firefox和對應的Geckodriver,在Geckodriver上,firefox設計了一套與chrome邏輯差異比較大的調試協議,在原文中,作者使用了一個TCP連接拆分錯誤來完成相應的利用,并且在Firefox 87.0當中被修復。而safaridriver實現了更嚴格的host檢查,導致DNS rebinding漏洞并不能生效。而包括chrome、MS Edge 和 Opera在內的瀏覽器仍然受到這個漏洞威脅。
但可惜的是,盡管這里我們通過實現一個很棒的利用鏈構造利用,但唯一的限制條件,--allowed-ips這個配置卻非常的少見,在普遍通過Selenium來操作webdriver的場景中,一般的用戶都只會配置Chrome的參數選項,而不是webdriver的參數,而且在官網中也明確提出--allowed-ips會導致可能的安全問題。
https://chromedriver.chromium.org/security-considerations
這個條件讓整個漏洞利用變得苛刻起來,但也許在未來的某一天,Chrome的某個新功能就會重寫這部分功能呢?這也說不好對吧~~
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1559/
暫無評論