文章总结: 本文深入分析CVE-2026-4747漏洞,该漏洞位于FreeBSD内核kgssapi.ko模块的svcrpcgssvalidate函数中,由于缺少边界检查导致栈缓冲区溢出。攻击者可通过RPCSECGSS认证的畸形数据包远程执行内核代码,最终获得root权限的交互式shell。利用过程需克服地址随机化、不可执行内存等限制,采用多轮ROP链写入shellcode。关键发现包括漏洞触发需NFS服务及有效Kerberos票据,利用链耗时约45秒。建议管理员及时更新系统补丁。 综合评分: 88 文章分类: 漏洞分析,渗透测试,二进制安全,红队,解决方案
CVE-2026-4747:一个FreeBSD内核高危漏洞的利用复盘
幻泉之洲
2026年4月11日 10:00 北京
在小说阅读器读本章
去阅读
本文分析了CVE-2026-4747漏洞,这是一个位于FreeBSD内核kgssapi.ko模块中的栈缓冲区溢出漏洞。通过RPCSEC_GSS认证的畸形数据包,攻击者能远程执行内核代码,最终获得root权限的交互式shell。我们将拆解漏洞成因、绕过限制的关键难点,并完整复盘从认证到代码执行的全链条。
漏洞基本信息
FreeBSD-SA-26:08.rpcsec_gss,CVE编号CVE-2026-4747。受影响的系统包括FreeBSD 13.5, 14.3和14.4。修复版本是14.4-RELEASE-p1及之后。
漏洞位于内核模块kgssapi.ko的函数svc_rpc_gss_validate()中。本质是一个经典的栈缓冲区溢出:在对RPC认证数据进行拷贝时,缺失了边界检查。
漏洞利用需要网络可达的NFS服务(监听2049/TCP)以及有效的Kerberos票据 (RPCSEC_GSS)。这意味着在真实企业内网中,任何拥有域账户凭据的用户都可能成为攻击入口。
漏洞根源:缺少检查的memcpy
有问题的代码
IXDR_PUT_ENUM(buf, msg->rm_direction); IXDR_PUT_LONG(buf, msg->rm_call.cb_rpcvers); … IXDR_PUT_ENUM(buf, oa->oa_flavor); IXDR_PUT_LONG(buf, oa->oa_length);
if (oa->oa_length) { // BUG: No bounds check on oa_length! // After 32 bytes of header, only 96 bytes remain in rpchdr. // If oa_length > 96, this overflows past rpchdr into: // local variables → saved callee-saved registers → return address memcpy((caddr_t)buf, oa->oa_base, oa->oa_length); buf += RNDUP(oa->oa_length) / sizeof(int32_t); }
简单来说,函数在栈上分配了一个128字节的缓冲区rpchdr。前面32字节用于填充RPC头部信息,剩下的96字节用于存放认证数据体(oa_base)。问题在于,代码直接信任了用户传入的oa_length,如果这个值大于96,memcpy就会溢出,覆盖栈上的保存寄存器和返回地址。
修复方案
修复补丁简单粗暴,就是在拷贝前加了个长度检查:
if (oa->oa_length > sizeof(rpchdr) – 8 * BYTES_PER_XDR_UNIT) { rpc_gss_log_debug(“auth length %d exceeds maximum”, oa->oa_length); client->cl_state = CLIENT_STALE; return (FALSE); }
一行缺失的检查,埋下了一个远程内核代码执行的雷。
栈内存布局与溢出路径
通过反汇编svc_rpc_gss_validate的函数序言,我们可以精确画出栈帧:
-
rpchdr数组起始于
[rbp-0xc0]。 -
memcpy的目标地址是
rpchdr + 32,即[rbp-0xa0]。 -
从
[rbp-0xa0]开始溢出,覆盖顺序依次是:局部变量、保存的寄存器RBX、R12、R13、R14、R15、保存的RBP,最后是返回地址。
但这是理论偏移。实际上,RPCSEC_GSS的凭证体前面还有一个GSS头部(包含版本、过程、序列号、服务类型)和一个上下文句柄(16字节)。加上XDR的对齐填充,这会导致我们的溢出数据整体向后偏移大约32字节。
所以,攻击载荷中控制返回地址的部分,实际是从凭证体的第200个字节开始的。这个偏移是通过发送De Bruijn序列并分析内核崩溃转储反复验证出来的,不是猜的。
如何触达漏洞代码?
这是个内核漏洞,但想远程碰到那块有问题的memcpy,条件还挺苛刻。
首先,为什么是NFS?因为kgssapi.ko模块就是为内核RPC子系统实现RPCSEC_GSS认证的。而FreeBSD内核里,主要(通常也是唯一)使用RPCSEC_GSS的RPC服务就是NFS服务器。NFS服务进程nfsd在2049端口监听,并且是在内核上下文处理RPC包的——这才让远程内核代码执行成为可能。
其次,为什么需要Kerberos?因为漏洞所在的svc_rpc_gss_validate()函数,只在满足一系列条件时才会被调用:
- RPC包使用RPCSEC_GSS认证(flavor=6)。
- GSS过程是DATA(不是INIT或DESTROY)。
- 服务器能找到与上下文句柄匹配的有效客户端条目。
- 通过重放序列检查。
没有有效的GSS上下文,服务器在第三步就会拒绝包(返回AUTH_REJECTEDCRED),你根本进不到那个有漏洞的memcpy。建立一个有效的GSS上下文,需要一次成功的Kerberos握手——攻击者必须拥有NFS服务主体的有效Kerberos票据。
这就把攻击场景限定在了拥有Kerberos基础设施(如Active Directory、FreeIPA)的企业环境。任何拥有有效Kerberos票据的用户(哪怕权限很低)都可以触发漏洞。在测试中,我们自建了KDC来模拟这个环境。
另外,XDR层对凭证体长度还有个MAX_AUTH_BYTES = 400的限制。所以我们的溢出长度范围被限定在97到400字节之间,也就是能溢出1到304个字节。
利用挑战与核心技巧
直接覆盖返回地址跳转执行代码?想得太简单了。内核环境限制重重,我们得解决一堆麻烦。
1. 如何执行任意代码?——没有可执行栈
FreeBSD内核默认启用kern.elf64.aslr.enable=1和kern.elf64.aslr.pie_enable=1,内核镜像和所有模块都是位置无关代码,地址随机化。栈也是不可执行的(NX)。
这意味着我们不能简单地在栈上布置shellcode然后跳过去。我们的思路是:利用ROP(面向返回编程)链,先在内核的某个可写区域(比如.BSS段)开辟一块可执行内存,再把我们的shellcode写进去,最后跳过去执行。
一个关键的ROP小工具(gadget)是pmap_change_prot()。它可以直接修改物理页的权限。我们用它把存放shellcode的.BSS页面的权限从“可写”改成“可读可写可执行”(RWX)。
2. 如何写入shellcode?——ROP链“搬运工”
我们最终的shellcode有432字节。但单次溢出的最大可控长度只有304字节(400减掉头部的96)。显然,一次送不完。
所以攻击被设计成了多轮。第一轮溢出完成两件事:调用pmap_change_prot开启BSS页面的执行权限,然后调用kthread_exit()干净地结束当前NFS工作线程(为下一轮腾出资源)。
接下来的13轮(第2到第14轮),每一轮都用一个ROP链执行一个“写入原语”:pop rdi; pop rax; mov [rdi], rax; ret。这个链子能用我们控制的数据(来自溢出凭证体)去覆盖一个我们控制的地址(也来自溢出凭证体)。每轮我们布置4个这样的“写入原语”,总共写入32字节的shellcode到BSS段中。13轮就是416字节。
为什么不用更高效的rep movsq(块复制)?试过。但能找到的push rsp; pop rsi这类gadget有副作用(会破坏计数寄存器),导致复制出来的数据前面混入了72字节的ROP链“垃圾”。pop; pop; mov虽然慢(40字节ROP链写8字节数据),但它可靠,没有副作用。
第15轮,也就是最后一轮,写入剩下的16字节shellcode,并布置一个跳转指令,将执行流导向BSS段中我们精心布置的shellcode起点。
3. shellcode设计:内核线程的“变身”之旅
最大的挑战在于:NFS工作线程是一个纯粹的内核线程。它没有用户地址空间(vmspace),没有用户态陷阱帧(trapframe)。它根本不知道怎么“返回”到用户态去运行一个/bin/sh。
我们的shellcode(432字节)分成了三部分:
-
入口点(Entry)
:首先清理硬件调试寄存器DR7(防止继承父线程的调试断点导致崩溃),然后调用
kproc_create()。这个函数会创建一个全新的内核进程,它拥有完整的proc结构体、线程、用户地址空间和陷阱帧——这是切换到用户态的前提。创建完成后,原NFS线程调用kthread_exit()退出。 -
工作函数(Worker)
:这是
kproc_create()创建的新进程的执行起点。它的任务是把当前这个内核进程“变成”/bin/sh。
- 初始化
image_args结构体,设置可执行路径为"/bin/sh",参数为"-c"和一个反向shell命令。 - 调用
kern_execve()。这个内核函数会加载/bin/sh,设置好用户态的RIP、RSP、段寄存器等。成功后,它返回EJUSTRETURN (-2)。 - 清除进程标志
P_KPROC。这是关键一步,如果不清除,fork_exit()(内核的回调机制)会在工作函数返回后调用kthread_exit()杀死进程,前功尽弃。清除后,fork_exit()就会走正常的userret路径。 - 返回。之后,
fork_exit()调用userret处理信号,最后通过doreti / iretq指令从陷阱帧返回到用户态——此时,处理器已经准备好执行/bin/sh了,并且是以root身份。
-
字符串数据
:存放
"/bin/sh"、"-c"和反向shell命令。这里用了经典的mkfifo技巧,因为FreeBSD的nc不支持-e参数。
4. 遇到的其他“坑”
-
MIT vs Heimdal不兼容
:攻击机用MIT Kerberos库,靶机FreeBSD用Heimdal库。建立GSS上下文时总失败,发现是MIT库默认对主机名进行DNS规范化解析,导致票据主体不匹配。解决办法是在
/etc/krb5.conf里禁用DNS规范化和反向解析。 -
NFS线程耗尽
:每一轮溢出都会“杀死”一个NFS工作线程。我们需要15轮,而建立GSS上下文也会临时占用线程。FreeBSD默认每个CPU生成8个NFS线程。单核虚拟机(8线程)下,打到第9轮左右线程就用光了。必须给靶机至少配置2个CPU(16线程)才有足够余量。在生产环境,NFS服务器线程数通常更多,这倒不算大问题。
漏洞验证复现步骤
为了方便研究人员复现,攻击者需要搭建包含以下组件的环境:
| 组件 | 作用 | 备注 | | — | — | — | | FreeBSD 14.4 虚拟机 (靶机) | 运行易受攻击的内核和NFS服务 | 需加载kgssapi.ko,启用NFS server | | MIT Kerberos KDC (在靶机上) | 为RPCSEC_GSS认证提供票据授予服务 | 需要创建NFS服务主体 | | Linux攻击机 | 运行利用脚本 | 需要python3-gssapi和krb5-client |
关键配置步骤包括在靶机上启动KDC、配置Kerberos、导出NFS共享;在攻击机上配置/etc/krb5.conf指向靶机的KDC,并获取测试用户票据。
此漏洞利用代码和分析仅供安全研究使用。未经授权对他人系统进行测试是非法行为。
总结
CVE-2026-4747是一个典型的、条件苛刻但威力巨大的漏洞。它结合了协议特性(RPCSEC_GSS)、基础设施依赖(Kerberos)和内核编程疏忽(缺少边界检查)。利用过程像一场精心编排的舞蹈,需要克服地址随机化、不可执行内存、内核线程限制等多重障碍。
整个攻击链耗时约45秒,发送15个特殊构造的RPC包,最终换来一个uid=0的交互式反向shell。这个案例再次提醒我们,即使是在被认为相对稳健的类Unix内核中,一个简单的长度检查缺失,在特定攻击路径下也可能被串联成完整的特权升级链条。对于系统管理员而言,及时更新系统补丁,尤其是内核及关键服务的补丁,永远是第一道也是最重要的防线。
参考资料
[1] https://github.com/califio/publications/blob/main/MADBugs/CVE-2026-4747/write-up.md
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:幻泉之洲 《CVE-2026-4747:一个FreeBSD内核高危漏洞的利用复盘》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论