PHP 7.4.21 内置服务器(php -S)源码泄露漏洞

背景

当PHP<= 7.4.21时,通过php -S开起的内置WEB服务器存在源码泄露漏洞,可以将PHP文件作为静态文件直接输出源码
简单说,就是 PHP <= 7.4.21 版本里自带的那个玩具服务器(php -S),脑子不太好使,如果你用特殊的方法“忽悠”它一下,它就会把 PHP 代码文件 当成 普通的文本文件(静态文件),不执行代码,直接把源码吐给你。

基本概念

  • php -S 
    是 PHP 给开发者方便调试用的“临时工”服务器,通常用于开发和测试环境,命令一敲,服务就起,但是这个服务器作用有限,处理并发和复杂请求的能力很弱,根本没打算让你在生产环境(公网)用。

  • 静态文件
    指的是内容固定,每次请求返回的内容都相同,无需服务器进行额外处理的文件。这些文件预先存放在服务器上,当客户端发起请求时,服务器直接将文件内容返回给客户端。像 .txt, .jpg这种,服务器拿到了直接扔给浏览器的,就叫静态文件。而像.php 这种,服务器得先“煮熟了”(执行代码)再端给客人的,就是动态文件。

漏洞原理

这个漏洞的核心在于 PHP 内置服务器(CLI Server)处理 HTTP 流水线(Pipelining)请求时的“状态机混淆”

  • 单线程架构限制:PHP 内置服务器(php -S)通常是单进程单线程的。为了提高效率,它支持在一个 TCP 连接中处理多个连续的 HTTP 请求(即 Pipelining)。

  • 请求解析循环:当服务器通过 recv() 接收到数据包时,如果缓冲区里包含了多个 HTTP 请求,服务器会进入一个循环来依次处理它们。

  • 共享状态污染(Root Cause)
    在源码 sapi/cli/php_cli_server.c 中,服务器维护了一个 request_info 结构体来保存当前请求的信息(包括路径、扩展名、是否为静态文件等)。
    当第一个请求(index.php)被解析后,服务器准备处理它。但在处理完成之前,循环继续读取缓冲区,解析了第二个请求(aaa.txt)。
    关键 Bug:由于逻辑设计缺陷,第二个请求的解析过程更新了全局或共享的 “MIME 类型判断”“静态文件标志位” ,覆盖了第一个请求的状态。

    • 如果第二个请求是静态文件,标志位变为“直接输出”。
    • 服务器回头去完成第一个请求的处理时,错误地使用了被更新后的“直接输出”标志,导致本该执行的 PHP 代码被当做纯文本发送。

通俗解释:
想象 php -S 是个眼神不好的服务员。

  • 正常情况:你点个 index.php,他知道这是生食,要去厨房做熟了给你;你点个 1.txt,他知道这是零食,直接拿给你。
  • 漏洞触发:
    你语速极快,在一个TCP连接里一口气说了两句话(两个 HTTP 请求):
    “我要 index.php!顺便再给我拿个 age.txt(随便一个不存在的静态文件)!”
  • 服务员懵逼了:
    因为代码里的 Bug(状态机竞争),他在处理完或者正在处理第一个请求时,逻辑乱了。他看着手里的 index.php,脑子里却想着你要的那个“静态文件”,结果手一抖,忘了把 index.php 送去厨房执行,直接当成生鲜(源码)端给了你。

检测方法:

看 HTTP 响应头(Response Header)

  • php版本为<= 7.4.21的版本
  • 使用的是 php -S 启动的内置 Web 服务器,而不是成熟的 Web 服务器(如 Apache、Nginx)
HTTP/1.1 200 OK
Host: 127.0.0.1:8000
Date: Sun, 25 Jan 2026 12:00:00 GMT
Connection: close
X-Powered-By: PHP/7.4.21      
			<-- 重点1:版本号 <= 7.4.21
Content-type: text/html; charset=UTF-8
Server: PHP 7.4.21 Development Server  
			<-- 重点2:明晃晃写着 Development Server而不是nginx/1.18.0,又或者没有Server

利用方法:

构造异常请求

GET /index.php HTTP/1.1  //这里的index.php是要读取源码的文件
Host: 106.52.241.166:8004


GET /age.txt HTTP/1.1     //这里要被检测为静态文件处理


//这里最后一定要多空几行
  • 注意事项
  1. 请求不能包含Content-Length头,用Brup Suite的话一定要关闭自动更新Content-Length
    如果 Burp 自动算了长度,它会把第二个 GET /age.18 当作第一个请求的“正文(Body)”发出去,而不是作为“第二个请求”。我们要的是让服务器觉得这是两个独立的请求连在一起。Pasted image 20260125153827

  2. 两个 GET 中间以及第二个请求的最后要多空几行
    因为服务器就是靠双换行符(\r\n\r\n)才知道第一个请求在哪结束,第二个请求在哪开始,如果没有空行,服务器会认为 GET /age.txt ... 这一串字符是第一个请求里的某个 Header 值,或者是一堆乱码 Body,根本不会把它识别为“第二个请求”

  3. 第二个请求文件的后缀,决定服务器以怎样的文件类型,返回第一个请求,请求的文件
    php -S 启动的内置 Web 服务器会通过检查文件扩展名来决定将请求的文件视为静态文件还是作为 PHP 文件执行。若扩展名不是 .php 或 .PHP,或者扩展名长度不等于 3,文件就会被当作静态文件处理

例题:

[2025学警杯]search
题目表明说flag在index.php里,说明就是要得到index.php的源码,又提示说这个php版本有点危险,尝试利用php版本造成的源码泄露漏洞,成功得到flagPasted image 20250413221425

[LilacCTF 2026]keep
打开网页,只有一句hello world,扫目录也没有发现其他文件,查看响应头发现php版本为7.3.4,并且没有Server头!!!Pasted image 20260125160057

猜测应该是php -S源码泄露漏洞,构造Playload请求,成功得到源码Pasted image 20260125160404

访问s3Cr37_f1L3.php.bak,发现是一句话木马,但尝试访问s3Cr37_f1L3.php没成功,猜测是要利用php -S源码泄露漏洞执行s3Cr37_f1L3.php.bak,成功执行Pasted image 20260125160704

不过因为关了自动更新Content-Length,所以改了admin参数需要手动更新Content-Length,成功得到flagPasted image 20260125161113

posted @ 2026-03-11 15:15  一只咸鱼灰  阅读(47)  评论(0)    收藏  举报