尧图精选

Nginx解析漏洞深度剖析:从cgi.fix_pathinfo到CVE-2013-4547的修复实战

🕒 发布时间:2026/10/1 9:15:30 📁 来源:尧图网络
前阵子一个老朋友发来一张截图是他们安全系统报出的中危漏洞名称写得很直白Nginx解析漏洞nginx_parsing_vulnerability旁边附了一个URLhttps://xxx/upload/avatar.jpg/1.php。他很困惑说公司Nginx版本不算旧也专门做过加固怎么还会被报这种老漏洞。我看完截图回了一句你先打开php.ini看一眼cgi.fix_pathinfo再打开php-fpm的pool配置看一眼security.limit_extensions。五分钟后他回我还真是这两个配置没改。这个场景我遇到太多次了。今天干脆把这类“Nginx解析漏洞”从头到尾讲透——它到底是什么原理、怎么复现、怎么排查、怎么修以及扫描报告里一堆编号该怎么看。文章里的操作都基于我实际做过的排查和测试相关命令和配置可以直接抄但请务必在授权环境或靶机里验证。1. 一次让Nginx“背锅”的漏洞排查经历1.1 客户截图里那条诡异的上传路径那次排查的对象是一个带用户头像上传的PHP站点。攻击者上传的是一张正常的JPG图片图片内容里被塞了一段简单的PHP探针代码。正常情况下访问/upload/avatar.jpg只会看到图片服务端不会对它做任何脚本解析。但是访问/upload/avatar.jpg/1.php的时候诡异的事情发生了浏览器返回了200而且响应体是PHP探针的输出页面。我当时的排查思路是这样的先看Nginx配置里关于avatar.jpg/1.php的处理逻辑发现站点根目录下有一个典型的PHP locationlocation ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; }按这个配置/upload/avatar.jpg/1.php请求进来时Nginx会把它匹配到PHP location然后把SCRIPT_FILENAME拼成/usr/share/nginx/html/upload/avatar.jpg/1.php交给后端的PHP-FPM处理。到这里还没问题——这个文件路径并不存在。问题出在PHP-FPM收到一个“不存在的PHP文件路径”之后的行为。如果cgi.fix_pathinfo是默认的1PHP-FPM会尝试把路径逐级向前回退最终找到/usr/share/nginx/html/upload/avatar.jpg发现这个文件存在于是直接把其中的PHP代码从头到尾执行了一遍。也就是说真正把JPG“变成”PHP并执行的始终是PHP-FPMNginx只是在中间做了一次看起来非常正常的转发。所以那次对话我给客户的结论是这不是Nginx产品本身的漏洞也不是某个0day而是一个由配置组合出来的经典解析缺陷。只要PHP-FPM允许“找不到脚本文件就往前找”并且允许执行非.php后缀的文件那你上传目录里任何包含PHP代码的图片都能通过追加/任意名字.php的方式被执行。1.2 网传的三类“Nginx解析漏洞”其实是三件事网上搜“Nginx解析漏洞”能看到大量互相抄来抄去的文章但仔细对照Payload和影响版本就会发现所谓的“Nginx解析漏洞”至少被混进了三种不同成因的问题。漏洞形态典型URL根源主要角色现状路径回退解析/uploads/1.jpg/1.phpcgi.fix_pathinfo回退机制PHP-FPM配置缺陷老站仍然大量存在空字节截断/uploads/1.jpg%00.phpPHP 5.3.4以下空字节截断老PHP版本缺陷现代环境基本绝迹URI规范化绕过/uploads/1.jpg .php、1.jpg%00.phpCVE-2013-4547Nginx 0.8.41-1.4.3及1.5.0-1.5.7新版本已修复旧版本仍可触发这三个问题触发条件各不相同修复方案也完全不同。把三件事混在一起处理很容易出现“改了一处、漏了两处”的情况。比如很多运维在知道cgi.fix_pathinfo1危险之后只把它改成0结果站点用了PATH_INFO风格的路由全部404还有人在location里加了if ($request_uri ~* \.jpg/)这类黑名单规则没过多久就被一种新的编码姿势绕过。要真正理解怎么修得先把底层机制看清楚。2. 漏洞根因透视Nginx和PHP-FPM为什么会“各说各话”2.1 理解一个Web请求从Nginx到PHP的完整链路在深入之前先建立一个最基本的请求链路认知。用户访问PHP站点时Nginx收到HTTP请求根据location规则决定用什么方式处理如果是PHP请求就通过fastcgi_pass把请求交给后端PHP-FPM进程。这个过程中Nginx真正传递给PHP-FPM的核心参数是SCRIPT_FILENAME——它告诉PHP-FPM“你要执行的文件在这个路径”。location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; }很多刚接触Nginx的开发者以为写了location ~ \.php$就万事大吉认为Nginx只会把.php结尾的请求交给PHP-FPM。但这句话只限定了一件事Nginx在什么情况下转发。真正决定“这个文件能不能被当作PHP脚本执行”的是PHP-FPM收到SCRIPT_FILENAME之后的文件查找和扩展名判断逻辑。Nginx和PHP-FPM是两个独立的处理器各自按自己的规则理解同一个请求这就是各种解析不一致类安全问题的根源。我常用的一个生活化类比Nginx是快递前台PHP-FPM是仓库打包员。前台只负责看快递单上有没有“PHP”标签有就扔给仓库仓库却会根据自己的一套地址修正逻辑去找货。如果地址写得花哨一点比如“图片文件/不存在的文件.php”前台认为这是PHP件仓库却修正地址后找到了旁边的图片文件还把图片当作“货物”直接发出去问题就出现了。2.2 fix_pathinfo的“自作聪明”是最大变量cgi.fix_pathinfo是PHP-CGI/PHP-FPM时代遗留下来的配置项本意是支持PATH_INFO风格的路由。什么是PATH_INFO就是访问/index.php/Home/Index这类URL时/index.php是真实脚本后面的/Home/Index作为路由参数传给框架。PHP需要把“脚本路径”和“额外路径信息”拆开。当cgi.fix_pathinfo1时PHP-FPM的查找逻辑大致如下拿到Nginx传来的SCRIPT_FILENAME比如/var/www/html/upload/avatar.jpg/1.php检查这个文件是否存在不存在把路径最后一段去掉变成/var/www/html/upload/avatar.jpg再检查发现这个文件存在就把它当作脚本执行。这个回退机制本身是给PATH_INFO路由设计的但任何一个机制一旦默认开启就很容易被业务逻辑意外使用。最常见的危险组合是cgi.fix_pathinfo1 PHP-FPM的security.limit_extensions未限制 上传目录可写且可直接通过URL访问。三个条件凑齐图片马就被激活了。另外要注意一个细节很多人以为只要把security.limit_extensions设置成空PHP-FPM才能执行JPG其实很多旧版PHP-FPM默认配置里根本没有显式声明这个值同样会执行非.php文件。所以排查时不要只看配置里有没有这行字要看实际生效值。2.3 CVE-2013-4547URI规范化差异导致的经典绕过如果说fix_pathinfo问题主要是PHP-FPM的锅那CVE-2013-4547就真的是Nginx自身解析逻辑的问题了。这个漏洞影响Nginx 0.8.41至1.4.3以及1.5.0至1.5.7版本修复版本是1.4.4和1.5.8。它的核心成因是Nginx对不同阶段的URI规范化不一致。Nginx在location匹配阶段对URI中的特殊字符处理和后续传给FastCGI时处理不一样导致同一个URL在Nginx眼里可能不是PHP文件但到了PHP-FPM眼里却被解析成脚本。经典Payload类似/uploads/x.jpg%00.php或者/uploads/x.jpg%20.php利用空字节或空格造成两端解析结果不同。这里有个非常重要的认知不要把CVE-2013-4547的Payload当成万能钥匙。它的成功条件非常苛刻不仅要求Nginx命中受影响版本还依赖PHP版本、文件系统以及Nginx的具体配置。很多公开文章直接贴一个URL让读者去“验证”结果大部分情况下就是404。我在实际测试中遇到过不少次扫描器报告这个CVE手工复现却始终不成功原因就是某些配套条件没凑齐。所以修复这类问题不能只盯着某一个CVE的Payload做拦截而是要从两端解析逻辑的一致性上彻底封堵。3. 授权环境下的一次完整复现3.1 实验环境与前置准备复现前先声明以下操作请一定在本地虚拟机或授权的攻防靶场中进行不要拿生产环境试。我复现时用的是最经典的一组老版本组合组件版本Nginx1.4.2或1.0.15PHP5.3.10 php-fpm系统CentOS 6.x虚拟机站点目录/usr/share/nginx/html上传目录/usr/share/nginx/html/uploads准备步骤编译或安装上述版本的Nginx和PHP保持最简化配置让cgi.fix_pathinfo1php-fpm的security.limit_extensions不设置或者设为空在uploads目录放一张图片马shell.jpg内容包含PHP探针代码。制作图片马的命令很简单cp normal.jpg shell.jpg echo ?php phpinfo(); ? shell.jpg file shell.jpgfile命令确认它仍然是JPEG image data文件头正常图片可以在浏览器里正常显示。PHP代码藏在文件末尾正常情况下不会影响图片展示。3.2 复现路径回退型解析环境就绪后执行curl -i http://127.0.0.1/uploads/shell.jpg/1.php注意这里1.php本身并不存在但响应会是HTTP/1.1 200 OK页面内容是phpinfo()的输出。这就是fix_pathinfo回退的效果PHP-FPM拿着/uploads/shell.jpg/1.php找不到文件逐级回退找到/uploads/shell.jpg直接执行。顺手可以看一下Nginx访问日志GET /uploads/shell.jpg/1.php HTTP/1.1 200日志里记录的是完整路径看起来只是访问了一个普通的.php文件没有任何异常特征。这也是这类问题隐蔽的原因之一——日志和WAF看到的都是“正常”的PHP请求很难从访问日志层面识别。复现成功的关键之一是1.php必须是不存在的文件。如果上传目录里真的有一个1.php文件PHP-FPM就不会回退而是直接执行这个文件那就不是解析漏洞而是文件上传漏洞了。这也是为什么修复方案里try_files $uri 404很有效——它在Nginx层直接拦掉那些“文件不存在”的请求让PHP-FPM根本没有机会做回退。3.3 空字节与CVE-2013-4547的触发真相和局限再试一个大家常问的Payloadcurl --path-as-is -i http://127.0.0.1/uploads/shell.jpg%00.php在PHP 5.3.4以下的系统里这个请求曾经能触发空字节截断让PHP把shell.jpg%00.php当作shell.jpg执行。但在我这套环境PHP 5.3.10以及后续的新版本上它基本都会返回404或抛出一个“Access denied”的提示。空字节截断在PHP端早已被修复这项技术在今天更多只有考古价值。CVE-2013-4547的情况类似。它的利用依赖Nginx版本和FastCGI配置同时踩中特定条件在已经升级或者使用了不同location书写方式的环境里复现成功率很低。我在测试中得到的经验是不用执着于把每一种历史Payload都打成功理解它为什么不成功反而更有价值——这说明版本升级和配置修正真的在起作用。3.4 复现中最容易踩的坑这部分内容一般文章不会写但都是我实测中撞过的墙。第一个坑是curl的URL规范化。curl默认会对URL做路径标准化比如把..折叠、把//合并、把某些特殊字符重新编码。复现带有空字节、空格和路径穿越的Payload时一定要加上--path-as-is参数否则你发送出去的请求可能和你命令行里写的不一样。curl --path-as-is -i http://127.0.0.1/uploads/shell.jpg/1.php第二个坑是URL里的空格。空格在URL里必须编码为%20但如果你在bash脚本里直接拼URL空格很容易被当成参数分隔符。我习惯用单引号把整个URL包起来再传避免shell展开。第三个坑是图片马的构造顺序。有些新手把PHP代码直接放在JPG文件头部解析时可能会因为JPG的二进制结构问题出现语法错误导致即使成功触发也看不到预期输出。稳妥的做法是保持原始图片文件完整只在文件尾部追加PHP代码然后确认追加后文件仍然能被正常识别为图片。第四个坑是只看状态码不看响应内容。有时候请求返回200但内容是空的容易被误判为复现失败。多观察响应体的长度和特征字符串比如phpinfo输出里的PHP Version才敢确认是否真的执行成功。4. 自查清单你的配置到底有没有暴露在风险里4.1 三个核心配置项三条命令就能查与其等扫描器报警不如先自己把服务器上的三个关键点查一遍。我每次排查这类问题时第一轮操作永远是这三条命令php -i | grep -i fix_pathinfo grep -R security.limit_extensions /etc/php-fpm.d/ /etc/php5/fpm/ 2/dev/null nginx -T | grep -A8 location.*\.php第一看cgi.fix_pathinfo是不是1第二看php-fpm的pool配置里有没有security.limit_extensions如果有值里有没有.php第三看Nginx的PHP location里有没有try_files $uri 404这行。下面这个表格可以直接让实习生拿去对照检查项危险值安全值说明cgi.fix_pathinfo10改成0影响PATH_INFO路由时需谨慎security.limit_extensions未设置或为空.php建议显式写.phpNginx php location中的try_files缺失try_files $uri 404;直接阻断不存在文件的转发上传目录写权限www/nginx可写只读或独立不可执行目录配合上传场景风险更高Nginx版本CVE-2013-45470.8.41-1.4.3 / 1.5.0-1.5.71.4.4 / 1.5.8升级即可修复PHP版本空字节截断5.3.4以下5.3.4现代版本基本免疫4.2 影响面判断有漏洞配置不等于可被利用自查出一个危险配置后不要急着扩散恐慌先评估业务影响面。解析漏洞要真正被利用必须同时满足两个条件一是服务端存在可被执行的“图片马”或包含PHP代码的文件二是这个文件放在可以直接通过URL访问的目录里。我一般按这个思路给客户分级纯静态站没有PHP-FPM解析漏洞天然不成立PHP站但没有上传功能解析漏洞很难找到可落地的文件载体风险相对低PHP站带头像/文件上传功能上传目录直接暴露在root下高危几乎必然可复现PHP站用了对象存储或独立静态域名上传文件在Nginx之外解析不到PHP风险低。所以同样是cgi.fix_pathinfo1两个站点的实际风险可能差着十万八千里。这也是为什么安全报告里同一个CVE编号在不同环境的“严重程度”完全不同。4.3 扫描器报“sf-0005-22843 / CVE-2025-1695”时该怎么解读很多人看到扫描报告里出现“sf-0005-22843”“CVE-2025-1695”这类编号会慌尤其名称里还带“F5 Nginx”以为被什么新型漏洞盯上了。我在几个客户的报告里都看到过这种组合。简单说下我的理解sf-0005-22843是部分扫描器知识库里的自定义编号用来映射“Nginx解析漏洞”这类配置风险而CVE-2025-1695是F5 Nginx产品线后来公开的一个漏洞编号和当年经典的cgi.fix_pathinfo解析方式不是一回事。扫描器把它们写在同一条告警里可能是因为它们的检测特征都指向“Nginx在某种路径下可能被绕过”但这个“绕过”的具体成因差得很远。我处理扫描报告的习惯是先看检测证据再看扫描器给出的URL然后在服务器上手动复现一次。如果扫描器是通过“存在某个配置文件特征”判定的那属于配置风险如果是通过“实际访问URL返回了PHP内容”判定的那属于可利用漏洞。不同结论对应完全不同的整改优先级。5. 修复实战从临时止血到架构级加固5.1 第一层在PHP-FPM里锁死解析边界最直接有效的修复是让PHP-FPM只允许执行.php后缀的文件。找到php-fpm的pool配置文件通常是/etc/php-fpm.d/www.conf也可能是/etc/php5/fpm/pool.d/www.conf确认或添加这一行security.limit_extensions .php设置完成后重启php-fpmsystemctl restart php-fpm这一招为什么最关键因为它把问题从“Nginx传什么、PHP就解析什么”变成了“不管Nginx传来什么PHP-FPM只认后缀为.php的文件”。即使fix_pathinfo仍然开着回退到shell.jpg时扩展名校验这一关也过不去解析直接失败。这是我在所有加固方案里优先级最高的一项。接着是修改cgi.fix_pathinfo。在php.ini中找到cgi.fix_pathinfo1改成0然后重启php-fpm。但如果站点依赖/index.php/模块/控制器这类PATH_INFO路由直接改成0会导致整站路由失效。这种情况我更建议用另一套组合方案保留fix_pathinfo1但牢牢靠security.limit_extensions .php和Nginx侧try_files双重兜底同时改造Nginx的传参方式让框架能从fastcgi_param PATH_INFO里拿到路由信息而不是依赖PHP-FPM的路径回退。5.2 第二层修正Nginx location配置的关键写法Nginx侧要做的第一件事是给PHP location加上try_files $uri 404。这一段配置可以直接替换掉原来“裸转发”的写法location ~ [^/]\.php(/|$) { try_files $uri 404; fastcgi_split_path_info ^(.?\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; }try_files $uri 404的作用是先看$uri对应的文件在磁盘上是否存在不存在直接返回404不再往下转发给PHP-FPM。这样/upload/avatar.jpg/1.php这种“文件不存在”的请求在Nginx阶段就被拦下fix_pathinfo根本没有机会参与。同时如果你能明确区分上传目录在Nginx层再补一刀禁止上传目录下任何PHP文件被执行location ~* ^/uploads/.*\.(php|php[0-9]|phtml)$ { deny all; }这里注意正则要覆盖常见的PHP扩展名变体.php、.php3、.php5、.phtml等。只拦.php的话某些解析环境里换个扩展名又能绕过。对于CVE-2013-4547最干净的修复还是升级Nginx到1.4.4以上或1.5.8以上版本。如果业务长期停留在老版本无法升级至少要把配置改为上面这种包含try_files的写法同时靠PHP-FPM的扩展名白名单兜底把可利用面压到最低。5.3 第三层从上传功能层面断了根配置修复属于“止血”架构改造才是“根治”。我做过几个长期项目总结出几条对上传场景最有效的架构级加固上传的文件统一重命名文件名由服务端生成随机字符串后缀白名单校验不接受用户提供的后缀图片类文件上传后用服务端图像库如Imagick、GD重新采样、重新压缩去掉图片中可能夹带的PHP代码。这比任何黑名单过滤都干净因为重绘之后原文件中的恶意内容会被破坏上传目录和站点目录分离或者直接把上传文件放到独立域名/对象存储/CDN上。静态文件不经过PHP-FPM解析链路从物理上隔绝了被当成脚本执行的可能在CI/CD流水线里加入配置基线扫描把cgi.fix_pathinfo1、security.limit_extensions缺失、缺少try_files等高风险配置作为发布阻断项对老旧的Linux发行版比如还在用PHP 5.2/5.3的存量系统优先规划版本升级升级前用容器方式隔离运行。这些改造看起来“重”但每一条都是在跟攻击者的私有化方式做切割。黑名单不断更新是永远追不上的能一刀切掉的部分干脆切掉。5.4 修复后的验证方法改完配置后不能光看“配置变了”就说修好了我习惯按下面这套请求做回归验证# 路径回退型预期404 curl --path-as-is -i http://127.0.0.1/uploads/shell.jpg/1.php # 空字节型预期404或403 curl --path-as-is -i http://127.0.0.1/uploads/shell.jpg%00.php # 直接访问PHP文件预期403如果ban了uploads目录的php curl -i http://127.0.0.1/uploads/test.php # 正常PHP页面预期200 curl -i http://127.0.0.1/index.php只要前三个请求分别返回404/403同时最后一个正常页面返回200就说明加固没有误伤业务。如果index.php或者其他PATH_INFO路由页面返回404优先检查是不是把cgi.fix_pathinfo改成0之后破坏了路由再按5.1节提到的方案调整。6. 踩坑复盘与防御思维延伸6.1 我在修复这类问题时的三个“反直觉”经验做过的类似排查越多越觉得有三个经验值得特别记下来。第一个反直觉经验是漏洞名里写着“Nginx”但根因常常在PHP-FPM。处置思路别被名字带着走先把请求链路拆开看请求到了哪一层、被哪一层当作脚本执行了。定位到“执行者”修复才有针对性。第二个反直觉经验是扫描器报出来的CVE编号未必代表你能复现。扫描器通常是用特征规则做判断看到某个参数值、某个版本号都可能报警和实际利用成功距离十万八千里。所以收到告警先做人工验证别急着给客户报“服务器被入侵了”。第三个反直觉经验是过滤Payload是最脆弱的修复方案。很多人喜欢在Nginx里加if ($request_uri ~* \.jpg/.\.php)这类规则今天拦住了这个姿势明天攻击者换一个编码方式、换一个中间件组合规则又废了。真正有效的修复永远是把“可以执行什么”这个边界在源头定死而不是黑名单式地追着攻击者跑。6.2 从Nginx解析漏洞看所有“解析不一致”类威胁这类“多组件解析不一致”的安全问题远不止Nginx一家存在。Tomcat曾经出现过的/xxx.jsp/路径绕过、IIS的asp;.jpg分号截断、各类中间件的PUT上传配合解析差异本质上都是同一个模型请求在进入不同处理组件时被规范化成了不同的样子于是安全决策和执行决策出现了错位。理解这一点之后防御思路会变得非常清晰安全决策应该尽可能靠后最好由最终执行脚本的组件自己做决定。对PHP就限制扩展名白名单对JSP就限制Servlet映射对任何Web应用都做一次“谁最终执行业务代码谁就必须校验文件身份”的检查。前端Nginx可以做防御但不能是唯一防线。我在评估这类风险时有一条很实在的检查习惯不管扫描报告写着什么编号登上服务器的第一件事永远是看三个点——PHP-FPM的扩展名白名单、fix_pathinfo的状态、Nginx的php location有没有try_files。这三条看完十个解析类告警至少能排除七成。如果只让我给后人留一句建议那就是让PHP-FPM只认.php后缀比追着任何一个CVE改配置都管用。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →