Lucene search
+L

Linux Docker / Container Detection

🗓️ 16 Jul 2026 12:39:41Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

Detect if a Linux session runs inside Docker, Podman, LXC, or Kubernetes.

Refs
Code
from kittysploit import *
from lib.post.linux.system import System
from lib.post.linux.session import LinuxSessionMixin


class Module(Post, System, LinuxSessionMixin):
    __info__ = {
        "name": "Linux Docker / Container Detection",
        "description": (
            "Detect whether the session runs inside Docker, Podman, LXC, or Kubernetes "
            "(/.dockerenv, cgroups, mountinfo, systemd-detect-virt -c). "
            "Unlike detect_vm, this does not rely on hypervisor/DMI signals."
        ),
        "platform": Platform.LINUX,
        "author": "KittySploit Team",
        "session_type": [SessionType.SHELL, SessionType.METERPRETER, SessionType.SSH],
        "tags": ["linux", "post", "gather", "docker", "container"],
        "agent": {
            "risk": "passive",
            "effects": [],
            "expected_requests": 6,
            "reversible": True,
            "approval_required": False,
            "produces": ["risk_signals", "tech_hints"],
            "cost": 0.5,
            "noise": 0.1,
            "value": 1.2,
            "chain": {
                "consumes_capabilities": ["shell"],
                "produces_capabilities": [],
                "suggested_followups": [
                    "post/shell/linux/gather/container_escape_check",
                    "post/shell/linux/exploits/copy_fail_docker_escape_cve_2026_31431",
                    "post/shell/multi/gather/privesc_suggester",
                ],
            },
        },
    }

    verbose = OptBool(False, "Print raw probe output for each check", False)

    def _run(self, command: str) -> str:
        try:
            output = self.linux_execute(command)
            return output.strip() if output else ""
        except Exception:
            return ""

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

        print_status("Detecting container / Docker environment...")
        evidence = []
        runtime = None

        # Marker files — strongest Docker/Podman signals
        print_status("Checking container marker files...")
        dockerenv = self._run("test -f /.dockerenv && echo yes || echo no")
        if dockerenv == "yes":
            evidence.append(("high", "Marker file /.dockerenv present"))
            runtime = runtime or "Docker"
            print_success("Found /.dockerenv")
        elif self.verbose:
            print_info("  /.dockerenv not present")

        containerenv = self._run("test -f /run/.containerenv && echo yes || echo no")
        if containerenv == "yes":
            evidence.append(("high", "Marker file /run/.containerenv present (Podman)"))
            runtime = runtime or "Podman"
            print_success("Found /run/.containerenv (Podman)")
        elif self.verbose:
            print_info("  /run/.containerenv not present")

        # cgroups — works on older Docker (v1) and many k8s setups
        print_status("Checking cgroups...")
        cgroup = self._run(
            "cat /proc/1/cgroup 2>/dev/null; echo ---; cat /proc/self/cgroup 2>/dev/null"
        )
        if self.verbose and cgroup:
            for line in cgroup.splitlines()[:20]:
                print_info(f"  {line}")
        runtime_from_cgroup = self._classify_cgroup(cgroup)
        if runtime_from_cgroup:
            evidence.append(("high", f"Cgroup path indicates {runtime_from_cgroup}"))
            runtime = runtime or runtime_from_cgroup
            print_success(f"Cgroup indicates {runtime_from_cgroup}")

        # systemd-detect-virt -c (container mode; detect_vm uses VM mode only)
        print_status("Checking systemd-detect-virt -c...")
        virt = self._run(
            "command -v systemd-detect-virt >/dev/null 2>&1 && "
            "systemd-detect-virt -c 2>/dev/null || echo unavailable"
        ).lower()
        if virt and virt not in ("unavailable", "none", ""):
            label = self._virt_label(virt)
            evidence.append(("high", f"systemd-detect-virt -c => {virt}"))
            runtime = runtime or label
            print_success(f"systemd-detect-virt -c: {virt}")
        elif self.verbose:
            print_info(f"  systemd-detect-virt -c: {virt or 'n/a'}")

        # Mount / filesystem signals
        print_status("Checking mountinfo / overlay...")
        mounts = self._run(
            "grep -E 'docker|overlay|containerd|kubelet|lxc|podman' "
            "/proc/self/mountinfo 2>/dev/null | head -n 15"
        )
        if mounts:
            evidence.append(("medium", "Container-related mounts in /proc/self/mountinfo"))
            runtime = runtime or self._runtime_from_mounts(mounts)
            print_success("Container-related mounts detected")
            if self.verbose:
                for line in mounts.splitlines()[:10]:
                    print_info(f"  {line}")
        elif self.verbose:
            print_info("  No docker/overlay/containerd mounts matched")

        # PID 1 often differs from host init inside containers
        print_status("Checking PID 1...")
        pid1 = self._run(
            "tr '\\0' ' ' < /proc/1/cmdline 2>/dev/null; "
            "echo; ls -l /proc/1/exe 2>/dev/null"
        )
        if self._pid1_looks_container(pid1):
            evidence.append(("medium", f"PID 1 looks containerized: {pid1.splitlines()[0][:80]}"))
            runtime = runtime or "container"
            print_success("PID 1 does not look like host init")
            if self.verbose and pid1:
                for line in pid1.splitlines()[:5]:
                    print_info(f"  {line}")
        elif self.verbose:
            print_info(f"  PID 1: {(pid1.splitlines() or ['n/a'])[0][:80]}")

        # Env / hostname soft signals
        print_status("Checking env / hostname hints...")
        soft = self._run(
            "hostname 2>/dev/null; "
            "printenv 2>/dev/null | grep -Ei "
            "'^(DOCKER_|KUBERNETES_|K8S_|container=|HOSTNAME=)' | head -n 20"
        )
        soft_hits = self._soft_env_hits(soft)
        for hit in soft_hits:
            evidence.append(("low", hit))
            print_info(f"  Hint: {hit}")
            if "kubernetes" in hit.lower() or "k8s" in hit.lower():
                runtime = runtime or "Kubernetes"
            elif "docker" in hit.lower():
                runtime = runtime or "Docker"

        # Summary
        print_status("=" * 60)
        if evidence:
            confidence = self._confidence(evidence)
            kind = (runtime or "container").upper()
            print_success(f"Container detected: {kind} (confidence={confidence})")
            print_info(f"Evidence ({len(evidence)}):")
            for severity, msg in evidence:
                print_info(f"  [{severity}] {msg}")
            print_info(
                "Suggested follow-up: post/shell/linux/gather/container_escape_check"
            )
        else:
            print_success("No container indicators found — likely bare metal or VM host")
            print_info(
                "Note: detect_vm checks hypervisors; a Docker guest on bare metal "
                "will often look like bare metal to detect_vm."
            )

        return True

    @staticmethod
    def _classify_cgroup(cgroup: str) -> str:
        text = (cgroup or "").lower()
        if not text:
            return ""
        if "kubepods" in text or "kubelet" in text:
            return "Kubernetes"
        if "/docker/" in text or "docker-" in text or ".scope" in text and "docker" in text:
            return "Docker"
        if "containerd" in text or "cri-containerd" in text:
            return "containerd"
        if "podman" in text or "libpod" in text:
            return "Podman"
        if "/lxc/" in text or "lxc.payload" in text:
            return "LXC"
        # Generic container path: .../docker-ce/... or long hex id under system.slice
        if "docker" in text:
            return "Docker"
        return ""

    @staticmethod
    def _virt_label(virt: str) -> str:
        mapping = {
            "docker": "Docker",
            "podman": "Podman",
            "lxc": "LXC",
            "lxc-libvirt": "LXC",
            "systemd-nspawn": "systemd-nspawn",
            "rkt": "rkt",
            "container-other": "container",
            "wsl": "WSL",
        }
        return mapping.get(virt, virt)

    @staticmethod
    def _runtime_from_mounts(mounts: str) -> str:
        text = (mounts or "").lower()
        if "kubelet" in text or "pods/" in text:
            return "Kubernetes"
        if "containerd" in text:
            return "containerd"
        if "podman" in text or "containers/storage" in text:
            return "Podman"
        if "docker" in text or "overlay" in text:
            return "Docker"
        return "container"

    @staticmethod
    def _pid1_looks_container(pid1: str) -> bool:
        text = (pid1 or "").lower()
        if not text.strip():
            return False
        host_like = (
            "/sbin/init",
            "/usr/lib/systemd/systemd",
            "/lib/systemd/systemd",
            "systemd",
        )
        # If exe clearly points at systemd/init as PID 1, not a strong container signal alone
        first = text.splitlines()[0] if text.splitlines() else text
        if any(h in first for h in ("systemd", "/sbin/init", "/bin/init")):
            return False
        container_like = (
            "docker-init",
            "tini",
            "dumb-init",
            "entrypoint",
            "/bin/sh",
            "/bin/bash",
            "sleep",
            "pause",
        )
        return any(c in text for c in container_like)

    @staticmethod
    def _soft_env_hits(blob: str) -> list:
        hits = []
        text = blob or ""
        for line in text.splitlines():
            low = line.lower()
            if low.startswith("docker_") or "docker_" in low:
                hits.append(f"env: {line.strip()[:100]}")
            elif low.startswith("kubernetes_") or low.startswith("k8s_"):
                hits.append(f"env: {line.strip()[:100]}")
            elif low.startswith("container="):
                hits.append(f"env: {line.strip()[:100]}")
            elif low.startswith("hostname=") and len(line.split("=", 1)[-1].strip()) == 12:
                # Docker default hostname is often a 12-char container id
                hits.append(f"hostname looks like container id: {line.split('=', 1)[-1].strip()}")
        # bare hostname line (first line from hostname cmd)
        lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
        if lines and "=" not in lines[0] and len(lines[0]) == 12 and lines[0].isalnum():
            hits.append(f"hostname looks like container id: {lines[0]}")
        # dedupe
        seen = set()
        out = []
        for h in hits:
            if h not in seen:
                seen.add(h)
                out.append(h)
        return out

    @staticmethod
    def _confidence(evidence: list) -> str:
        scores = {"high": 3, "medium": 2, "low": 1}
        total = sum(scores.get(sev, 0) for sev, _ in evidence)
        if total >= 5 or any(sev == "high" for sev, _ in evidence):
            return "high"
        if total >= 2:
            return "medium"
        return "low"

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

17 Jul 2026 05:31Current
5.9Medium risk
Vulners AI Score5.9
1