Lucene search
+L

Linux Gather Sudo Policy Audit (GTFO)

🗓️ 15 Jul 2026 10:06:48Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

Linux Gather Sudo Policy Audit (GTFO) reconnaissance tool for sudo policy auditing.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import re
import time

from kittysploit import *

from lib.post.linux.session import LinuxSessionMixin
from lib.post.linux.sudo_policy_lists import ALWAYS_BLOCK, ALLOW_NOEXEC


class Module(Post, LinuxSessionMixin):
    __info__ = {
        "name": "Linux Gather Sudo Policy Audit (GTFO)",
        "description": "GTFO-style sudo policy audit (NOPASSWD / NOEXEC)",
        "author": "KittySploit Team",
        "platform": Platform.LINUX,
        "session_type": [SessionType.SHELL, SessionType.METERPRETER, SessionType.SSH],
        "references": [
            "https://gtfobins.github.io/",
            "https://attack.mitre.org/techniques/T1548.003/",
        ],
        "agent": {
            "risk": "passive",
            "effects": ["reconnaissance"],
            "expected_requests": 4,
            "reversible": True,
            "approval_required": False,
            "produces": ["risk_signals"],
            "cost": 0.8,
            "noise": 0.35,
            "value": 0.95,
            "requires": {"capabilities_any": ["shell"], "capabilities_all": []},
            "chain": {
                "consumes_capabilities": ["shell"],
                "produces_capabilities": [],
            },
        },
    }

    block_file = OptString("", "Extra ALWAYS_BLOCK paths (local file)", False)
    noexec_file = OptString("", "Extra ALLOW_NOEXEC paths (local file)", False)
    audit_user = OptString("", "Limit audit to one user", False)
    test_noexec_bypass = OptBool(True, "Test NOPASSWD NOEXEC bypass", False)
    require_root = OptBool(True, "Require root for full audit", False)
    cleanup = OptBool(True, "Remove uploaded script after run", False)

    def _load_local_list(self, path: str) -> list[str]:
        entries: list[str] = []
        if not str(path or "").strip():
            return entries
        local = os.path.abspath(str(path).strip())
        if not os.path.isfile(local):
            raise ProcedureError(FailureType.ConfigurationError, f"List file not found: {local}")
        with open(local, encoding="utf-8", errors="replace") as handle:
            for line in handle:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                entries.append(line)
        return entries

    @staticmethod
    def _bash_array(items: list[str]) -> str:
        quoted = " ".join(LinuxSessionMixin.linux_shell_quote(p) for p in items)
        return f"({quoted})"

    def _audit_script(self, *, block: list[str], noexec: list[str], user_filter: str, test_bypass: bool) -> str:
        block_arr = self._bash_array(block)
        noexec_arr = self._bash_array(noexec)
        user_filter_q = self.linux_shell_quote(user_filter) if user_filter else ""
        test_bypass_flag = "1" if test_bypass else "0"
        return f"""#!/bin/bash
set -u
TEST_BYPASS={test_bypass_flag}
USER_FILTER={user_filter_q!r}

ALWAYS_BLOCK={block_arr}
ALLOW_NOEXEC={noexec_arr}

viol=0

run_as_user() {{
  local user="$1"; shift
  if command -v runuser >/dev/null 2>&1; then
    runuser -u "$user" -- "$@"
  else
    su -s /bin/sh "$user" -c "$(printf '%q ' "$@")"
  fi
}}

check_base_state() {{
  local user="$1" bin="$2"
  [[ ! -e "$bin" ]] && {{ echo NOT_INSTALLED; return; }}
  if sudo -n -l -U "$user" "$bin" >/dev/null 2>&1; then
    if run_as_user "$user" sudo -n "$bin" -c true >/dev/null 2>&1; then
      echo NOPASSWD
    else
      echo PASSWD
    fi
  else
    echo BLOCKED
  fi
}}

check_noexec_flag() {{
  if sudo -n -l -U "$1" "$2" 2>/dev/null | grep -q NOEXEC; then
    echo SET
  else
    echo UNSET
  fi
}}

check_noexec_enforced() {{
  local u="$1" b="$2" o=""
  case "$b" in
    */vim|*/vi|*/view)   o=$(run_as_user "$u" sudo -n "$b" -c ':!id -u' 2>/dev/null);;
    */less)              o=$(echo '!id -u' | run_as_user "$u" sudo -n "$b" 2>/dev/null);;
    */python*|*/python3) o=$(run_as_user "$u" sudo -n "$b" -c 'import os;print(os.geteuid())' 2>/dev/null);;
    */perl)              o=$(run_as_user "$u" sudo -n "$b" -e 'print $<\\n' 2>/dev/null);;
    */awk)               o=$(run_as_user "$u" sudo -n "$b" 'BEGIN{{print PROCINFO["uid"]}}' 2>/dev/null);;
    *) echo UNKNOWN; return;;
  esac
  [[ "$o" == "0" ]] && echo BYPASSED || echo ENFORCED
}}

ADMIN_USERS=()
if [[ -n "$USER_FILTER" ]]; then
  ADMIN_USERS=("$USER_FILTER")
else
  for g in sudo wheel admin; do
    if getent group "$g" >/dev/null 2>&1; then
      IFS=, read -r -a members <<<"$(getent group "$g" | cut -d: -f4)"
      for m in "${{members[@]}}"; do
        [[ -n "$m" ]] && ADMIN_USERS+=("$m")
      done
    fi
  done
fi

if [[ ${{#ADMIN_USERS[@]}} -eq 0 ]]; then
  mapfile -t ADMIN_USERS < <(awk -F: '$3>=1000{{print $1}}' /etc/passwd)
fi

echo "KS_SUDO_AUDIT_BEGIN"
for user in "${{ADMIN_USERS[@]}}"; do
  [[ -z "$user" ]] && continue
  echo "KS_USER $user"
  for cmd in "${{ALWAYS_BLOCK[@]}}"; do
    st=$(check_base_state "$user" "$cmd")
    ok=NO
    [[ "$st" == BLOCKED || "$st" == NOT_INSTALLED ]] && ok=YES
    [[ "$ok" == NO ]] && viol=1
    echo "KS_ROW|$user|$cmd|$st|BLOCKED|$ok"
  done
  for cmd in "${{ALLOW_NOEXEC[@]}}"; do
    base=$(check_base_state "$user" "$cmd")
    case "$base" in
      BLOCKED|NOT_INSTALLED) st=$base ;;
      NOPASSWD|PASSWD)
        if [[ "$(check_noexec_flag "$user" "$cmd")" == UNSET ]]; then
          st=NOEXEC_MISSING
        elif [[ "$base" == NOPASSWD && "$TEST_BYPASS" == 1 ]]; then
          st=$(check_noexec_enforced "$user" "$cmd")
        else
          st=NOEXEC_SET
        fi
        ;;
      *) st=$base ;;
    esac
    ok=NO
    [[ "$st" == ENFORCED || "$st" == NOEXEC_SET || "$st" == BLOCKED || "$st" == NOT_INSTALLED ]] && ok=YES
    [[ "$ok" == NO ]] && viol=1
    echo "KS_ROW|$user|$cmd|$st|NOEXEC|$ok"
  done
done
echo "KS_SUMMARY violations=$viol"
exit "$viol"
"""

    def _parse_output(self, output: str) -> tuple[list[dict], int]:
        rows: list[dict] = []
        violations = 0
        current_user = ""
        for line in output.splitlines():
            line = line.strip()
            if line.startswith("KS_USER "):
                current_user = line.split(" ", 1)[1].strip()
            elif line.startswith("KS_ROW|"):
                parts = line.split("|")
                if len(parts) >= 6:
                    rows.append({
                        "user": parts[1],
                        "command": parts[2],
                        "state": parts[3],
                        "expect": parts[4],
                        "compliant": parts[5],
                    })
            elif line.startswith("KS_SUMMARY violations="):
                try:
                    violations = int(line.split("=", 1)[1].strip())
                except ValueError:
                    violations = 0
        return rows, violations

    def run(self):
        if not self.linux_require_linux():
            return False

        if self.require_root and not self.linux_is_root():
            print_error("Root privileges are required to audit sudo policy for all admin users.")
            print_info("Run as root or set require_root=false to attempt a limited audit.")
            return False

        try:
            block = list(ALWAYS_BLOCK) + self._load_local_list(str(self.block_file or ""))
            noexec = list(ALLOW_NOEXEC) + self._load_local_list(str(self.noexec_file or ""))
        except ProcedureError:
            raise
        except OSError as exc:
            raise ProcedureError(FailureType.ConfigurationError, f"Cannot read list file: {exc}") from exc

        user_filter = str(self.audit_user or "").strip()
        script_body = self._audit_script(
            block=block,
            noexec=noexec,
            user_filter=user_filter,
            test_bypass=bool(self.test_noexec_bypass),
        )

        remote_dir = "/tmp"
        remote_path = f"{remote_dir}/ks_sudo_policy_audit_{int(time.time())}.sh"
        print_info("=" * 60)
        print_status("Sudo policy audit")
        print_info(f"  ALWAYS_BLOCK entries: {len(block)}")
        print_info(f"  ALLOW_NOEXEC entries: {len(noexec)}")
        if user_filter:
            print_info(f"  Target user: {user_filter}")

        if not self.linux_upload_bytes(script_body.encode("utf-8"), remote_path, executable=True):
            print_error("Failed to upload audit script")
            return False

        print_status("Running sudo policy audit (this may take several minutes)...")
        output = self.linux_execute(f"bash {self.linux_shell_quote(remote_path)}", timeout=0)

        if self.cleanup:
            self.linux_execute(f"rm -f {self.linux_shell_quote(remote_path)}")

        if not output:
            print_warning("Audit script returned no output")
            return False

        rows, violations = self._parse_output(output)
        if not rows and "KS_SUDO_AUDIT_BEGIN" not in output:
            print_error("Unexpected audit output")
            print_info(output[:3000])
            return False

        bad_rows = [r for r in rows if r.get("compliant") == "NO"]
        users = sorted({r["user"] for r in rows})

        for user in users:
            user_bad = [r for r in bad_rows if r["user"] == user]
            if not user_bad:
                continue
            print_info("-" * 60)
            print_warning(f"Violations for user: {user} ({len(user_bad)})")
            for row in user_bad[:40]:
                print_warning(
                    f"  {row['command']} — state={row['state']} "
                    f"(expected {row['expect']})"
                )
            remaining = len(user_bad) - 40
            if remaining > 0:
                print_info(f"  ... {remaining} more violation(s)")

        print_info("=" * 60)
        if violations > 0 or bad_rows:
            print_error(f"POLICY VIOLATIONS DETECTED ({len(bad_rows)} non-compliant rule(s))")
            return False

        print_success("POLICY OK — no sudo GTFO policy violations detected")
        return True

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

15 Jul 2026 10:06Current
5.8Medium risk
Vulners AI Score5.8
1