PHP-FPM 学习笔记

前言

遇到一个利用 PHP-FPM Bypass Open_basedir 的问题 , 过去我一直没注重这个问题 , 这里刚好整理学习下~

这一章先不谈利用方式 , 仅记录一下基础内容 .


Web Server 发展阶段

早期阶段

早期的 WebServer 负责处理全部请求 . 它接收到客户端请求 , 读取文件 , 再传输回客户端 . 按照如下的流程 .

此时 WebServer 仅能处理 html 等静态页面 , 但随着技术的发展 , 出现了 php , asp 等动态语言 , WebServer无法处理 .

显然不可能让 WebServer 增加功能来处理请求 , 现在动态语言这么多( python , php , jsp , asp , ... ) , 而且之后还会越来越多 . 肯定不能无限制的增加 WebServer 的功能

于是开发者这么想 : 要处理哪门语言 , 就将它传输给哪门语言的解释器 . 比如要处理 PHP 语言 , 就将处理交给 PHP 解释器去处理 .

那么 WebServer 如何与对应语言的解释器通信呢 ? 并且 WebServer 肯定不能直接将原始请求转发给对应语言的解释器 , 毕竟解释器可识别不了 HTTP 请求啊 ! 因此 , 为了解决不同语言的解释器与 WebServer 之间的通信问题 , 开发者研究出一类协议 , WebServer 会根据协议对 HTTP 请求进行重新封装 . 这个协议就是 CGI 协议 , 用于处理封装后的请求的程序叫做 CGI 程序


CGI 阶段

CGI( Common Gateway Interface , 通用网关接口 )的出现让 Web 从静态变为动态 .

当 WebServer 认为这是一个 CGI 请求时 , 会调用相关的 CGI 程序 , 并封装环境变量和标准输入等数据 , 然后传输给 CGI 程序 . CGI 程序处理完毕后会生成 HTML 页面 , 然后再通过标准输出将页面返回给 WebServer , WebServer 再将内容返回给客户端 . 整个流程如下

随着 Web 的发展 , 越来越多的网站都需要动态页面与用户交互 . CGI 的缺点慢慢显现 .

HTTP 每生成一个动态页面 , 系统就必须启动一个新的进程来运行 CGI 程序 . CGI 采用的是 fork and execution 方式 , 每次请求都需要建立新的 CGI 程序来进行处理 . 也就是说 , WebServer 每收到一个请求 , 就会去 fork 一个 CGI 进程 , 请求结束时再 kill 这个进程 . 如果有 10000 个请求 , WebServer 就需要 fork , kill CGI 进程 10000 次 .

不断的 fork 进程是一个极为消耗时间与资源的工作 , 这就导致了性能低下 , 更别提如今各种高并发场景了 . 于是就出现了 FastCGI 技术 .


FastCGI 阶段

FastCGI 由 CGI 改进而来 , 传统 CGI 接口的性能很差 , 且安全性不足 . 在处理现今各种高并发访问时是远远不够的 . 因此已经不再被使用 .

FastCGI( Fast Common Gateway Interface , 快速通用网关接口 ) 是一个可伸缩且高速的 HTTP 服务器和动态脚本语言间通信的接口 . 它的主要优点是将 HTTP 服务器和脚本解析服务器( 比如 PHP 解析器 )分开 , 同时以进程池的方式在脚本解析服务器上启动一个或者多个脚本解析守护子进程 , 并且通过一个进程管理器管理调度 . 多数流行的 HTTP 服务器都支持 FastCGI 模式 .

当 WebServer 遇到动态页面时 , 可以直接将其交付给 FastCGI 进程管理器来执行 , FastCGI 进程管理器调度进程池的子进程去处理执行 . 然后将得到的处理总结果返回给客户端 . 这种方式可以让 WebServer 专一的处理静态请求和将脚本解析服务器解析的结果返回 . 这在很大程度上提高了整个应用系统的性能 . 其执行流程如下所示

