多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

PHP文件上传漏洞攻防:从绕过技巧到安全防御实践

PHP文件上传漏洞攻防:从绕过技巧到安全防御实践 1. 从一次“意外”的文件上传说起那天我正在为一个内部系统做安全审计目标是一个用PHP写的、看起来平平无奇的后台管理页面。它有一个“上传头像”的功能我随手选了一张普通的JPG图片上传成功。然后我换了一个文件文件名是shell.php.jpg上传系统提示“文件类型不合法”。这很正常很多开发者都会做后缀名检查。接着我尝试了shell.php内容是一句话木马系统直接拦截提示“禁止上传脚本文件”。看起来防御得还不错对吧但当我将同一个shell.php文件的内容复制到一个文本编辑器里然后在文件开头加上了GIF89a这几个字符再把文件另存为shell.gif上传时——系统竟然显示“上传成功”。那一刻我就知道这个看似坚固的堡垒墙上已经出现了一道裂缝。这个场景就是典型的“文件上传漏洞”攻防战的一个缩影。在PHP的世界里文件上传功能几乎是每个Web应用的标配从用户头像、文档附件到产品图片无处不在。然而也正是这个看似简单的功能如果开发者在实现时考虑不周就会成为攻击者直通服务器核心的“高速公路”。攻击者上传一个恶意的脚本文件如PHP、JSP然后通过Web请求这个文件服务器就会执行其中的代码从而可能导致服务器被完全控制、数据泄露等严重后果。今天我们就以PHP语言为例深入拆解攻击者常用的那些绕过文件上传限制的“花招”并站在防御者的角度看看如何构建真正有效的防线。无论你是刚入门的安全爱好者还是负责线上业务的开发者理解这些绕过方式背后的原理都至关重要。2. 文件上传漏洞的核心服务器如何“认”文件在深入绕过技巧之前我们必须先搞清楚服务器这里特指PHP环境到底是如何判断一个上传文件“是什么”以及“该怎么办”的。这决定了我们所有绕过手段的靶心在哪里。2.1 客户端校验最脆弱的防线第一种也是最初级的校验发生在用户的浏览器里即客户端校验。这通常是通过JavaScript来完成的。常见形态function checkFile() { var file document.getElementById(upload).files[0]; var fileName file.name; var ext fileName.substring(fileName.lastIndexOf(.) 1).toLowerCase(); var allowedExt [jpg, png, gif]; if (allowedExt.indexOf(ext) -1) { alert(只允许上传jpg, png, gif格式的文件); return false; } return true; }为什么它脆弱这种校验完全依赖于客户端浏览器的执行。攻击者可以轻易地绕过禁用浏览器JavaScript这是最简单粗暴的方法。拦截并修改HTTP请求使用Burp Suite、Fiddler等代理工具在HTTP请求包发送到服务器之前直接修改文件名或文件内容。直接构造请求通过Python的Requests库、cURL命令等完全模拟一个上传请求其中可以包含任何客户端脚本无法约束的内容。实操心得在安全测试中只要看到页面上有文件类型提示但刷新后消失或者提交瞬间就弹出错误没有网络请求发出基本可以断定存在客户端校验。这时候直接上代理工具抓包改包是第一步。防御视角客户端校验只能作为提升用户体验的一种方式比如快速提示用户选错了文件绝不能作为安全依赖。任何安全校验都必须放在服务器端进行。2.2 服务端校验攻防的主战场服务端校验是真正的安全关卡主要围绕以下几个维度展开而我们的绕过手段也针对这些维度后缀名扩展名校验检查文件名最后一个点.之后的部分。这是最普遍但也最容易被绕过的校验。MIME类型校验检查HTTP请求头中的Content-Type字段例如image/jpeg、text/plain、application/octet-stream。文件内容头校验检查文件内容的开头几个字节文件头/魔术字节判断其实际格式。例如GIF89a是GIF图片的文件头%PDF是PDF的文件头。文件内容完整性/二次渲染校验对上传的图片进行缩放、裁剪或水印等处理二次渲染。如果上传的是嵌入了恶意代码的图片经过渲染后代码很可能被破坏而失效。自定义内容校验开发者编写特定逻辑例如检查文件中是否包含?php等PHP标签。PHP通过全局数组$_FILES来接收上传的文件其中包含了name客户端文件名、type由浏览器提供的MIME类型、tmp_name服务器上的临时路径等关键信息。重要提示$_FILES[‘file’][‘type’]是不可信的因为它来自客户端HTTP请求头可以被篡改。安全的做法是使用PHP的finfo_file()函数或mime_content_type()函数基于tmp_name指向的临时文件的实际内容来判断MIME类型。3. 针对后缀名校验的绕过手段这是最古老也最经典的战场。假设服务器只允许上传.jpg,.png,.gif文件。3.1 大小写绕过服务器校验逻辑如果是in_array($ext, [jpg, png, gif])那么攻击者可以上传shell.PHP、shell.Php等。在Windows服务器上文件系统通常不区分大小写shell.PHP会被当作shell.php执行。在Linux/Unix系统上虽然系统区分大小写但若Web服务器如Apache配置不当也可能通过AddType指令将.PHP解析为PHP程序。防御方案在校验时将后缀名统一转换为小写再进行比较in_array(strtolower($ext), $allowed_ext)。3.2 名单绕过利用“黑名单”缺陷如果服务器采用“黑名单”策略即禁止上传.php,.asp,.jsp等那么名单之外的后缀就可能被利用。非常见可执行后缀.php5,.php7,.phtml 这些可能在某些PHP配置下被解析。查看Apache的httpd.conf或.htaccess中是否有AddType application/x-httpd-php .php5 .phtml这样的配置。.phps 通常用于显示PHP源码但配置错误时可能被执行。服务器解析漏洞IIS 6.0目录解析漏洞上传shell.php;.jpg IIS 6.0会将其解析为shell.php。虽然现在较少见但在老旧系统中仍需警惕。IIS 6.0分号漏洞上传shell.php;.jpg同样可能被解析。Apache解析漏洞老版本Apache在遇到多后缀文件时可能会从右向左识别直到遇到可识别的后缀。例如如果.php是可执行的那么shell.php.xxx可能被解析为PHP文件。更常见的是配置问题如果Apache的httpd.conf里有AddHandler php5-script .php那么任何包含.php的文件都可能被解析例如shell.php.jpg。实际上这通常需要配合其他漏洞如文件包含才能利用。.htaccess文件攻击如果服务器允许上传.htaccess文件且目标目录有AllowOverride All或AllowOverride FileInfo等配置攻击者可以上传一个自定义的.htaccess文件内容为AddType application/x-httpd-php .jpg。这样该目录下所有的.jpg文件都会被当作PHP脚本来解析。这是非常严重的一种绕过方式。防御方案绝对不要使用黑名单必须采用白名单机制。只允许明确、安全的有限几种后缀如[jpg, jpeg, png, gif]。同时确保服务器配置安全避免解析漏洞。3.3 双写、点号、空格与路径截断这些手法主要针对不严谨的过滤函数。双写绕过如果过滤函数是str_replace(‘.php’, ‘’, $filename)那么shell.p.phphp经过替换后中间的’.php’被删掉变成了shell.php。点号绕过shell.php.或shell.php...。在Windows系统中文件系统会自动去除末尾的点号最终保存为shell.php。如果服务器代码没有处理末尾点号就可能被绕过。空格绕过shell.php末尾有一个空格。在某些情况下尤其是代码使用$_FILES[‘file’][‘name’]直接拼接保存路径时这个空格可能导致校验和保存时的文件名不一致。路径截断已基本失效在PHP版本 5.3.4且magic_quotes_gpc Off时可以利用%00空字符进行截断。例如保存路径为$target_dir . $_FILES[‘file’][‘name’]攻击者上传文件名设为shell.php%00.jpg经过某些处理后%00后的.jpg会被截断最终服务器保存并执行shell.php。高版本PHP已修复此问题但作为历史知识需要了解。防御方案对文件名进行严格的过滤和规范化。使用basename()函数去除路径信息。使用trim()函数去除首尾空格和点号。避免将用户输入文件名直接拼接进文件路径。应采用“随机生成文件名如MD5值 固定白名单后缀”的方式。例如$upload_dir ‘uploads/’; $file_extension strtolower(pathinfo($original_name, PATHINFO_EXTENSION)); $allowed_ext [‘jpg’, ‘png’, ‘gif’]; if (!in_array($file_extension, $allowed_ext)) { die(‘文件类型不允许。’); } // 生成随机文件名保留安全的后缀 $new_filename md5(uniqid() . mt_rand()) . ‘.’ . $file_extension; $target_path $upload_dir . $new_filename;4. 针对MIME类型与文件头的绕过手段4.1 修改MIME类型如前所述直接校验$_FILES[‘file’][‘type’]是无效的。攻击者用Burp Suite抓包将Content-Type: application/x-php改为Content-Type: image/jpeg即可轻松绕过。防御方案永远不要信任 $_FILES[‘file’][‘type’]。必须使用服务器端函数检测真实MIME类型$finfo finfo_open(FILEINFO_MIME_TYPE); $real_mime finfo_file($finfo, $_FILES[‘file’][‘tmp_name’]); finfo_close($finfo); $allowed_mime [‘image/jpeg’, ‘image/png’, ‘image/gif’]; if (!in_array($real_mime, $allowed_mime)) { die(‘真实的文件类型不允许。’); }4.2 伪造文件头制作图片马这是对抗“文件内容头校验”的经典方法。即使服务器用finfo_file检查我们也可以让一个文本文件“看起来”像一张图片。操作方法准备一个纯文本的PHP Webshell例如?php eval($_POST[‘cmd’]);?。用十六进制编辑器如WinHex或Linux下的echo和cat命令在这个文本的最开头插入对应图片格式的魔术字节。GIFGIF89aJPEGFF D8 FF E0或FF D8 FF E1PNG89 50 4E 47 0D 0A 1A 0A(即‰PNG…的十六进制)将文件保存为.gif或.jpg后缀。例如在Linux下制作一个GIF图片马echo ‘GIF89a’ shell.gif echo ‘?php phpinfo(); ?’ shell.gif此时file shell.gif命令可能会显示 “GIF image data…”而finfo_file检测其MIME类型也是image/gif。但文件末尾的PHP代码依然存在。这种图片马上传成功后如何利用单独请求这个图片文件服务器通常会把它当作静态图片处理不会执行PHP代码。要利用它通常需要配合文件包含漏洞Local File Inclusion, LFI。假设网站有一个漏洞页面include.php?filexxx可以包含任意本地文件。那么攻击者可以上传图片马到uploads/shell.gif然后访问include.php?fileuploads/shell.gif。当这个文件被include()或require()函数包含时文件开头的图片头会被当作普通文本输出可能乱码但遇到?php ... ?标签时PHP解析器就会开始执行其中的代码。防御方案二次渲染这是对抗图片马最有效的方法。使用GD库或ImageMagick等图形处理库将上传的图片重新保存一次。function resizeImage($tmp_path, $target_path) { $image_info getimagesize($tmp_path); if ($image_info false) { return false; // 不是有效图片 } list($width, $height, $type) $image_info; switch ($type) { case IMAGETYPE_JPEG: $src_img imagecreatefromjpeg($tmp_path); break; case IMAGETYPE_PNG: $src_img imagecreatefrompng($tmp_path); break; case IMAGETYPE_GIF: $src_img imagecreatefromgif($tmp_path); break; default: return false; } // 创建一个新的真彩色图像 $dst_img imagecreatetruecolor($width, $height); // 复制并调整大小这里实际是1:1复制但过程会剥离元数据 imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $width, $height, $width, $height); // 保存为新文件 imagejpeg($dst_img, $target_path, 90); // 以JPEG格式保存质量90% imagedestroy($src_img); imagedestroy($dst_img); return true; }经过二次渲染图片文件中所有非像素数据包括我们附加的PHP代码都会被清除只保留纯粹的图像数据。即使后续被文件包含也不会包含可执行的代码。存储文件与Web目录分离不要将用户上传的文件保存在Web服务器根目录下。应该保存在一个无法通过URL直接访问的目录中。通过一个专门的“下载/查看”脚本如download.php?id123来读取文件并输出给用户。这个脚本在输出图片时使用正确的Content-Type头并且绝不执行文件内容。这样即使上传了恶意文件攻击者也无法直接通过URL触发执行。禁用危险函数在php.ini中将disable_functions设置为包含eval,assert,system,exec,shell_exec,passthru等危险函数可以限制Webshell即使被执行后的破坏能力。但这属于纵深防御不能替代对上传漏洞本身的修复。5. 条件竞争漏洞利用时间差的攻击这是一种逻辑漏洞而非简单的过滤绕过。它的原理是服务器在处理上传文件时通常会经历“保存临时文件 - 安全检查 - 移动到正式目录”的过程。如果安全检查如病毒扫描、内容分析耗时较长且在这期间临时文件或未通过检查的文件已经被保存在一个可访问的路径下攻击者就有机会在文件被删除前访问并执行它。典型漏洞代码// 1. 保存上传文件 $upload_file ‘/tmp/’ . $_FILES[‘file’][‘name’]; move_uploaded_file($_FILES[‘file’][‘tmp_name’], $upload_file); // 2. 进行耗时的安全检查例如调用外部杀毒引擎 $is_safe expensive_virus_scan($upload_file); // 假设这个函数需要2秒 // 3. 根据检查结果处理文件 if ($is_safe) { rename($upload_file, ‘/web/uploads/’ . basename($upload_file)); echo ‘上传成功’; } else { unlink($upload_file); // 删除恶意文件 echo ‘文件不安全已删除’; }攻击过程攻击者编写一个自动上传脚本持续快速地上传一个Webshell文件例如shell.php内容为?php file_put_contents(‘/web/uploads/backdoor.php’, ‘?php eval($_POST[c]);?’);?。由于服务器在expensive_virus_scan执行的2秒内文件已经存在于/tmp/shell.php假设/tmp目录Web可访问攻击者的另一个脚本就疯狂地访问http://target.com/tmp/shell.php。只要在文件被删除前的这2秒时间窗口内有一次访问成功那么shell.php就会被执行。它执行的任务可能是在一个安全的正式目录如/web/uploads/里写入一个更持久的后门文件backdoor.php。一旦backdoor.php写入成功即使原来的/tmp/shell.php被删除攻击者也已经获得了持久化的访问权限。防御方案先检查后保存理想流程是先将文件保存在一个不可执行的、随机命名的临时位置如/tmp/下但确保该目录没有.php的解析权限进行检查。检查通过后再将其移动到最终的可访问目录并且赋予最终文件一个随机的新名称。原子操作如果必须保存后再检查应确保文件在检查期间位于一个绝对无法通过Web访问的目录。检查通过后再将其移动rename到Web目录。在Linux上rename是原子操作可以避免竞争条件。使用不可预测的路径最终保存的文件名不要使用用户提供的原始名或简单的时间戳而应使用高强度随机数如random_bytes()生成增加攻击者猜测路径的难度。设置严格的目录权限上传目录应配置为仅能执行静态文件如图片、CSS、JS禁止解析脚本。对于Nginx可以配置location ~* ^/uploads/.*\.(php|php5|phtml)$ { deny all; }。对于Apache可以在上传目录放置.htaccess文件内容为php_flag engine off。6. 组合拳与高级技巧在实际的渗透测试中攻击者很少只使用一种方法而是会根据目标情况组合多种技巧。场景模拟一个网站允许上传头像进行了以下检查客户端JS校验后缀。服务端检查后缀白名单.jpg,.png,.gif。服务端使用finfo_file检查MIME类型。对图片进行了二次渲染。上传目录似乎无法直接执行.php文件。攻击链可能如下绕过客户端校验使用Burp Suite拦截上传请求。绕过后缀名校验尝试上传.phtml、.php7等或者利用解析漏洞构造shell.php.jpg如果Apache配置有问题。如果都不行则接受上传图片马shell.jpg。绕过MIME校验在Burp Suite中将Content-Type改为image/jpeg。绕过文件头校验确保文件内容以FF D8 FF E0JPEG头开头后面跟PHP代码。对抗二次渲染这是难点。如果二次渲染算法非常严格普通的图片马会失效。此时需要研究利用图像处理库本身的漏洞。例如某些GD库或ImageMagick版本存在漏洞可能被精心构造的图片文件触发实现代码执行CVE-2016-3714 ImageTragick。或者攻击者可能寻找不进行二次渲染的其他上传点例如“上传文档”、“上传压缩包”等功能。寻找文件包含点上传成功后需要利用LFI漏洞。通过信息收集如查看源码注释、错误信息、爬取网站链接寻找?file,?page,?include等参数。如果找到即可通过?fileuploads/shell.jpg来执行代码。利用.htaccess如果发现服务器是Apache且上传目录有执行.htaccess的权限攻击者可能会先尝试上传一个.htaccess文件内容为AddType application/x-httpd-php .jpg。如果成功再上传shell.jpg此时shell.jpg就会被解析为PHP。防御的黄金法则白名单白名单还是白名单无论是后缀名还是MIME类型只允许明确、最小化的集合。文件内容校验使用finfo_file()进行MIME检查。对图片进行二次渲染这是杀死图片马最有效的手段。重命名使用随机字符串如md5(uniqid() random_bytes(8))重命名上传的文件并保留白名单内的安全后缀。隔离存储将上传的文件存储在Web根目录之外通过安全的脚本进行读取和分发。设置权限确保上传目录没有执行脚本的权限。禁用危险函数在php.ini中配置disable_functions。日志与监控记录所有上传操作文件名、大小、IP、时间并对上传目录的文件变化进行监控便于事后审计和发现攻击行为。框架与库使用成熟的、经过安全审计的上传处理库如Symfony的HttpFoundation组件中的File组件而不是自己从头实现。文件上传漏洞的攻防是一场持续的战斗。攻击者的手法在不断进化从简单的后缀修改到复杂的逻辑漏洞利用。作为开发者理解这些绕过方式的原理不是为了去攻击而是为了在构建功能时能清晰地预见到潜在的风险点从而设计出更健壮、更安全的代码。安全是一个过程而非一个功能永远保持警惕和学习是守护数字世界的必修课。
返回列表