从这个漏洞入手,一是为了更加熟悉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调试)