尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Burst Lab | PHP 审计 09|文件上传代码审计:校验缺陷与多种绕过手段

Burst Lab | PHP 审计 09|文件上传代码审计:校验缺陷与多种绕过手段 Burst Lab | PHP 审计 09文件上传代码审计校验缺陷与多种绕过手段Burst Lab | PHP 审计 09文件上传代码审计校验缺陷与多种绕过手段前言文件上传不是一个点而是一条链一、先建立威胁模型上传成功不等于 RCE二、PHP 上传代码审计先找完整链路2.1 入口$_FILES 不是终点2.2 推荐使用“上传审计六元组”三、最常见的校验缺陷与绕过方向3.1 只做前端校验等于没有服务端校验3.2 信任 $_FILES[type] 或请求 Content‑Type3.3 只校验扩展名文件名解析差异3.4 只做“文件头”或“魔数”校验3.5 用户可控文件名或路径目录穿越、覆盖与IDOR3.6 上传目录位于 Web 根目录且可被解释执行3.7 图片、文档与压缩包后处理风险会在上传之后出现3.8 文件公开预览上传漏洞不一定执行脚本也可能导致 XSS四、一个高质量审计案例应该怎么写4.1 有缺陷的示例4.2 不要只写“可绕过”而应这样分析五、一份可落地的 PHP 8 上传实现5.1 这段代码防住了什么5.2 它仍然不替代什么六、审计时最容易漏掉的十个问题七、防御优先级从根本到补充第一优先级最小业务类型白名单第二优先级存储隔离与禁止执行第三优先级服务端重命名与授权第四优先级内容解析与资源约束第五优先级日志与监控总结免责声明Burst Lab | PHP 审计 09文件上传代码审计校验缺陷与多种绕过手段本文从代码审计与防御验证视角讲解 PHP 文件上传。重点不是记住某个“万能后缀”或某段上传脚本而是识别上传链路中不同组件对“文件名、类型、内容、路径、访问方式”的理解差异。所有验证都应在本地靶场或已授权测试环境中完成。文中的测试建议使用无害标记文件不应上传可执行脚本或对生产系统进行绕过测试。前言文件上传不是一个点而是一条链很多人审计上传功能时只盯着下面这一句move_uploaded_file($_FILES[file][tmp_name],$target);但真正的上传风险往往不在“移动文件”这一瞬间而出现在整条处理链的任意一个环节浏览器构造请求 ↓ PHP 解析 multipart/form-data ↓ 业务代码读取 $_FILES ↓ 文件名 / 扩展名 / MIME / 内容校验 ↓ 重命名与路径拼接 ↓ 落盘存储 ↓ 缩略图、解压、转码、病毒扫描等后处理 ↓ 下载、预览或 Web 服务器直接访问同一个文件可能被多次“解释”浏览器解释Content-TypePHP 提供原始文件名业务代码用pathinfo()取扩展名finfo_file()推断 MIME图片库解析像素Web 服务器再根据目录和扩展名决定如何响应。高危漏洞的本质通常就是这些解释结果不一致。本文的目标是建立一套可复用的方法如何快速定位 PHP 上传入口如何判断校验是否只是“看起来存在”常见绕过方向分别对应什么代码缺陷如何区分“可以上传异常文件”与“能够造成代码执行”怎样写出可维护、可验证的上传防护代码。一、先建立威胁模型上传成功不等于 RCE文件上传漏洞的影响至少可以分成五类风险类别典型后果审计时应确认的证据非法文件落盘敏感或不符合业务规则的文件被保存校验与实际落盘结果不一致覆盖与越权覆盖其他用户文件、替换业务资源文件名/对象 ID/目标路径可控存储型 XSS浏览器将上传内容当作可执行页面展示上传内容可公开访问且响应头不安全资源消耗大文件、压缩炸弹、图片解码消耗 CPU/内存大小、像素、解压后体积未限制代码执行上传文件被脚本引擎或危险后处理执行文件可执行、目录可解析、访问路径可达因此文章或审计报告不能只写“绕过了上传限制”。更准确的结论应当是校验缺陷仅校验客户端 MIME。 验证结果服务端接受了与声明类型不一致的文件。 实际影响异常内容可落盘当前上传目录位于 Web 根目录外且通过下载控制器按 attachment 返回未证实代码执行。 风险评级中危待结合公开访问、后处理组件继续评估。这才是高质量审计结论先说明缺陷再说明可验证影响不夸大。二、PHP 上传代码审计先找完整链路2.1 入口$_FILES不是终点全局检索以下关键字$_FILES move_uploaded_file is_uploaded_file rename copy file_put_contents Storage::putFile storeAs finfo_file getimagesize imagecreatefrom ZipArchive readfile找到上传控制器后不要立即下结论。请继续追踪原始文件名来自哪里扩展名怎么取MIME 怎么判断文件内容是否解析最终保存到哪里是否使用用户提供的文件名上传后是否有解压、转码、OCR、预览、同步等后处理文件如何被访问、下载或预览。2.2 推荐使用“上传审计六元组”对每个上传点记录六项信息Input$_FILES 中哪些字段可控 Validation名称、扩展名、MIME、内容、大小分别怎么校验 Transform是否重命名、转码、压缩、解压、生成缩略图 Storage保存目录是否在 Web 根目录下是否可覆盖 Exposure用户如何访问文件直链、下载控制器还是 CDN ExecutionWeb 服务器、解释器、后处理程序会不会执行或解析它只要其中一个环节没有闭环校验就可能被后续环节抵消。三、最常见的校验缺陷与绕过方向下面的“绕过方向”用于理解审计思路和本地无害验证不提供可执行脚本或攻击载荷。3.1 只做前端校验等于没有服务端校验前端限制示例仅浏览器层面生效inputtypefileacceptimage/*// JS前端后缀校验可被直接绕过functioncheckFile(){letextdocument.getElementById(f).value.split(.).pop();if(![jpg,png].includes(ext))alert(仅图片);}为什么有问题前端JS、HTML属性只约束普通浏览器用户。攻击者抓包重放HTTP请求可以完全绕过全部前端逻辑如果后端不复做校验安全规则直接失效。审计关注点本地验证思路抓包观察上传HTTP请求看服务端是否独立做校验是否只依靠accept属性、JS做类型/后缀拦截后端完全没有对扩展名、MIME、文件内容的判断逻辑。修复原则前端只做用户体验优化全部安全校验必须放在服务端执行。3.2 信任$_FILES[type]或请求Content‑Type?php// $_FILES[type]来自HTTP请求头由客户端可控if($_FILES[file][type]!image/jpeg){exit(only jpeg);}move_uploaded_file($_FILES[file][tmp_name],upload/1.jpg);问题本质$_FILES[file][type]直接取自客户端上传的Content‑Type请求头不是服务端对文件内容检测得到的可信结果。攻击者抓包即可随意篡改这个值实现类型绕过。审计关注点是否直接信任$_FILES[xxx][type]做安全判断是否把客户端传入MIME直接写入数据库、直接拿来设置HTTP响应头“前端限制 仅客户端MIME判断”的双重伪校验组合。修复示例服务端读取真实文件MIME?php$finfonewfinfo(FILEINFO_MIME_TYPE);$real_mime$finfo-file($_FILES[file][tmp_name]);$allow[image/jpeg,image/png];if(!in_array($real_mime,$allow))exit(非法文件);注意MIME检测仅作为一层防护不能单独作为唯一安全防线。3.3 只校验扩展名文件名解析差异?php$extpathinfo($_FILES[file][name],PATHINFO_EXTENSION);if($ext!jpg){exit(invalid extension);}$dest./upload/.$_FILES[file][name];move_uploaded_file($_FILES[file][tmp_name],$dest);问题本质扩展名只是文件名的文本片段不能代表文件真实内容。同时不同组件对文件名解析行为不一致多点后缀、大小写、尾随点/空格、Unicode字符业务代码、反向代理、操作系统可能处理结果各不相同容易出现校验逻辑与实际存储逻辑脱节。审计关注点无害测试用例不上传可执行脚本仅测试文件名解析行为多点文件名a.xxx.jpg大小写变体A.JPGWindows下尾随点、尾随空格test.jpg.Unicode组合字符、超长文件名记录三处结果业务代码接收的文件名、磁盘实际保存文件名、URL访问时使用的文件名看三者是否出现不一致。修复原则?php// 原始文件名仅用于页面展示绝不用于存储$origin_name$_FILES[file][name];// 服务端生成随机存储文件名$save_nameuuid_create()..jpg;$dest./upload/.$save_name;move_uploaded_file($_FILES[file][tmp_name],$dest);// 数据库保存原始展示名、随机存储名、归属用户、检测得到的真实类型存储后缀由服务端检测到的真实文件类型映射生成不要直接复用用户提交文件名。3.4 只做“文件头”或“魔数”校验?php$fpfopen($_FILES[file][tmp_name],rb);$magicfread($fp,2);fclose($fp);if($magic!\xff\xd8)exit(不是jpeg);move_uploaded_file($_FILES[file][tmp_name],upload/a.jpg);错误认知文件头部字节匹配图片魔数就代表整个文件是安全图片。问题本质魔数只能简单过滤一部分恶意文件。攻击者可以构造头部是合法图片签名后面拼接恶意数据的文件。只校验文件头无法校验完整文件格式如果后续业务有图片解析、文档解析、文件包含等后处理逻辑依旧存在风险。OWASP明确要求魔数只能作为多层防护的其中一环不能单独作为安全边界。审计关注点是否仅读取头部少量字节就直接放行上传之后是否交给图片库、文档转换器、解压程序做二次处理是否没有做完整格式解析校验。修复原则图片业务推荐重编码使用图片库完整解码文件确认可以正常解析根据解析出的真实格式确定输出后缀将像素数据重新编码生成全新文件删除原始上传文件新文件依旧保存在不可执行目录。重编码不能做到绝对安全目的是剥离原始文件携带的畸形数据、EXIF恶意注释等歧义内容。3.5 用户可控文件名或路径目录穿越、覆盖与IDOR?php// 直接拼接用户可控原始文件名$name$_FILES[file][name];$target__DIR__./uploads/.$name;move_uploaded_file($_FILES[file][tmp_name],$target);即使加上basename()也只能缓解部分路径穿越无法解决重名覆盖、越权下载、响应头注入等问题。问题本质攻击者传入../../shell.php这类文件名有可能跳出预设上传目录写入任意位置文件。move_uploaded_file()仅校验源文件来自HTTP上传不会做路径合法性、权限、覆盖保护校验目标文件存在时会直接覆盖。审计关注点是否直接拼接用户可控文件名、用户ID、对象ID构造存储路径文件存在是否直接覆盖是否使用realpath()严格锁定基目录下载接口是否只根据文件名做读取缺少归属用户鉴权IDOR。修复核心思路存储文件名全部由服务端随机生成上传目录写死为程序常量不允许用户修改数据库记录原始展示名、随机存储键、归属用户ID、文件大小、检测类型下载、预览接口通过文件ID查询数据库做所有权鉴权不要直接根据文件名读取磁盘。3.6 上传目录位于 Web 根目录且可被解释执行这一步是“文件可以落盘”升级为RCE高危风险的关键条件。高危组合条件上传目录在网站Web根目录内可以通过HTTP直接访问Web服务器(Nginx/Apache/IIS)对此目录继承脚本解析规则攻击者可以上传、构造可被解析的后缀文件。审计关注点上传目录是否处于站点根目录、静态资源目录、别名映射路径Web服务器配置是否允许该目录执行脚本是否存在二次解析、try_files错误配置预览功能是否直接重定向到上传目录URL。最高优先级修复上传文件存储在Web根目录之外。如果必须对外提供访问使用PHP控制器接口按数据库ID读取文件设置安全HTTP头Content-Disposition: attachment X-Content-Type-Options: nosniff不要依靠黑名单拦截少数脚本后缀当作唯一防御手段真正的边界是存储隔离禁止脚本执行。3.7 图片、文档与压缩包后处理风险会在上传之后出现即使上传接口校验全部通过如果业务后续有自动化处理逻辑漏洞依旧可能发生。处理动作审计关注点生成图片缩略图畸形图片、超大尺寸DoS、第三方图片库CVE漏洞文档转PDF、预览转换器沙箱、宏、外部资源引用、参数注入ZIP解压Zip‑Slip路径穿越、压缩炸弹、解压条目数量限制音视频转码外部二进制调用、参数拼接、资源配额隔离杀毒扫描扫描失败、超时时是否直接放行文件危险解压伪代码示例没有路径校验?php$zipnewZipArchive();$zip-open($_FILES[file][tmp_name]);// 直接解压不校验每个输出文件路径存在Zip‑Slip风险$zip-extractTo(./upload/);压缩包修复关键点遍历压缩包内每一个条目校验解压目标路径必须严格落在预设目录之内同时限制解压文件数量、解压后总大小、处理超时时间。3.8 文件公开预览上传漏洞不一定执行脚本也可能导致 XSS问题本质上传SVG、HTML、JS、XML这类浏览器会直接解析的文件如果服务端直接以内联inline方式返回给浏览器会触发存储型XSSMIME嗅探漏洞也可能改变文件解析方式。危险示例直接使用客户端传入的MIME设置响应头?php// $mime来自用户上传提交的值完全可控header(Content‑Type: .$_GET[mime]);readfile(./upload/.$_GET[fileid]);审计关注点Content‑Type是否直接取自数据库中保存的用户提交MIME预览下载接口是否使用Content‑Disposition: inline是否缺少X‑Content‑Type‑Options: nosniff用户上传文件是否和主站共用同一个Cookie安全域。修复原则不支持预览的文件一律强制attachment下载预览尽量部署在独立隔离域名Content‑Type必须由服务端校验后硬编码输出不能取自用户输入SVG、HTML等可渲染格式单独做严格策略不要混入普通图片上传逻辑。四、一个高质量审计案例应该怎么写4.1 有缺陷的示例?phpif($_FILES[avatar][type]!image/jpeg){exit(only jpg);}$filename$_FILES[avatar][name];$path__DIR__./public/uploads/.$filename;move_uploaded_file($_FILES[avatar][tmp_name],$path);echo/uploads/.$filename;4.2 不要只写“可绕过”而应这样分析Input客户端可控制文件名、Content-Type、文件内容和文件大小。 Validation只校验 $_FILES[avatar][type]属于客户端声明的 MIME。 Transform无重命名无解析无重编码。 Storage落盘到 public/uploads文件名使用用户输入可能重名覆盖。 Exposure接口直接返回可访问 URL文件位于 Web 根目录。 Execution需继续检查 Web 服务器对此目录和不同文件类型的实际处理规则。 结论存在不可信 MIME 校验和用户可控文件名问题可导致异常内容落盘、文件覆盖及公开访问。是否构成脚本执行取决于 Web 服务器解析规则不能仅凭 PHP 代码直接下结论。这段分析比一句“文件上传 getshell”更专业也更适合真实项目和面试场景。五、一份可落地的 PHP 8 上传实现以下示例以“仅接受 JPEG/PNG 头像”为目标。它展示防护思路不应机械复制到所有文件业务。?phpdeclare(strict_types1);constMAX_UPLOAD_BYTES5*1024*1024;// 5 MiBconstUPLOAD_DIR__DIR__./../private-uploads/avatars;/** var arraystring, string MIME storage extension */constALLOWED_IMAGE_TYPES[image/jpegjpg,image/pngpng,];functionfailUpload(string$message,int$status400):never{http_response_code($status);exit($message);}if(!isset($_FILES[avatar])||!is_array($_FILES[avatar])){failUpload(Missing upload field.);}$file$_FILES[avatar];if(($file[error]??UPLOAD_ERR_NO_FILE)!UPLOAD_ERR_OK){failUpload(Upload failed.);}$tmpName$file[tmp_name]??;$size$file[size]??0;if(!is_string($tmpName)||!is_uploaded_file($tmpName)){failUpload(Invalid upload source.);}if(!is_int($size)||$size1||$sizeMAX_UPLOAD_BYTES){failUpload(Invalid file size.);}$finfonewfinfo(FILEINFO_MIME_TYPE);$mime$finfo-file($tmpName);if(!is_string($mime)||!array_key_exists($mime,ALLOWED_IMAGE_TYPES)){failUpload(Unsupported image type.);}$imageInfogetimagesize($tmpName);if($imageInfofalse){failUpload(Invalid image content.);}[$width,$height]$imageInfo;if($width1||$height1||$width4096||$height4096){failUpload(Invalid image dimensions.);}if(!is_dir(UPLOAD_DIR)!mkdir(UPLOAD_DIR,0750,true)!is_dir(UPLOAD_DIR)){failUpload(Storage unavailable.,500);}$storageNamebin2hex(random_bytes(16))...ALLOWED_IMAGE_TYPES[$mime];$destinationUPLOAD_DIR.DIRECTORY_SEPARATOR.$storageName;if(!move_uploaded_file($tmpName,$destination)){failUpload(Could not store upload.,500);}// 数据库中应记录storageName、originalName、mime、size、ownerId、createdAt。// 不应直接使用原始文件名作为磁盘路径或公开访问路径。echojson_encode([oktrue,fileId$storageName,],JSON_THROW_ON_ERROR);5.1 这段代码防住了什么不信任前端accept属性不信任$_FILES[type]检查 PHP 上传错误码确认临时文件确实来自 HTTP 上传以服务端检测到的 MIME 做白名单用getimagesize()确认图片可被基本解析限制字节大小和像素尺寸服务端生成存储名保存到 Web 根目录之外不向客户端暴露真实磁盘路径。5.2 它仍然不替代什么在真实系统中还可能需要用户认证、对象所有权和上传频率限制恶意文件扫描与隔离流程异步图片重编码、队列资源限制存储桶访问策略与签名 URL下载接口的权限控制和审计日志对图片库、文档转换器等第三方依赖的持续更新。六、审计时最容易漏掉的十个问题只看move_uploaded_file()没看文件后续如何被访问只检查前端accept没有服务端白名单信任$_FILES[type]用原始文件名做存储名没有处理重名覆盖和并发竞争上传目录在 Web 根目录且继承了脚本解析规则只限制文件字节数没限制图片像素或解压后体积解压 ZIP 前没有检查条目路径和总资源消耗下载接口没有鉴权只要猜到文件 ID 就能读取认为“禁止 PHP 后缀”就等于安全。七、防御优先级从根本到补充第一优先级最小业务类型白名单业务只需要头像就只接收经过验证的 JPEG、PNG 或 WebP不要做“除了少数危险类型外都允许”的黑名单。第二优先级存储隔离与禁止执行文件保存到 Web 根目录之外。需要下载时通过受鉴权的控制器读取需要预览时使用独立域、明确响应头和受控类型。第三优先级服务端重命名与授权客户端不能决定磁盘路径、对象归属或覆盖目标。文件 ID、所有权和访问权限必须由服务端维护。第四优先级内容解析与资源约束对图片、压缩包、文档、音视频建立各自的格式校验、资源上限和隔离策略不要用一种通用正则解决所有文件类型。第五优先级日志与监控记录上传用户、文件 ID、原始名、检测类型、大小、失败原因和访问事件。大量被拒绝的类型、异常大小、短时间高频上传都应纳入告警。总结文件上传代码审计最重要的不是“能否改一个后缀绕过”而是识别整个链路的解释差异用户声明的文件名 / MIME ≠ 服务端识别到的内容类型 ≠ 文件系统最终保存的名称 ≠ Web 服务器或预览组件的处理方式只校验扩展名、只校验客户端 MIME、只看文件头、直接使用原始文件名、把上传文件放在 Web 根目录——这些做法单独看都可能“似乎有校验”组合起来却很容易形成真实风险。审计时请始终按下面的顺序追踪输入 → 校验 → 转换 → 存储 → 暴露 → 执行。防御时请优先做到严格白名单、服务端重命名、根目录外存储、禁止执行、受控访问、资源限制。做到这些文件上传才会从“靠黑名单赌运气”变成可验证、可维护的安全能力。免责声明本文全部内容仅用于代码审计学习、安全原理研究、授权靶场实验教学帮助开发者理解文件上传漏洞的产生原因、审计思路与防御方案。文中涉及的漏洞分析、缺陷复现思路仅允许在个人本地搭建、拥有完整授权的隔离靶场环境中开展验证。严禁未经授权对任何公网业务系统、第三方站点实施上传测试、漏洞探测、恶意文件上传等行为所有操作必须严格遵守《中华人民共和国网络安全法》《中华人民共和国刑法》相关规定未授权测试将承担相应法律责任。文中给出的 PHP 防护代码为参考示例仅代表学习层面的防护思路不保证直接复制即可适配全部业务场景生产环境上线前需要结合业务场景做完整安全测试。任何个人、组织若利用本文讲解的技术内容实施违规、违法行为由此产生的全部民事、刑事法律责任由行为人自行承担本文作者不承担任何连带责任。本文为原创学习笔记转载、引用请标注原文出处禁止未经许可用于商业培训、商业文档汇编。若文中存在原理错误、代码疏漏欢迎评论区指正交流。
返回列表