FastCGI 模式与传统 CGI 模式的区别之一是 Web 服务器不是直接执行 CGI 程序了 , 而是通过 socket 与 FastCGI 进程管理器进行交互 . WebServer 需要将 CGI 接口数据封装在遵循 FastCGI 协议包中发送给 FastCGI 进程管理器 . 正是由于 FastCGI 进程管理器是基于 socket 通信的,所以也是分布式的,Web服务器和CGI 响应器服务器分开部署 .


PHP-FPM

那么到底什么是 PHP-FPM 呢? 上面提到 , FastCGI 为了提高 CGI 程序的性能 , 会采用 进程池 的方式解决每次 fork 新的 CGI 进程的开销 . 并通过 FastCGI 进程管理器统一管理调度 CGI 子进程

以 PHP 语言为例 , FastCGI 进程管理器就被称为 PHP-FPM ( PHP FastCGI Process Manager ) , 它是 PHP 语言下 FastCGI 的实现 , 提供了进程管理的功能 . Apache / Nginx 等服务器中间件将用户请求按照 FastCGI 的规则打包好发送给 PHP-FPM,再由 PHP-FPM 来将打包好的数据分配给进程池中的子进程处理解析并返回结果 .

PHP-FPM 进程池中管理的子进程包含一个 Master 进程和多个 Worker 进程 . 其中 Master 进程负责监听端口 , 以便接收 WebServer 的连接请求 . 每个 Worker 进程内部都嵌入了一个 PHP 解释器 , 这是 PHP 代码真正执行的地方 .


PHP 安装模式

PHP 实际上有三种安装模式 , 分别如下所示

  1. CGI 模式
  2. FastCGI 模式
  3. Module 模式

网络上很多帖子把 PHP-FPM 也算作一种安装模式 , 但我认为 PHP-FPM 仅仅是 FastCGI 进程管理器 , 并不算是一种安装模式( 这里的安装模式是指 WebServer 与 PHP解释器 的通信方式 )

事实上 , 现在主流的安装模式仅有 " FastCGI 模式 " 与 " Module 模式 " .


Apache + Module 模式

这个 Module 模式是什么 ? 如果你使用的是 Apache 服务器 , 那你就会非常熟悉 . 你应该是按照下面的顺序安装 PHP 的 .

事实上 , 所谓 Module 模式 , 就是把 PHP 作为 apache 的一个子模块来运行,使用 LoadModule 来加载 PHP 模块 . 当 WebServer 访问 PHP 文件时,Apache 会调用 PHP 模块来解析,PHP 模块通过 SAPI ( Server Application Programming Interface , 服务器应用程序编程接口 ) 来把数据传递给 PHP 解析器进行解析 .

一般情况下 , Apache 服务器是通过 Module 安装 PHP 的 . 可以在 phpinfo() 中根据 Server API 参数来判断 .


Nginx + FastCGI 模式

而在 Nginx 服务器中 , 往往是通过 FastCGI 模式安装 PHP 的 . 具体的安装步骤应该如下

这里的安装流程参考了这里

然后还需要修改配置文件 , 配置 PHP-FPM 和 Nginx 的通信 . 可以有两种通信方式 , 分别为 " TCP 模式 "" Unix Domain Sockets 模式 "

  • TCP 模式

    TCP 通信模式是 PHP-FPM 进程监听本机端口( 默认为 9000 ) , Nginx将用户请求按照 FastCGI 规则打包好发送给 PHP-FPM , 再由 PHP-FPM 调用进程池中的子进程进行解析 .

    TCP 通信模式允许通过远程网络进程之间的通信 , 也可以通过 LoopBack 接口进行本地进程之间的通信

  • Unix Domain Sockets 模式

    Unix Domain Sockets 通信模式又被称为 IPC( Inter-process communication , 进程间通信 ) , 用于实现同一主机上的进程间通信 .

    该模式以文件( 一般是以 .sock 为后缀 ) 作为 socket 的唯一标识符 , 需要通信的两个进程引用同一个 socket 文件标识符就可以建立通道进行通信了 .

