黑客大神谈一谈Linux与suid提权

前几天我在代码审计知识星球里发表了一个介绍nmap利用interactive模式提权的帖子:
#进入nmap的交互模式nmap--interactive#执行sh , 提权成功!sh
但具体实施的时候会遇到很多有趣的问题 , 我们来详细研究一下 。
suid提权说到这个话题 , 我们不得不先介绍一下两个东西:
?suid提权是什么
?nmap为什么可以使用suid提权
通常来说 , Linux运行一个程序 , 是使用当前运行这个程序的用户权限 , 这当然是合理的 。 但是有一些程序比较特殊 , 比如我们常用的ping命令 。
ping需要发送ICMP报文 , 而这个操作需要发送RawSocket 。 在Linux2.2引入CAPABILITIES前 , 使用RawSocket是需要root权限的(当然不是说引入CAPABILITIES就不需要权限了 , 而是可以通过其他方法解决 , 这个后说) , 所以你如果在一些老的系统里ls-al$(whichping) , 可以发现其权限是-rwsr-xr-x , 其中有个s位 , 这就是suid:
root@linux:~#ls-al/bin/ping-rwsr-xr-x1rootroot44168May72014/bin/ping
suid全称是SetownerUserIDuponexecution 。 这是Linux给可执行文件的一个属性 , 上述情况下 , 普通用户之所以也可以使用ping命令 , 原因就在我们给ping这个可执行文件设置了suid权限 。
设置了s位的程序在运行时 , 其EffectiveUID将会设置为这个程序的所有者 。 比如 , /bin/ping这个程序的所有者是0(root) , 它设置了s位 , 那么普通用户在运行ping时其EffectiveUID就是0 , 等同于拥有了root权限 。
这里引入了一个新的概念EffectiveUID 。 Linux进程在运行时有三个UID:
?RealUID执行该进程的用户实际的UID
?EffectiveUID程序实际操作时生效的UID(比如写入文件时 , 系统会检查这个UID是否有权限)
?SavedUID在高权限用户降权后 , 保留的其原本UID(本文中不对这个UID进行深入探讨)
【黑客大神谈一谈Linux与suid提权】通常情况下EffectiveUID和RealUID相等 , 所以普通用户不能写入只有UID=0号才可写的/etc/passwd;在suid的程序启动时 , EffectiveUID就等于二进制文件的所有者 , 此时RealUID就可能和EffectiveUID不相等了 。
有的同学说某某程序只要有suid权限 , 就可以提权 , 这个说法其实是不准确的 。 只有这个程序的所有者是0号或其他superuser , 同时拥有suid权限 , 才可以提权 。
nmap为什么可以用suid提权常用nmap的同学就知道 , 如果你要进行UDP或TCPSYN扫描 , 需要有root权限:
$nmap-sUtargetYourequestedascantypewhichrequiresrootprivileges.QUITTING!$nmap-sS127.0.0.1Yourequestedascantypewhichrequiresrootprivileges.QUITTING!
原因就是这些操作会用到RawSocket 。
有时候你不得不使用sudo来执行nmap , 但在脚本调用nmap时sudo又需要tty , 有可能还要输入密码 , 这个限制在很多情况下会造成一些不必要的麻烦 。
所以 , 有一些管理员会给nmap加上suid权限 , 这样普通用户就可以随便运行nmap了 。
当然 , 增加了s位的nmap是不安全的 , 我们可以利用nmap提权 。 在nmap5.20以前存在interactive交互模式 , 我们可以通过这个模式来提权:
补充一个 , --interactive应该是比较老版本的nmap提供的选项 , 最近的nmap上都没有这个选项了 , 不过可以写一个nse脚本 , 内容为os.execute('/bin/sh') , 然后nmap--script=shell.nse来提权
的确是一个非常及时的补充 , 因为现在大部分的nmap都是没有interactive交互模式了 。
但经过测试我们发现 , 这个方法启动的shell似乎仍然是当前用户的 , 并没有我们想象中的提权 。
Linux发行版与shell我曾使用interactive模式提权成功 , 但是因为那个nmap版本过老 , 没有script支持 , 所以没法测试script的提权方法;同样 , 新的nmap支持script但又没有interactive模式 , 无法做直观对比 , 我只能先猜想提权失败的原因:
?nmap在高版本中限制了suid权限
?lua脚本中限制了suid权限
?新版Linux系统对子进程的suid权限进行了限制
这些猜想中变量太多 , 所以我需要控制一下 。 首先我阅读了老版本nmap的源码 , 发现其实!sh执行的就是很简单的system('sh') , 而且前面并没用丢弃EffectiveUID权限的操作:
}elseif(*myargv[0]=='!'){cptr=strchr(command,'!');system(cptr+1);}
那么我们将这个过程抽象成这么一个C程序suid.c:
intmain(intargc,char*argv[]){returnsystem(argv[1]);}
编译 , 并赋予其suid权限:
root@linux:/tmp#gccsuid.c-osuidroot@linux:/tmp#chmod+ssuid
接着我尝试在不同系统中 , 用www-data用户运行./suidid:
Linux发行版
Ubuntu14.04
Ubuntu16.04
Ubuntu18.04
CentOS6
CentOS8
Debian6
Debian8
Kali2019
可见 , 有些系统是root权限 , 有些系统仍然是原本用户权限 。 那么上面nmap提权失败的原因 , 就可以排除nmap的原因了 。
同样 , CentOS6和Debian6都是较老的发行版 , 但CentOS6的表现却和新版Ubuntu类似 , 经过网上的询问和翻文档 , 得到了bash中的这么一段说明:
Iftheshellisstartedwiththeeffectiveuser(group)idnotequaltotherealuser(group)id,andthe-poptionisnotsupplied,nostartupfilesareread,shellfunctionsarenotinheritedfromtheenvironment,theSHELLOPTS,BASHOPTS,CDPATH,andGLOBIGNOREvariables,iftheyappearintheenvironment,areignored,andtheeffectiveuseridissettotherealuserid.Ifthe-poptionissuppliedatinvocation,thestartupbehavioristhesame,buttheeffectiveuseridisnotreset.
如果启动bash时的EffectiveUID与RealUID不相同 , 而且没有使用-p参数 , 则bash会将EffectiveUID还原成RealUID 。
我们知道 , Linux的system()函数实际上是执行的/bin/sh-c , 而CentOS的/bin/sh是指向了/bin/bash:
[root@localhosttmp]#ls-al/bin/shlrwxrwxrwx.1rootroot4Apr102017/bin/sh->bash
这就解释了为什么CentOS中suid程序执行id获得的结果仍然是www-data 。 假设我们此时将sh修改成dash , 看看结果是什么:
[root@localhosttmp]#su-s/bin/bashnobodybash-4.1$ls-al/bin/shlrwxrwxrwx.1rootroot9Feb1900:21/bin/sh->/bin/dashbash-4.1$./suididuid=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并没有限制EffectiveUID , 这里可以看到成功获取了root权限 。
Ubuntu的特殊处理但是 , 我们来看看Ubuntu16.04 , 其/bin/sh指向的同样是dash:
$ls-al/bin/shlrwxrwxrwx1rootroot49月182016/bin/sh->dash$ls-al/bin/dash-rwxr-xr-x1rootroot1540722月172016/bin/dash
为什么仍然会出现无法提权的情况?
此时我们又需要了解另一个知识了 。 通常来说 , 类似Ubuntu这样的发行版都会对一些程序进行修改 , 比如我们平时在查看PHP版本的时候 , 经常会看到这样的banner:PHP7.0.33-0ubuntu0.16.04.11 , 在官方的版本号后会带上Ubuntu的一些版本号 , 这是因为Ubuntu发行版在打包这些软件时会增加一些自己的代码 。
那么我们可以来看看Ubuntu16.04源中dash目录:
我们对原始代码进行patch后 , 会发现多了一个setprivileged函数:
voidsetprivileged(inton){staticintis_privileged=1;if(is_privileged==on)return;is_privileged=on;/**Tolimitbogussystem(3)orpopen(3)callsinsetuidbinaries,require*-pflagtoworkinthissituation.*/if(!on&&(uid!=geteuid()||gid!=getegid())){setuid(uid);setgid(gid);/*PS1mightneedtobechangedaccordingly.*/choose_ps1();}}
on的取值取决于用户是否传入了-p参数 , 而uid和gid就是当前进程的RealUID(GID) 。 可见 , 在on为false , 且RealUID不等于EffectiveUID的情况下 , 这里重新设置了进程的UID:
setuid(uid)
setuid函数用于设置当前进程的EffectiveUID , 如果当前进程是root权限或拥有CAP_SETUID权限 , 则RealUID和SavedUID将被一起设置 。
所以 , 可以看出 , Ubuntu发行版官方对dash进行了修改:当dash以suid权限运行、且没有指定-p选项时 , 将会丢弃suid权限 , 恢复当前用户权限 。
这样一来 , dash在suid的表现上就和bash相同了 , 这也就解释了为什么在Ubuntu16.04以后 , 我们无法直接使用SUID+system()的方式来提权 。
如何突破限制?同样的 , 你下载Debian10最新的dash , 也可以看到类似代码 。 那么 , 为什么各大发行版分分在sh中增加了这个限制呢?
我们可以将其理解为是Linux针对suid提权方式的一种遏制 。 因为通常来说 , 很多命令注入漏洞都是发生在system()和popen()函数中的 , 而这些函数依赖于系统的/bin/sh 。 相比CentOS来说 , Ubuntu和Debian中的sh一直都是dash , 也就一直受到suid提权漏洞的影响 。
一旦拥有suid的程序存在命令注入漏洞或其本身存在执行命令的功能 , 那么就有本地提权的风险 , 如果在sh中增加这个限制 , 提权的隐患就能被极大地遏制 。
那么 , 如果我们就是要留一个具有suid的shell作为后门 , 我们应该怎么做?
将之前的suid.c做如下修改:
intmain(intargc,char*argv[]){setuid(0);system(argv[1]);}
编译和执行 , 我们就可以发现 , id命令输出的uid就是0了:
另一种方法 , 我们可以给dash或bash增加-p选项 , 让其不对shell降权 。 但这里要注意 , 我们不能再使用system函数了 , 因为system()内部执行的是/bin/sh-c , 我们只能控制-c的参数值 , 无法给sh中增加-p选项 。
这里我们可以使用execl或其他exec系列函数:
intmain(intargc,char*argv[]){returnexecl("/bin/sh","sh","-p","-c",argv[1],(char*)0);}
此时输出结果类似于Ubuntu14.04里的结果 , 因为我给sh加了-p参数:
因为nmapscript中使用的是lua语言 , 而lua库中似乎没有直接启动进程的方式 , 都会依赖系统shell , 所以我们可能并不能直接通过执行shell的方式来提权 。 但是因为此时nmap已经是root权限 , 我们可以通过修改/etc/passwd的方式来添加一个新的superuser:
localfile=io.open("/etc/passwd","a")file:write("root2::0:0::/root:/bin/bashn")file:close()
成功提权:
当然我们需要先感谢Linux内核和Ubuntu和Debian等发行版的开发人员 , 他们也在慢慢帮我们不断提高系统的安全性和稳定性 , 但类似于nmap这样功能强大的软件 , 我们无法奢求一律SecureByDefault , 所以必须学习一些更有趣的知识 。
Linux2.2以后增加了capabilities的概念 , 可以理解为水平权限的分离 。 以往如果需要某个程序的某个功能需要特权 , 我们就只能使用root来执行或者给其增加SUID权限 , 一旦这样 , 我们等于赋予了这个程序所有的特权 , 这是不满足权限最小化的要求的;在引入capabilities后 , root的权限被分隔成很多子权限 , 这就避免了滥用特权的问题 , 我们可以在capabilities(7)-Linuxmanualpage中看到这些特权的说明 。
类似于ping和nmap这样的程序 , 他们其实只需要网络相关的特权即可 。 所以 , 如果你在Kali下查看ping命令的capabilities , 你会看到一个cap_net_raw:
$ls-al/bin/ping-rwxr-xr-x1rootroot73496Oct522:34/bin/ping$getcap/bin/ping/bin/ping=cap_net_raw+ep
这就是为什么kali的ping命令无需设置setuid权限 , 却仍然可以以普通用户身份运行的原因 。
同样 , 我们也可以给nmap增加类似的capabilities:
sudosetcapcap_net_raw,cap_net_admin,cap_net_bind_service+eip/usr/bin/nmapnmap--privileged-sS192.168.1.1
再次使用TCPSYN扫描时就不会出现权限错误的情况了:


    推荐阅读