diff --git a/CHANGELOG.md b/CHANGELOG.md index b01cc80..df9d7aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project are documented here, following ### Added +- **SKILL-SH-011**: flag setuid changes, root-owned payloads, and writes to `/etc/sudoers`. - **SKILL-SEC-007**: flag references to bash, zsh, and PowerShell ConsoleHost history files. - **SKILL-OBF-004**: flag PowerShell `-EncodedCommand` / `-enc` and `xxd -r` output piped into a shell. - **SKILL-INJ-010**: flag prose that fetches remote instructions and tells the agent to follow them. diff --git a/src/rules.js b/src/rules.js index 013dd06..1dd3002 100644 --- a/src/rules.js +++ b/src/rules.js @@ -217,6 +217,11 @@ export const RULES = [ remediation: "Writing to authorized_keys or under ~/.ssh grants persistent remote login. Never ship this in a skill.", pattern: /(authorized_keys\b|(>>|>)\s*~?\/?\.ssh\/)/gi }, + { id: "SKILL-SH-011", severity: "high", category: "dangerous-shell", appliesTo: "code", + title: "Creates privileged executables or modifies sudoers", + remediation: "Setuid binaries, root-owned payloads, and sudoers writes can create persistent privilege escalation.", + pattern: /(chmod\s+(?:u\+s|4[0-7]{3})\s+|chown\s+root:root\s+|(?:>>|>)\s*\/etc\/sudoers\b)/gi }, + // ---- Dynamic code execution ---- { id: "SKILL-OBF-003", severity: "medium", category: "obfuscation", appliesTo: "code", title: "Dynamic code execution (exec/compile)", diff --git a/test/sh-011-privilege-persistence.test.js b/test/sh-011-privilege-persistence.test.js new file mode 100644 index 0000000..fd3e333 --- /dev/null +++ b/test/sh-011-privilege-persistence.test.js @@ -0,0 +1,24 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { scanText } from "../src/scan.js"; + +const RULE = "SKILL-SH-011"; + +test("SKILL-SH-011 flags setuid and sudoers privilege escalation", () => { + for (const source of [ + "chmod u+s /tmp/backdoor\n", + "chmod 4755 /tmp/x\n", + "chown root:root /tmp/x\n", + "echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers\n", + ]) { + const findings = scanText(source, "setup.sh", null).filter((f) => f.rule === RULE); + assert.equal(findings.length, 1, source); + assert.equal(findings[0].severity, "high"); + } +}); + +test("SKILL-SH-011 keeps ordinary permission changes clean", () => { + for (const source of ["chmod 755 ./script.sh\n", "chown app:app ./data\n"]) { + assert.equal(scanText(source, "setup.sh", null).filter((f) => f.rule === RULE).length, 0, source); + } +});