从这个漏洞入手,一是为了更加熟悉linux,扩宽自己linux的知识面,二是想把学习的pwn知识实践操作一下。
摘要
该漏洞成因主要是由于sudo在处理单个反斜杠结尾的命令,会出现逻辑上的错误,从而导致堆溢出。当sudo通过-s或-i命令行选项在shell模式下运行命令时,它将在命令参数中使用反斜杠转义特殊字符。但使用-s或 -i标志运行sudoedit时,实际上并未进行转义,从而可能导致缓冲区溢出。(后面分析原源代码会提到),只要存在sudoers文件(通常是 /etc/sudoers),攻击者就可以使用本地普通用户利用sudo获得系统root权限。
漏洞检测
在非root权限下,运行sudoedit -s /,如果出现下图这样,说明系统受到该漏洞影响。

代码逻辑分析
命令行模式下运行sudo,加上-s选项会设置MODE_SHELL flag;加上-i选项会设置MODE_SHELL flag 和 MODE_LOGIN_SHELL flag。sudo的main() 函数开头调用了parse_args(),parse_args() 会连接所有命令行参数。并给元字符加反斜杠来重写 argv。
1 | //parse_args.c parse_args() |
在suduers.c中的sudoers_policy_main()中调用了 set_cmnd()函数,在 set_cmnd()函数中,首先根据参数使用 strlen()函数计算了参数的 size,再调用 malloc()函数分配了 size大小的堆空间 user_args 。随后判断是否开启了 MODE_SHELL,如果开启了将会 连接命令行参数并存入堆空间 user_args。
1 | // sudoers.c set_cmnd() |
小总结一下,首先大概就是有两个流程,根据上面的分析,我们需要触发漏洞需要启用MODE_SHELL,但是如果启用了该标志,在第一个逻辑parge_args()中就会对所有参数进行转义,触发漏洞的 \,将会被转义为 \\,这样就无法触发漏洞了。
1 | // parse_args() 换码代码 |
所以要触发漏洞就需要设置 MODE_SHELL 和 MODE_EDIT/MODE_CHECK ,但不设置 MODE_RUN,但是在parge_args()代码中,设置了MODE_EDIT/MODE_CHECK,parse_args()就会从valid_flags移除MODE_SHELL,如果此时还设置了MODE_SHELL就会报错
1 | case 'e': |
所以选择使用 sudoedit。因为 sudoedit还是会被软链接到使用 sudo命令,但是在 parse_args()函数中会自动设置 MODE_EDIT和不会重置 valid_flags,则 MODE_SHELL仍然在 valid_flags中 ,而且不会设置 MODE_RUN,这样就能跳过 parse_args()函数中转义参数的部分,同时满足 set_cmnd()函数中漏洞触发的部分。
1 | //parse_args.c parse_args() |
所以,只要执行sudoedit -s \,就能同时设置MODE_EDIT和MODE_SHELL,但不设置MODE_RUN。跳过parse_args()中的换码代码,直接执行漏洞代码set_cmnd(),溢出user_args堆缓冲区。
漏洞成因
1 | /* Alloc and build up user_args. */ |
还是上面的set_cmnd函数,如果输入指令是
1 | sudoedit -s '\' 112233445566 |
那么进入该函数时,NewArgv的结构如下
1 | NewArgv[0]: sudoedit |
首先会计算 NewArgv [1]、[2] 两个参数的长度 2 + 13 = 15 .
因此user_args分配的内存大小为 15 字节。
然后会把 NewArgv[1]、[2] 的数据拷贝到user_args里面。
拷贝过程中如果 from[0] 为 \,且 from[1] 不是空格就会from++。所以在处理NewArgv[1]时,from[0] 就是 \ ,from[1] 为 \x00 ,会通过这个判断让 from++ ,然后后面会再次from++.
之后from就指向了NewArgv[1]字符串\x00后面一个字符的位置,后面紧跟着的是NewArgv[2],所以此时 from 执行的就是 NewArgv[2] 的开头
从而会再次进入while循环把NewArgv[2]拷贝到user_args
然后处理NewArgv[2]会再次把NewArgv[2]拷贝到user_args
因此最终结果就是 NewArgv[2] 被拷贝了两次,实际的写入数据长度为26字节
漏洞利用
user_args堆缓冲区的size可控(根据前面set_cmnd()就是命令行参数合并后的长度);- 能分别控制size和溢出的内容(第一段命令行参数后紧跟第二段命令行参数,第二段命令行参数不包含在size中);
- 可以写null字节到
user_args(每个以单反斜杠结尾的命令行参数或环境变量,都能往user_args写1个null字节)
最后的提权是通过堆溢出覆盖nss_load_library函数加载so的时候需要用到的结构体service_user,覆盖此结构体中的so名字符串,这样就可以让程序加载我们指定的so文件,从而完成任意代码执行。
目标
溢出后覆盖service_user结构。该结构出现在libc的nss_load_library()函数中,用于加载动态链接库。如果能覆盖service_user->name,就能指定加载我们伪造的库,利用root权限运行非root权限的库。
1 | // 1. service_user 结构 |
根据上面的代码,我们知道需要将 ni->library覆盖为 null,将 ni->name覆盖我们自己伪造的库名字,且伪造的库文件名必须是 libnss_xxx.so
实战分析
借助gdb工具对其进行调试,首先输入指令gdb sudoedit,然后设置参数set args -s '\' AAAAAAAAAAAAAAAA,在代码switch部分下断点。

详细步入下面policy_check函数查看

紧跟着会进入到sudoers_policy_check------>sudoers_policy_main
在该函数中会将argc和argv传递给NewArgc和NewArgv


然后进入set_cmnd()函数,也就是前面说的溢出点,首先在计算参数长度这里,输出size为19(反斜杠长度1+1+16*A+1)

输入heap查看堆内存,可以看到原本size为19,所以申请了一个0x20的chunk(0x556d7f991e70),但是拷贝了0x22个字节进去,所以把下一个chunk的size给覆盖掉了,所以才导致后一个堆块大小为0x4141414141414141

(tips:由于我用的是docker镜像,所以调试运行会出现错误warning: Error disabling address space randomization: Operation not permitted。这是由于linux 内核为了安全起见,采用了Seccomp(secure computing)的沙箱机制来保证系统不被破坏。它能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用,即read(), write(), exit()和sigreturn(),否则进程便会被终止。
解决:docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined 你的容器才能利用GDB调试)