我这里选择配置 TCP 模式下的 PHP-FPM . 仅需要做如下的配置即可 .

重启 PHP-FPM 和 Nginx 服务 , 可以在 phpinfo() 的 Server API 参数得知我们以 FastCGI 模式安装了 PHP

注 : 如果目标服务器是以 CGI 模式安装 PHP 的 , 那么 Server API 的值为 " CGI/FastCGI "


FastCGI 协议剖析

之前提到 WebServer 与 PHP-FPM 通过 FastCGI 协议进行交互 . 因此在研究 PHP-FPM 安全问题中 FastCGI 协议报文结构就显得格外重要 . 这需要记录一下

如果你知道 HTTP 协议 , 那么学习 FastCGI 协议就不会很困难了 . FastCGI 是 WebServer 和某个语言后端进行数据交换的协议 , 它由多个记录( Record )组成 . 与 HTTP 协议类似 , Record 也有 Header 和 Body 一说 , WebServer 将这两者按照 FastCGI 的规则封装好后发送给语言后端 , 语言后端解码后拿到具体数据 , 进行指定操作 , 再将结果按照该协议封装好后返回给 WebServer .

Record 剖析

其组成结构如下

 与 HTTP 协议不同 , Record Header 固定为 8 个字节( HTTP Header 长度可变 ) ,  Record Header 由 8 个 unsigned char 类型的变量组成 , 每个变量 1 个字节

 version : 显示版本信息 , 如果是 WebServer 发送给 PHP-FPM 的消息 , 请求头中仅需要将其置 " 0 " 即可

 Type : 每次发送请求的类型 , 这个比较复杂后面再说

 RequestId : 占两个字节 , 这是一个唯一的标识Id , 以避免 PHP-FPM 同时处理多个请求时的影响

 ContentLength : 占两个字节 , 用来表示此 Record Body 中数据的长度 , PHP-FPM 拿到该字段后 , 会从 TCP 数据流中读取相同大小的数据 , 作为 Record Body . 

 PaddingLength : 额外填充长度 , 为了提高处理请求的能力 , 每个请求的大小都必须为 8 的倍数 . 该长度表示在请求尾部填充空白的长度 .  

 Reserved : 保留字段

 contentData : Record Body 数据 , 其支持的最大 Body 大小为 65536 字节 . 

 paddingData : 额外填充的空白数据

Record Type

Record Header 中第二个字节( 也就是 Type 选项 ) 是非常重要的 , 这里拿出来单独说明

Type 用于指定该 Record 的作用 , 一个 Record 的大小是有限的 , 作用是单一的 , 因此 WebServer 往往会在一个 TCP 流中传输多个 Record . 通过 Type 标志数据流中每个 Record 的作用 , 通过 RequestId 判断多个 Record 是否属于同一请求 .

Record Type 的种类非常多 , 在这里可以看到常用的几种 Type 值

从中可以明确看出 : WebServer 和后端语言通信时,发送的第一个数据包是 Type 为 1 的Record , 然后两者建立连接并互相通信 , 陆续发送 Type 为 4 , 5 , 6 , 7 的 Record,结束通信时发送 Type 为 2 , 3 的 Record .

其中 , Type 为 4 的 Record 是最值得关注的 , 因为环境参数在 PHP-FPM 中有着至关重要的作用 . 至于有什么作用 , 就要牵扯到漏洞利用了 , 因此这里不再多说 .


总结

这一章总体的谈了谈 WebServer 的几大发展阶段 , 提到了什么是 PHP-FPM , 什么是 FastCGI , 它们之间又有什么样的关联 . 之后将会学习 PHP-FPM 存在的安全问题以及利用方式 .

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