CTF实战:手把手教你用Hex Fiend绕过PNG上传检测,拿下HarekazeCTF2019的Flag CTF实战Hex Fiend魔改PNG文件头突破双重检测机制在CTF竞赛中文件上传漏洞一直是Web安全赛道的经典题型。这道来自HarekazeCTF2019的Avatar Uploader 1题目巧妙地将finfo_file()和getimagesize()两种检测机制组合在一起为参赛者设置了双重关卡。本文将带你深入十六进制层面用Hex Fiend这把手术刀精准改造PNG文件结构同时突破这两道防线。1. 环境搭建与初步侦察首先访问题目环境系统会提示登录。测试发现任意用户名如test即可进入上传界面关键限制条件如下文件大小 ≤ 256KB图片尺寸 ≤ 256×256像素仅接受PNG格式上传合规PNG后仅返回头像更新成功此时需要分析服务器端验证逻辑。题目虽未直接提供源码但通过报错信息可逆向推导出关键检查点// 文件类型检测 $finfo finfo_open(FILEINFO_MIME_TYPE); $type finfo_file($finfo, $_FILES[file][tmp_name]); if (!in_array($type, [image/png])) { error(Not PNG); } // 图像尺寸检测 $size getimagesize($_FILES[file][tmp_name]); if ($size[0] 256 || $size[1] 256) { error(Image too large); } // 隐藏的彩蛋检测 if ($size[2] ! IMAGETYPE_PNG) { error(Flag: .getenv(FLAG1)); }2. 检测机制深度剖析2.1 finfo_file()的工作原理finfo_file()通过魔数(magic number)识别文件类型。对于PNG文件其固定头部结构为偏移量值十六进制ASCII表示0-789 50 4E 47 0D 0A 1A 0A.PNG....提示前8个字节是PNG的签名区任何PNG文件都必须以这组魔数开头2.2 getimagesize()的检测逻辑该函数返回包含多个维度的数组Array ( [0] 宽度(px) [1] 高度(px) [2] 图像类型常量 [3] HTML格式的宽高字符串 [bits] 位深度 [mime] MIME类型 )其中[2]对应IMAGETYPE常量3表示PNG类型。题目中的彩蛋触发条件正是$size[2] ! IMAGETYPE_PNG。3. Hex Fiend实战操作我们需要构造一个能同时满足finfo_file()识别为PNGgetimagesize()认为不是PNG 的特殊文件。3.1 准备原始PNG文件用画图工具创建200×200像素的PNG图片通过命令行验证文件属性file sample.png # 应显示PNG image identify sample.png # 查看尺寸信息3.2 使用Hex Fiend进行二进制编辑打开Hex Fiend拖入sample.png定位到文件头签名区前8字节应显示89 50 4E 47 0D 0A 1A 0A保留签名区删除后续所有数据块IHDR、IDAT等手动添加以下异常结构00000000: 89 50 4E 47 0D 0A 1A 0A # PNG签名 00000008: 00 00 00 01 49 48 44 52 # 伪造的IHDR长度 00000010: 00 00 00 00 00 00 00 00 # 零宽高 00000018: 08 06 00 00 00 # 位深和压缩方式注意这种结构会通过文件头检查但会破坏正常的PNG解析4. 漏洞利用与防御4.1 上传构造的特殊文件将修改后的文件上传服务器端将发生finfo_file()读取有效签名返回image/pnggetimagesize()解析异常结构IMAGETYPE_PNG校验失败触发彩蛋条件返回flag4.2 安全防护建议对于开发者应该组合使用多种检测方式文件签名验证实际图像内容解析文件扩展名白名单实施二次渲染$img imagecreatefrompng($uploadedFile); imagepng($img, $sanitizedFile);设置严格的权限隔离location ^~ /uploads/ { deny all; php_flag engine off; }5. 扩展思考与变种这种技术在实际渗透测试中也有广泛应用场景绕过CDN的图片安全检查制作Webshell图片马突破云存储的内容审查一个进阶技巧是在保留有效PNG结构的同时通过篡改CRC校验值来触发解析异常# Python示例修改IHDR块的CRC with open(test.png, rb) as f: f.seek(29) # IHDR CRC位置 f.write(b\xFF\xFF\xFF\xFF) # 写入错误CRC这种方法的优势在于能保持文件表面合规性只在深层解析时才会暴露问题。