elastic / elastic/detection-rules

[Rule Tuning] Potential Linux Hack Tool Launched: script-distributed tools report as their interpreter

Closed
#6,637 1 comment 0 reactions 1 assignee Claimed by @eric-forte-elastic View on GitHub
community
Dominant language
Python
Stars
2.7k
Forks
696
Avg merge
4d 17h
Merged PRs (30d)
87

Description

### Link to Rule

https://github.com/elastic/detection-rules/blob/main/rules/linux/execution_potential_hack_tool_executed.toml

`1df1152b-610a-4f48-9d7a-504f6ee5d9da`, Potential Linux Hack Tool Launched

### Rule Tuning Type

Other

### Description

This rule matches 40 tool names against `process.name`. For the tools distributed as interpreted scripts, `process.name` is frequently the interpreter rather than the tool, so those entries cannot match. I found this while validating an unrelated rule and then hit the same mechanism a second time on a different one, which is why I think it is structural rather than a one-off.

**The measurement.**

I ran the real `sqlmap` from its GitHub source on a host with Elastic Agent 9.4.0 and Elastic Defend 9.4.1. `process.name` came through as `python3.10`. Never `sqlmap`.

That result depends on how the script is invoked, and the behaviour is not uniform, so I mapped it against `/proc//comm`, which is what becomes `process.name`:

| Invocation | resulting `comm` | rule matches |
|---|---|---|
| `./tool.sh` with `#!/bin/bash` | `tool.sh` | yes |
| `./tool.py` with `#!/usr/bin/python3` | `tool.py` | yes |
| `./tool.py` with `#!/usr/bin/env python3` | `python3` | **no** |
| `python3 tool.py` | `python3` | **no** |
| `bash tool.sh` | `bash` | **no** |

The `env` form is the one that matters, because it is what modern tooling ships with. It resets `comm` to the interpreter, since the kernel execs `env`, which then execs the interpreter.

**Which entries in the list this affects.**

I pulled the first line of each tool's entry point from its own repository:

| Tool in the rule's list | Shebang | Effect |
|---|---|---|
| `sqlmap` | `#!/usr/bin/env python` | reports as the interpreter |
| `crackmapexec` | `#!/usr/bin/env python3` | reports as the interpreter |
| `commix` | `#!/usr/bin/env python` | reports as the interpreter |
| `wpscan` | `#!/usr/bin/env ruby` | reports as the interpreter |
| `gobuster` | Go binary | unaffected |

Four of the five script-based tools I checked use the `env` form. I did not check all 40, so I would not claim a precise count, but the pattern is consistent and these four are among the more commonly used entries.

Separately, five entries carry a script extension in the name itself:

```
linenum.sh
linpeas.sh
linux-exploit-suggester.sh
panix.sh
linux-exploit-suggester-2.pl
```

Those only match when the script is executed directly and its shebang names the interpreter by absolute path. Run as `bash linpeas.sh` or `sh linpeas.sh`, which is common in write-ups and in tooling that pipes a script in, `comm` is `bash` or `sh` and the entry cannot match.

**Why I think this is worth raising rather than working around.**

The evasion is free. An operator who runs `python3 sqlmap.py` instead of `./sqlmap.py`, or `bash linpeas.sh` instead of `./linpeas.sh`, defeats those entries without renaming anything, without recompiling, and probably without knowing the rule exists. Nothing in the rule's own documentation signals that the coverage is invocation-dependent.

**A second, independent instance of the same mechanism.**

While validating a rule of my own on Composer, I hit this again. Composer is a PHP script with a `#!/usr/bin/php` shebang, and the sensor records:

```json
{
"name": "php8.1",
"executable": "/usr/bin/php8.1",
"args": ["/usr/bin/php", "/usr/bin/composer", "install", "--no-interaction"],
"command_line": "/usr/bin/php /usr/bin/composer install --no-interaction"
}
```

My rule matched `process.name == "composer"` and returned zero on a host that had just completed a real install. I fixed mine by matching the interpreter name and requiring the tool path in `process.args`. That approach works, but it is not free: matching tool names in `process.args` reintroduces substring collisions that need real false positive measurement per name, and short entries in this rule's list such as `john`, `dirb` and `hydra` would need care.

**What I am not doing.**

I am not proposing a patch. A fix here is a design decision about how much `process.args` matching this rule should take on and what the resulting noise looks like, and that is yours to make rather than something to bolt on from outside. I would rather surface the gap with the evidence than submit a tuning that trades a false negative for an unmeasured false positive.

If it is useful, I am happy to measure the false positive surface for `process.args` matching on the specific names in this list and report back.

### Example Data

Real `sqlmap` execution captured by Elastic Defend on Linux, trimmed to the relevant fields. The tool is `sqlmap`, the recorded process name is not:

```json
{
"host": { "os": { "type": "linux" } },
"event": { "action": "exec", "type": "start" },
"process": {
"name": "python3.10",
"executable": "/usr/bin/python3.10",
"args": ["python3", "sqlmap.py", "-u", "http:///?id=1", "--batch"]
}
}
```

The `comm` matrix above was produced on the same platform with a minimal script that prints `/proc/self/comm`, so the behaviour can be reproduced without any tooling:

```bash
printf '#!/usr/bin/env python3\nprint(open("/proc/self/comm").read().strip())\n' > t.py
chmod +x t.py && ./t.py # prints: python3

printf '#!/usr/bin/python3\nprint(open("/proc/self/comm").read().strip())\n' > d.py
chmod +x d.py && ./d.py # prints: d.py
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.