SecureAEye LAB ONLINE · SIG 657 · DET ~41s

← Blog

The obvious Linux credential-theft rule raised zero alerts. Here's why, and what fired instead

Detection win · 2026-10-30

For eight days our Linux lab victim let an attacker read credential files without a single relevant alert. We detonated the two stock Atomic Red Team tests for unsecured credentials six times between 2026-10-20 and 2026-10-28, and every run graded partially detected: the SIEM saw the attack start, and nothing about what it actually did.

When we wrote the "obvious" rule to fix it, it raised zero alerts. This post is about why, and about the two rules that do fire now, 100430 (T1552.001, credentials in files) and 100431 (T1552.003, shell history).

What the lab saw before

Every one of the six runs produced the same five alerts and nothing else:

RuleWhat it is
100101Our lab marker: "a detonation started"
5402sudo to root
5501 / 5502PAM session opened / closed

That is the harness becoming root. The part where the attack reads a secret left no trace at all. The reason was simple once we looked: lab/config/wazuh/agent/linux-audit.rules only recorded execve for bash, sh, python3, curl and wget, and only watched /etc/shadow and /etc/ssh/ for file access. The Atomic tests run inside pwsh, read files in home directories, and use cat, grep and find. auditd was never asked to look. This was a collection gap, not a rule gap: no rule can match an event that was never logged.

The obvious fix, and why it was silent

The textbook answer is a file watch. Tell auditd to log reads of the credential file, then alert on that:

-w /root/.bash_history -p r -k lab_cred_access
<rule id="100430" level="7">
  <if_group>audit</if_group>
  <field name="audit.key">^lab_cred_access$</field>
  <field name="audit.file.name" type="pcre2">(?i)\.bash_history$</field>
</rule>

We deployed it and re-detonated (run-2026-10-28-048). It raised zero alerts.

The watch itself is ordinary auditd. The problem is on the Wazuh side: the bundled audit ruleset that our custom rules hang off (if_group audit) raises alerts for execve records, not for the plain file-access records a -w ... -p r watch produces. So the read never becomes a parent alert that a child rule can match. A rule written against it is structurally dead on a stock Wazuh install, and nothing tells you so. There is no error and no warning. It just never fires.

If you take one thing from this post: a rule that has never fired is not a detection, it is a hypothesis. We only caught this because the lab grades every rule on a real detonation.

We had a second problem in the same rule. It chased .bash_history, which is ATT&CK T1552.003 (shell history), but it was tagged T1552.001 (credentials in files). Our batch planner had paired the two techniques with each other's tests, and our first gap write-up carried the mix-up forward. Worth checking in your own content: the two sub-techniques sit next to each other and are easy to swap.

What fires now: key on the reader, not the file

Since Wazuh alerts on execve, we moved the detection to the process that does the reading. Two changes:

1. Collect it. Add execve watches for the readers the attacks use (PR #145):

-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/cat  -k lab_exec
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/grep -k lab_exec
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/find -k lab_exec

2. Match the reader plus a credential path in its arguments. The Wazuh decoder exposes the binary as audit.command and each argument as audit.execve.a1, a2 and so on. Rule 100430 (PRs #151 and #154) fires when a shell reader is run on a file that looks like a secret:

<rule id="100430" level="6">
  <if_group>audit</if_group>
  <field name="audit.key">^lab_exec$</field>
  <field name="audit.command" type="pcre2">(?i)^(?:cat|less|more|tail|head|strings|grep|nl)$</field>
  <field name="audit.execve.a1" type="pcre2">(?i)(?:\.drone\.yml$|\.aws[/\\]|(?:^|[/\\])\.env$|(?:^|[/\\])credentials$|id_rsa|\.npmrc$|\.git-credentials$|\.kube[/\\]config|\.docker[/\\]config|terraform\.tfstate)</field>
  <mitre><id>T1552.001</id></mitre>
</rule>

Rule 100431 is simpler: grep or find executed under the same lab_exec key, level 5, tagged T1552.003.

The proof runs

RunTechniqueAtomic testResultRuleLatencyAlerts in window
run-2026-10-28-050T1552.003Search Through Bash History (3cfde62b)detected1004313 s6 (1 expected)
run-2026-10-28-057T1552.001Access Drone CI Config File (a11f22fb)detected1004301 s9 (1 expected)

In run 057 the alert came from cat /home/<user>/.drone.yml. The file path landed in audit.execve.a1, exactly where the rule looks, so we didn't need to tune argument positions.

One honest detail about 057. The Drone CI test runs find / -name .drone.yml -exec cat and has no setup step, so it only reads something if a .drone.yml already exists. The earlier runs found nothing because there was nothing to find. For the proof run we staged a harmless .drone.yml in the victim's home directory first. That's a fair test of the rule (a real attacker only bothers when the file is there), but it means the three earlier Drone CI runs graded "partial" were partly a test with no target, not only a blind SIEM. The fixture was removed when both victims were reverted to their clean snapshots on 2026-10-30.

Where these rules are weak

We'd rather say this than have you find it.

What we'd tell a defender

Attack record: unsecured-credentials. Grounded in the committed bundles lab/results/run-2026-10-28-050/run.json and lab/results/run-2026-10-28-057/run.json, the six earlier partial runs on the same two tests (run-2026-10-20-003/004, run-2026-10-27-029/030, run-2026-10-28-023/024), rules 100430 and 100431 in lab/config/wazuh/rules/local_rules.xml, and lab/config/wazuh/agent/linux-audit.rules. Run 048 (the silent file-watch rule) raised no alerts and is recorded in the comment above rule 100430.