PbootCMS CSRF和存储型XSS

Report 漏洞分析

PbootCMS 存储型XSS

这本来是个简单的后台存储型XSS漏洞,后台请求没验证Referer,但设置有表单提交的FormToken。在我了解到一些有趣的特性后发现FormToken是可预测的,因此可以实现CSRF与XSS的组合攻击。 首先在\apps\common\AdminController.php的第83行:

<?php
// 首次加载时,生成页面验证码
    if (! issetSession('formcheck')) {
        session('formcheck', get_uniqid());
    }
    $this->assign('formcheck', session('formcheck')); // 注入formcheck模板变量
?>

首次加载后台页面会用get_uniqid()函数生成formcheck,跟进该函数到\core\function\handle.php第439行:

<?php
    function get_uniqid(){
        return mt_rand();
    }
?>

简单点说就是mt_rand()生成随机数,再用encrypt_string()编码作为formcheck的值。本来用随机数作为表单凭证是没有问题的,但mt_rand()和其他容易出问题的函数一样,它生成的不是真随机数。该函数的英文版文档有如下说明:

Caution:This function does not generate cryptographically secure values, and should not be used for cryptographic purposes. If you need a cryptographically secure value, consider using random_int(), random_bytes(), or openssl_random_pseudo_bytes() instead.

它警告开发者mt_rand()生成的数值不符合密码学安全,不适用于加密,应当用其他更安全的随机函数替代它。现在我们知道mt_rand()生成随机数是不安全的,但这个随机数为什么不安全呢?PHP进程期间只要是同一个进程处理的请求,mt_rand()都会共享相同的种子,以下面的代码为例:

<?php
    mt_srand(12345678);
    echo mt_rand()."<br/>";
    echo mt_rand()."<br/>";
    echo mt_rand()."<br/>";
?>

无论页面刷新多少次生成的随机数都是固定的,知道种子的值就能预测下一个随机数。不过mt_rand()算法复杂不可逆,php_mt_seed也是通过穷举种子推算随机数,和已知随机数序列对比来破解的。为了实现formcheck的可预测,要先了解php_mt_seed的工作流程,以下面代码为例:

    mt_srand();
    echo mt_rand()."<br/>";
    echo mt_rand()."<br/>";
    echo mt_rand()."<br/>";

我们用mt_srand()随机播种生成了三个随机数,再将生成的第一个随机数1760754047交给php_mt_seed破解:

root@kali:./php_mt_seed 1760754047
Pattern: EXACT
Version: 3.0.7 to 5.2.0
Found 0, trying 0x20000000 - 0x23ffffff, speed 2334.2 Mseeds/s 
seed = 0x21dd2356 = 568140630 (PHP 3.0.7 to 5.2.0)
seed = 0x21dd2357 = 568140631 (PHP 3.0.7 to 5.2.0)
Found 2, trying 0xfc000000 - 0xffffffff, speed 2710.2 Mseeds/s 
Version: 5.2.1+
Found 2, trying 0x38000000 - 0x39ffffff, speed 25.4 Mseeds/s 
seed = 0x38275304 = 942101252 (PHP 7.1.0+)
Found 3, trying 0xec000000 - 0xedffffff, speed 25.2 Mseeds/s 
seed = 0xec972aca = 3969329866 (PHP 5.2.1 to 7.0.x; HHVM)
seed = 0xec972aca = 3969329866 (PHP 7.1.0+)
Found 5, trying 0xee000000 - 0xefffffff, speed 25.2 Mseeds/s 
seed = 0xeeeb73cc = 4008408012 (PHP 5.2.1 to 7.0.x; HHVM)
seed = 0xeeeb73cc = 4008408012 (PHP 7.1.0+)
Found 7, trying 0xfe000000 - 0xffffffff, speed 25.1 Mseeds/s 
Found 7

php_mt_seed穷举出几个可能的种子值,我们将3969329866设定为种子值,最后生成的三个随机数和刚开始随机生成的一模一样:

<?php
    mt_srand(3969329866);
    for($i=0;$i<3;$i++){
        echo mt_rand()."<br/>";
    }
?>

回头看PbootCMS同样是通过mt_rand()生成fromcheck的,而且生成Token的get_uniqid()函数在AdminController的析构函数__construct中, Token被放在session陪伴整个任务进程,也就是说在访问后台页面后,注销账户前都会使用相同的Token。因此漏洞的执行链很清晰了,访问后台页面获取一个mt_rand()生成的随机数,利用php_mt_seed破解种子值,通过种子推出可能的随机数,将XSS和可能的formcheck放到CSRF表单就能完成后台写入XSS,打到前台的目的。