diff --git a/README.md b/README.md new file mode 100644 index 0000000..09ede5f --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# Vex + +--------------- + +Autonomous RouterOS configuration analyzer to find security issues. No networking required, only read configurations. + +![](/banner/banner.png) + +``` +Vex: RouterOS Security Inspector +Designed for security engineers + +Author: Magama Bazarov, +Pseudonym: Caster +Version: 1.0 +``` + +# Disclaimer + +The tool is intended solely for analyzing the security of RouterOS hardware. The author is not responsible for any damage caused by using this tool + +------------- +# Operating + +It is written in Python 3 and its work is based on looking for certain elements in configurations that may indicate RouterOS network security issues. The search for suspicious elements is performed using regular expressions. + +Vex performs 23 search steps, these include: + +``` +1. Discovery Protocols Check: Checks whether discovery protocols (such as LLDP) are enabled on all interfaces; +2. Bandwidth Server Check: Checks whether the Bandwidth Server is enabled; +3. DNS Settings Check: Checks whether remote DNS queries are allowed; +4. DDNS Settings Check: Checks whether Dynamic Domain Name System (DDNS) is enabled; +5. UPnP Settings Check: Checks if UPnP (Universal Plug and Play) is enabled; +6. SSH Settings Check: Checks whether cryptographic settings for SSH are enabled; +7. Firewall Filter Rules Check: Retrieves and displays firewall filter rules; +8. Firewall Mangle Rules Check: Retrieves and displays firewall mangle rules; +9. Firewall NAT Rules Check: Retrieves and displays firewall NAT rules; +10. Firewall Raw Rules Check: Retrieves and displays Raw firewall rules; +11. Routes Check: Retrieves and displays routes; +12. SOCKS Settings Check: Checks if the SOCKS proxy is enabled; +13. IP Services Check: Checks the status of various IP services (Telnet, FTP, API, API-SSL, SSH, Winbox, HTTP, HTTPS); +14. BPDU Guard Settings Check: Checks the BPDU Guard settings for STP protection; +15. ROMON Settings Check: Checks if ROMON is enabled; +16. MAC Telnet Server Check: Checks the MAC Telnet Server settings; +17. MAC Winbox Server Check: Checks the MAC Winbox Server settings; +18. MAC Ping Server Check: Checks the MAC Ping Server settings; +19. DHCP Snooping Settings Check: Checks the DHCP Snooping settings to protect against DHCP attacks; +20. NTP Client Settings Check: Checks the NTP client settings; +21. VRRP Security Check: Checks the VRRP authentication settings; +22. OSPF Security Check: Checks OSPF settings for authentication and passive interfaces; +23. SNMP Security Check: Checks SNMP community settings for insecure values; +``` + +The tool will not only help can help improve the security of the device, but also help improve the quality of hardening. + +> Warning: For a complete RouterOS check, it is recommended to export the configuration using `export verbose` to unload the entire configuration + +-------- + +# Usage + +```bash +caster@kali:~$ sudo apt install python3-colorama +caster@kali:~$ git clone https://github.com/casterbyte/Vex +caster@kali:~$ cd Vex/ +caster@kali:~/Vex$ python3 vex.py --help +``` + +``` +usage: vex.py [-h] --config CONFIG + +options: + -h, --help show this help message and exit + --config CONFIG RouterOS configuration file name +``` + +To perform a configuration analysis, you must supply the RouterOS configuration file as input. This is done with the `--config` argument: + +```bash +caster@kali:~/Vex$ python3 vex.py --config RouterOS.conf +``` + +Here is an example of the analyzed config: + +```bash +[+] Device Information: +[*] Software ID: 7HD9-Z1QD +[*] Model: C52iG-5HaxD2HaxD +[*] Serial Number: HEB08WY6MPT +------------------------------ +[+] Interfaces found: +[*] Type: bridge, Name: home +[*] Type: ethernet, Name: ether1 +[*] Type: ethernet, Name: ether2 +[*] Type: ethernet, Name: ether3 +[*] Type: ethernet, Name: ether4 +[*] Type: ethernet, Name: ether5 +[*] Type: wifiwave2, Name: wifi1 +[*] Type: wifiwave2, Name: wifi2 +[*] Type: vrrp, Name: vrrp1 +[*] Type: wireguard, Name: wg-outerspace +[*] Type: ethernet, Name: switch1 +[*] Type: list, Name: all +[*] Type: list, Name: none +[*] Type: list, Name: dynamic +[*] Type: list, Name: static +[*] Type: list, Name: LAN +[*] Type: lte, Name: default +[*] Type: macsec, Name: default +------------------------------ +[+] IP Addresses found: +[*] IP Address: 192.168.0.254/24, Interface: home +[*] IP Address: 10.10.101.71/32, Interface: wg-outerspace +[*] IP Address: 192.168.0.11/24, Interface: vrrp1 +------------------------------ +[+] Discovery Protocols Check: +[*] Security Warning: detected set discover-interface-list=all. Possible disclosure of sensitive information +------------------------------ +[+] Bandwidth Server Check: +[*] Security Warning: detected active Bandwidth Server with 'enabled=yes' setting. Possible unwanted traffic towards Bandwidth Server, be careful +------------------------------ +[+] DNS Settings Check: +[*] Security Warning: detected directive 'set allow-remote-requests=yes'. This router is a DNS server, be careful +[*] Router is acting as a DNS server and should restrict DNS traffic from external sources to prevent DNS Flood attacks +------------------------------ +[+] DDNS Settings Check: +[*] Warning: DDNS is enabled. If not specifically used, it is recommended to disable it. +------------------------------ +[+] UPnP Settings Check: +[*] Security Warning: detected directive 'set enabled=yes'. The presence of active UPnP can be indicative of post-exploitation of a compromised RouterOS, and it can also be the cause of an external perimeter breach. Switch it off +------------------------------ +[+] SSH Settings Check: +[*] Security Warning: detected 'strong-crypto=no'. It is recommended to enable strong cryptographic ciphers for SSH +------------------------------ +[+] Firewall Filter Rules found: +[*] Rule: add action=accept chain=input comment="Allow Established & Related, Drop Invalid" connection-state=established,related +[*] Rule: add action=drop chain=input connection-state=invalid +[*] Rule: add action=accept chain=forward connection-state=established,related +[*] Rule: add action=drop chain=forward connection-state=invalid +[!] Don't forget to use the 'Drop All Other' rule on the external interface of the router. This helps protect the router from external perimeter breaches. +------------------------------ +[+] Firewall Mangle Rules found: +[*] No mangle rules found. +[!] In some scenarios, using the mangle table can help save CPU resources. +------------------------------ +[+] Firewall NAT Rules found: +[*] Rule: add action=masquerade chain=srcnat comment="Access to Internet" out-interface=wg-outerspace +------------------------------ +[+] Firewall Raw Rules found: +[*] No raw rules found. +------------------------------ +[+] Routes: +[*] Route: add distance=1 dst-address=111.111.111.111/32 gateway=192.168.1.1 +[*] Route: add dst-address=192.168.54.0/24 gateway=192.168.0.253 +[*] Route: add dst-address=0.0.0.0/0 gateway=wg-outerspace +------------------------------ +[+] SOCKS Settings Check: +[*] Security Warning: detected directive 'set enabled=yes'. SOCKS proxy can be used as a pivoting tool to access the internal network +------------------------------ +[+] IP Services Check: +[*] Security Warning: SSH service is enabled. Filter access, you can use more secure key authentication +[*] Security Warning: API-SSL service is enabled. If not in use, it is recommended to disable it to prevent brute-force attacks +[*] Security Warning: Winbox service is enabled. Winbox is constantly being attacked. Be careful with it, filter access +[*] Security Warning: Telnet service is enabled. Turn it off, it's not safe to operate the equipment with it +[*] Security Warning: API service is enabled. If not in use, it is recommended to disable it to prevent brute-force attacks +[*] Security Warning: HTTP service is enabled. Be careful with web-based control panels. Filter access +[*] Security Warning: HTTPS service is enabled. Be careful with web-based control panels. Filter access +[*] Security Warning: FTP service is enabled. If you don't use FTP, disable it and try not to store sensitive information there +------------------------------ +[+] BPDU Guard Settings Check: +[*] Security Warning: detected 'bpdu-guard=no'. It is recommended to enable BPDU Guard to protect STP from attacks +------------------------------ +[+] ROMON Settings Check: +[*] Security Warning: ROMON is enabled. Be careful with this. If RouterOS is compromised, ROMON can be jumped to the next MikroTik hardware +------------------------------ +[+] MAC Telnet Server Check: +[*] Security Warning: MAC Telnet server is active on all interfaces. This reduces the security of the Winbox interface. Filter access +------------------------------ +[+] MAC Winbox Server Check: +[*] Security Warning: MAC Winbox Server is accessible on all interfaces. This reduces the security of the Winbox interface. Filter access +------------------------------ +[+] MAC Ping Server Check: +[*] Security Warning: MAC Ping Server is enabled. Possible unwanted traffic +------------------------------ +[+] DHCP Snooping Settings Check: +[*] Security Warning: detected 'dhcp-snooping=no'. It is recommended to enable DHCP Snooping to protect the network from DHCP attacks (DHCP Spoofing) +------------------------------ +[+] NTP Client Settings Check: +[*] Security Warning: NTP client is enabled. Servers: 0.pool.ntp.org, 1.pool.ntp.org +------------------------------ +[+] VRRP Security Check: +[*] No issues found with VRRP authentication settings +------------------------------ +[+] OSPF Security Check: +[*] Security Warning: OSPF authentication is not configured. There is a risk of connecting an illegal OSPF speaker +[*] Security Warning: OSPF passive interfaces are not configured. There is a risk of connecting an illegal OSPF speaker +------------------------------ +[+] SNMP Security Check: +[*] Security Warning: SNMP community 'public' is set. Information Disclosure is possible. Please change SNMP community string +[*] Security Warning: SNMP community 'private' is set. Information Disclosure is possible. Please change SNMP community string +------------------------------ +``` + + + + + + + + + diff --git a/banner/banner.png b/banner/banner.png new file mode 100644 index 0000000..88e47a0 Binary files /dev/null and b/banner/banner.png differ diff --git a/vex.py b/vex.py new file mode 100644 index 0000000..f7c10ba --- /dev/null +++ b/vex.py @@ -0,0 +1,805 @@ +#!/usr/bin/env python3 + +import argparse +import re +from colorama import init, Fore, Style + +init(autoreset=True) + +def parse_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument('--config', required=True, help='RouterOS configuration file name') + return parser.parse_args() + +def load_config(file_path): + with open(file_path, 'r') as file: + return file.read().splitlines() + +def combine_multiline_statements(config_lines): + combined_lines = [] + buffer = "" + for line in config_lines: + line = line.strip() + if line.endswith("\\"): + buffer += line[:-1] + " " + else: + buffer += line + combined_lines.append(buffer) + buffer = "" + return combined_lines + +def extract_device_info(config_lines): + software_id = None + model = None + serial_number = None + version = None + + for line in config_lines: + if line.startswith("# software id ="): + software_id = line.split('=')[1].strip() + elif line.startswith("# model ="): + model = line.split('=')[1].strip() + elif line.startswith("# serial number ="): + serial_number = line.split('=')[1].strip() + + return software_id, model, serial_number + +def extract_interfaces(config_lines): + interfaces = [] + current_interface_type = None + + for line in config_lines: + line = line.strip() + if line.startswith('/interface '): + current_interface_type = line.split()[1] + continue + + if line.startswith('/') and not line.startswith('/interface'): + current_interface_type = None + + if current_interface_type: + if line.startswith('set ') or line.startswith('add '): + name_match = re.search(r'name=(\S+)', line) + default_name_match = re.search(r'default-name=(\S+)', line) + if name_match: + interface_name = name_match.group(1) + interfaces.append((current_interface_type, interface_name)) + elif default_name_match: + interface_name = default_name_match.group(1) + interfaces.append((current_interface_type, interface_name)) + + return interfaces + +def extract_ip_addresses(config_lines): + ip_addresses = [] + ip_pattern = re.compile(r'^/ip address') + add_pattern = re.compile(r'add address=([\d\.\/]+)(?: disabled=\S+)? interface=(\S+) network=\S+') + + inside_ip_address_block = False + + for line in config_lines: + if ip_pattern.match(line): + inside_ip_address_block = True + continue + + if inside_ip_address_block and line.startswith("add "): + add_match = add_pattern.search(line) + if add_match: + ip_address = add_match.group(1) + interface_name = add_match.group(2) + ip_addresses.append((ip_address, interface_name)) + + return ip_addresses + +def check_discovery_protocols(config_lines): + discovery_pattern = re.compile(r'^/ip neighbor discovery-settings') + set_pattern = re.compile(r'set discover-interface-list=(\S+)') + + for line in config_lines: + if discovery_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + if next_line_index < len(config_lines): + next_line = config_lines[next_line_index] + set_match = set_pattern.search(next_line) + if set_match: + discovery_setting = set_match.group(1) + if discovery_setting.lower() == 'all': + return (True, f"detected set discover-interface-list={discovery_setting}") + return (False, "No security issues found with Discovery protocols.") + +def check_bandwidth_server(config_lines): + bandwidth_pattern = re.compile(r'^/tool bandwidth-server') + set_pattern_enabled = re.compile(r'set .*enabled=yes') + set_pattern_disabled = re.compile(r'set .*enabled=no') + + inside_bandwidth_block = False + for line in config_lines: + if bandwidth_pattern.match(line): + inside_bandwidth_block = True + continue + + if inside_bandwidth_block: + if set_pattern_disabled.search(line): + return (False, "No issues found with Bandwidth Server.") + elif set_pattern_enabled.search(line): + return (True, "detected active Bandwidth Server with 'enabled=yes' setting") + + return (True, "detected active Bandwidth Server (default enabled)") + +def check_dns_settings(config_lines): + dns_pattern = re.compile(r'^/ip dns') + set_pattern = re.compile(r'set allow-remote-requests=yes') + + for line in config_lines: + if dns_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + if next_line_index < len(config_lines): + next_line = config_lines[next_line_index] + if set_pattern.search(next_line): + return (True, "detected directive 'set allow-remote-requests=yes'") + return (False, "No issues found with DNS settings.") + +def check_ddns(config_lines): + ddns_pattern = re.compile(r'^/ip cloud') + set_pattern = re.compile(r'set ddns-enabled=yes') + + for line in config_lines: + if ddns_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + if next_line_index < len(config_lines): + next_line = config_lines[next_line_index] + if set_pattern.search(next_line): + return (True, "DDNS is enabled. If not specifically used, it is recommended to disable it.") + return (False, "No issues found with DDNS settings.") + +def check_upnp_settings(config_lines): + upnp_pattern = re.compile(r'^/ip upnp') + set_pattern = re.compile(r'set .*enabled=(yes|no)') + + inside_upnp_block = False + + for line in config_lines: + if upnp_pattern.match(line): + inside_upnp_block = True + continue + + if inside_upnp_block: + set_match = set_pattern.search(line) + if set_match: + upnp_status = set_match.group(1) + if upnp_status == "yes": + return (True, "detected directive 'set enabled=yes'") + inside_upnp_block = False + + return (False, "No issues found with UPnP settings.") + +def extract_firewall_rules(config_lines, table): + firewall_rules = [] + firewall_pattern = re.compile(rf'^/ip firewall {table}') + add_pattern = re.compile(r'add .*') + + inside_firewall_block = False + + for line in config_lines: + if firewall_pattern.match(line): + inside_firewall_block = True + continue + + if inside_firewall_block: + if line.startswith("add "): + firewall_rules.append(line) + else: + inside_firewall_block = False + + return firewall_rules + +def extract_nat_rules(config_lines): + nat_rules = [] + nat_pattern = re.compile(r'^/ip firewall nat') + add_pattern = re.compile(r'add .*') + + inside_nat_block = False + + for line in config_lines: + if nat_pattern.match(line): + inside_nat_block = True + continue + + if inside_nat_block: + if line.startswith("add "): + nat_rules.append(line) + else: + inside_nat_block = False + + return nat_rules + +def check_bpdu_guard(config_lines): + bridge_port_pattern = re.compile(r'^/interface bridge port') + bpdu_guard_pattern = re.compile(r'add .*bpdu-guard=no') + + for line in config_lines: + if bridge_port_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + while next_line_index < len(config_lines) and config_lines[next_line_index].startswith('add '): + if bpdu_guard_pattern.search(config_lines[next_line_index]): + return (True, "detected 'bpdu-guard=no'. It is recommended to enable BPDU Guard to protect STP from attacks") + next_line_index += 1 + return (False, "No issues found with BPDU Guard settings.") + +def check_ssh_settings(config_lines): + ssh_pattern = re.compile(r'^/ip ssh') + strong_crypto_pattern = re.compile(r'set .*strong-crypto=no') + + for line in config_lines: + if ssh_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + if next_line_index < len(config_lines): + next_line = config_lines[next_line_index] + if strong_crypto_pattern.search(next_line): + return (True, "detected 'strong-crypto=no'. It is recommended to enable strong cryptographic ciphers for SSH") + return (False, "No issues found with SSH settings.") + + +def check_dhcp_snooping(config_lines): + bridge_pattern = re.compile(r'^/interface bridge') + dhcp_snooping_pattern = re.compile(r'add .*dhcp-snooping=no') + + for line in config_lines: + if bridge_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + while next_line_index < len(config_lines) and config_lines[next_line_index].startswith('add '): + if dhcp_snooping_pattern.search(config_lines[next_line_index]): + return (True, "detected 'dhcp-snooping=no'. It is recommended to enable DHCP Snooping to protect the network from DHCP attacks (DHCP Spoofing)") + next_line_index += 1 + return (False, "No issues found with DHCP Snooping settings.") + +def extract_routes(config_lines): + routes = [] + route_pattern = re.compile(r'^/ip route') + add_pattern = re.compile(r'add .*') + + inside_route_block = False + + for line in config_lines: + if route_pattern.match(line): + inside_route_block = True + continue + + if inside_route_block: + if line.startswith("add "): + routes.append(line) + else: + inside_route_block = False + + return routes + +def check_socks_settings(config_lines): + socks_pattern = re.compile(r'^/ip socks') + set_pattern = re.compile(r'set .*enabled=yes') + + for line in config_lines: + if socks_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + if next_line_index < len(config_lines): + next_line = config_lines[next_line_index] + if set_pattern.search(next_line): + return (True, "detected directive 'set enabled=yes'. SOCKS proxy can be used as a pivoting tool to access the internal network") + return (False, "No issues found with SOCKS settings.") + +def check_vrrp_authentication(config_lines): + vrrp_pattern = re.compile(r'^/interface vrrp') + auth_pattern = re.compile(r'authentication=none') + + for line in config_lines: + if vrrp_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + if next_line_index < len(config_lines): + next_line = config_lines[next_line_index] + if auth_pattern.search(next_line): + return (True, "VRRP authentication is set to 'none'. This poses a risk of VRRP Hijacking.", "It's recommended to set the maximum priority to 255 if possible. If using VRRPv3, use FW to filter traffic towards MCAST 224.0.0.18") + return (False, "No issues found with VRRP authentication settings", None) + +def check_ospf_authentication(config_lines): + ospf_pattern = re.compile(r'^/routing ospf interface-template') + auth_pattern = re.compile(r'auth=') + + inside_ospf_block = False + + for line in config_lines: + if ospf_pattern.match(line): + inside_ospf_block = True + continue + + if inside_ospf_block: + if 'add ' in line: + if auth_pattern.search(line) is None: + return (True, "OSPF authentication is not configured. This poses a security risk.") + inside_ospf_block = False + + return (False, "No issues found with OSPF authentication settings.") + +def check_ospf_passive_setting(config_lines): + ospf_pattern = re.compile(r'^/routing ospf interface-template') + auth_pattern = re.compile(r'auth=') + passive_pattern = re.compile(r'passive') + + inside_ospf_block = False + auth_missing = False + passive_missing = False + + for line in config_lines: + if ospf_pattern.match(line): + inside_ospf_block = True + continue + + if inside_ospf_block: + if 'add ' in line: + if auth_pattern.search(line) is None: + auth_missing = True + if passive_pattern.search(line) is None: + passive_missing = True + inside_ospf_block = False + + return auth_missing, passive_missing + +def check_ip_services(config_lines): + services_pattern = re.compile(r'^/ip service') + telnet_pattern = re.compile(r'set telnet .*disabled=(yes|no)') + ftp_pattern = re.compile(r'set ftp .*disabled=(yes|no)') + api_pattern = re.compile(r'set api .*disabled=(yes|no)') + api_ssl_pattern = re.compile(r'set api-ssl .*disabled=(yes|no)') + ssh_pattern = re.compile(r'set ssh .*disabled=(yes|no)') + winbox_pattern = re.compile(r'set winbox .*disabled=(yes|no)') + www_pattern = re.compile(r'set www .*disabled=(yes|no)') + www_ssl_pattern = re.compile(r'set www-ssl .*disabled=(yes|no)') + + warnings = set() + info = set() + + inside_services_block = False + + for line in config_lines: + if services_pattern.match(line): + inside_services_block = True + continue + + if inside_services_block: + if telnet_pattern.search(line): + telnet_disabled = telnet_pattern.search(line).group(1) + if telnet_disabled == "no": + warnings.add((True, "Telnet service is enabled. Turn it off, it's not safe to operate the equipment with it")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "Telnet service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + if ftp_pattern.search(line): + ftp_disabled = ftp_pattern.search(line).group(1) + if ftp_disabled == "no": + warnings.add((True, "FTP service is enabled. If you don't use FTP, disable it and try not to store sensitive information there")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "FTP service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + if api_pattern.search(line): + api_disabled = api_pattern.search(line).group(1) + if api_disabled == "no": + warnings.add((True, "API service is enabled. If not in use, it is recommended to disable it to prevent brute-force attacks")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "API service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + if api_ssl_pattern.search(line): + api_ssl_disabled = api_ssl_pattern.search(line).group(1) + if api_ssl_disabled == "no": + warnings.add((True, "API-SSL service is enabled. If not in use, it is recommended to disable it to prevent brute-force attacks")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "API-SSL service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + if ssh_pattern.search(line): + ssh_disabled = ssh_pattern.search(line).group(1) + if ssh_disabled == "no": + warnings.add((True, "SSH service is enabled. Filter access, you can use more secure key authentication")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "SSH service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + if winbox_pattern.search(line): + winbox_disabled = winbox_pattern.search(line).group(1) + if winbox_disabled == "no": + warnings.add((True, "Winbox service is enabled. Winbox is constantly being attacked. Be careful with it, filter access")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "Winbox service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + if www_pattern.search(line): + www_disabled = www_pattern.search(line).group(1) + if www_disabled == "no": + warnings.add((True, "HTTP service is enabled. Be careful with web-based control panels. Filter access")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "HTTP service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + if www_ssl_pattern.search(line): + www_ssl_disabled = www_ssl_pattern.search(line).group(1) + if www_ssl_disabled == "no": + warnings.add((True, "HTTPS service is enabled. Be careful with web-based control panels. Filter access")) + else: + info.add((False, Fore.YELLOW + Style.BRIGHT + "HTTPS service is " + Fore.WHITE + Style.BRIGHT + "disabled")) + + return list(warnings), list(info) + +def check_ntp_client(config_lines): + ntp_client_pattern = re.compile(r'^/system ntp client') + set_pattern = re.compile(r'set enabled=yes mode=unicast servers=(\S+)') + + ntp_client_enabled = False + ntp_servers = [] + + for line in config_lines: + if ntp_client_pattern.match(line): + next_line_index = config_lines.index(line) + 1 + if next_line_index < len(config_lines): + next_line = config_lines[next_line_index] + set_match = set_pattern.search(next_line) + if set_match: + ntp_client_enabled = True + ntp_servers = set_match.group(1).split(',') + + if ntp_client_enabled: + return (True, f"NTP client is enabled. Servers: {', '.join(ntp_servers)}") + else: + return (False, "NTP client is not enabled or not using unicast mode.") + +def check_romon_settings(config_lines): + romon_pattern = re.compile(r'^/tool romon') + set_pattern = re.compile(r'set .*enabled=yes') + + inside_romon_block = False + + for line in config_lines: + if romon_pattern.match(line): + inside_romon_block = True + continue + + if inside_romon_block: + if set_pattern.search(line): + return (True, "ROMON is enabled. Be careful with this. If RouterOS is compromised, ROMON can be jumped to the next MikroTik hardware") + inside_romon_block = False + + return (False, "No issues found with ROMON settings.") + +def check_mac_telnet_server(config_lines): + mac_server_pattern = re.compile(r'^/tool mac-server') + allowed_interface_list_pattern = re.compile(r'set allowed-interface-list=all') + + inside_mac_server_block = False + + for line in config_lines: + if mac_server_pattern.match(line): + inside_mac_server_block = True + continue + + if inside_mac_server_block: + if allowed_interface_list_pattern.search(line): + return (True, "MAC Telnet server is active on all interfaces. This reduces the security of the Winbox interface. Filter access") + inside_mac_server_block = False + + return (False, "No issues found with MAC Telnet Server settings.") + +def check_mac_winbox_server(config_lines): + mac_winbox_pattern = re.compile(r'^/tool mac-server mac-winbox') + inside_mac_winbox_block = False + + for line in config_lines: + if mac_winbox_pattern.match(line): + inside_mac_winbox_block = True + continue + + if inside_mac_winbox_block: + if 'set allowed-interface-list=all' in line: + return (True, "MAC Winbox Server is accessible on all interfaces. This reduces the security of the Winbox interface. Filter access") + inside_mac_winbox_block = False + + return (False, "No issues found with MAC Winbox Server settings.") + +def check_mac_ping_server(config_lines): + mac_ping_pattern = re.compile(r'^/tool mac-server ping') + inside_mac_ping_block = False + + for line in config_lines: + if mac_ping_pattern.match(line): + inside_mac_ping_block = True + continue + + if inside_mac_ping_block: + if 'set enabled=yes' in line: + return (True, "MAC Ping Server is enabled. Possible unwanted traffic") + inside_mac_ping_block = False + + return (False, "No issues found with MAC Ping Server settings.") + +def check_snmp_communities(config_lines): + snmp_pattern = re.compile(r'^/snmp community') + name_pattern = re.compile(r'name=(\S+)') + + issues_found = False + snmp_issues = [] + + inside_snmp_block = False + + for line in config_lines: + if snmp_pattern.match(line): + inside_snmp_block = True + continue + + if inside_snmp_block: + name_match = name_pattern.search(line) + if name_match: + snmp_name = name_match.group(1) + if snmp_name.lower() in ["public", "private"]: + issues_found = True + snmp_issues.append(f"SNMP community '{snmp_name}' is set. Information Disclosure is possible. Please change SNMP community string") + + if issues_found: + return (True, snmp_issues) + else: + return (False, "No issues found with SNMP settings.") + +if __name__ == "__main__": + banner = ''' + VVVVVVVV VVVVVVVV + V::::::V V::::::V + V::::::V V::::::V + V::::::V V::::::V + V:::::V V:::::Veeeeeeeeeeee xxxxxxx xxxxxxx + V:::::V V:::::ee::::::::::::eex:::::x x:::::x + V:::::V V:::::e::::::eeeee:::::ex:::::x x:::::x + V:::::V V:::::e::::::e e:::::ex:::::xx:::::x + V:::::V V:::::Ve:::::::eeeee::::::e x::::::::::x + V:::::V V:::::V e:::::::::::::::::e x::::::::x + V:::::V:::::V e::::::eeeeeeeeeee x::::::::x + V:::::::::V e:::::::e x::::::::::x + V:::::::V e::::::::e x:::::xx:::::x + V:::::V e::::::::eeeeeeee x:::::x x:::::x + V:::V ee:::::::::::::ex:::::x x:::::x + VVV eeeeeeeeeeeeexxxxxxx xxxxxxx +''' + + print(banner) + print(" Vex: RouterOS Security Inspector") + print(" Designed for security engineers\n") + print(" For documentation visit: https://github.com/casterbyte/Vex\n") + print(" " + Fore.YELLOW + "Author: " + Style.RESET_ALL + "Magama Bazarov, ") + print(" " + Fore.YELLOW + "Pseudonym: " + Style.RESET_ALL + "Caster") + print(" " + Fore.YELLOW + "Version: " + Style.RESET_ALL + "1.0\n") + print(" " + Fore.YELLOW + Style.BRIGHT + "DISCLAIMER: The tool is intended solely for analyzing the security of RouterOS hardware. The author is not responsible for any damage caused by using this tool") + print(" " + Fore.YELLOW + Style.BRIGHT + "CAUTION: for the tool to work correctly, use the RouterOS configuration from using the" + Fore.WHITE + Style.BRIGHT + " export verbose command\n") + + + args = parse_arguments() + config_lines = load_config(args.config) + config_lines = combine_multiline_statements(config_lines) + + software_id, model, serial_number = extract_device_info(config_lines) + print(Fore.WHITE + Style.BRIGHT + "[+] Device Information:" + Style.RESET_ALL) + if software_id: + print(Fore.YELLOW + Style.BRIGHT + "[*] Software ID: " + Fore.WHITE + Style.BRIGHT + f"{software_id}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] Software ID: " + Fore.WHITE + Style.BRIGHT + "unknown") + if model: + print(Fore.YELLOW + Style.BRIGHT + "[*] Model: " + Fore.WHITE + Style.BRIGHT + f"{model}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] Model: " + Fore.WHITE + Style.BRIGHT + "unknown") + if serial_number: + print(Fore.YELLOW + Style.BRIGHT + "[*] Serial Number: " + Fore.WHITE + Style.BRIGHT + f"{serial_number}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] Serial Number: " + Fore.WHITE + Style.BRIGHT + "unknown") + print("------------------------------") + + interfaces = extract_interfaces(config_lines) + print(Fore.WHITE + Style.BRIGHT + "[+] Interfaces found:" + Style.RESET_ALL) + if interfaces: + for interface_type, interface_name in interfaces: + print(Fore.YELLOW + Style.BRIGHT + "[*] Type: " + Fore.WHITE + Style.BRIGHT + f"{interface_type}, " + Fore.YELLOW + Style.BRIGHT + "Name: " + Fore.WHITE + Style.BRIGHT + f"{interface_name}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] No interfaces found.") + print("------------------------------") + + ip_addresses = extract_ip_addresses(config_lines) + print(Fore.WHITE + Style.BRIGHT + "[+] IP Addresses found:" + Style.RESET_ALL) + if ip_addresses: + for ip_address, interface_name in ip_addresses: + print(Fore.YELLOW + Style.BRIGHT + "[*] IP Address: " + Fore.WHITE + Style.BRIGHT + f"{ip_address}, " + Fore.YELLOW + Style.BRIGHT + "Interface: " + Fore.WHITE + Style.BRIGHT + f"{interface_name}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] No IP addresses found.") + print("------------------------------") + + discovery_protocol_status, discovery_protocol_message = check_discovery_protocols(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] Discovery Protocols Check:" + Style.RESET_ALL) + if discovery_protocol_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{discovery_protocol_message}. Possible disclosure of sensitive information") + else: + print(f"[*] {discovery_protocol_message}") + print("------------------------------") + + bandwidth_server_status, bandwidth_server_message = check_bandwidth_server(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] Bandwidth Server Check:" + Style.RESET_ALL) + if bandwidth_server_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{bandwidth_server_message}. Possible unwanted traffic towards Bandwidth Server, be careful") + else: + print(f"[*] {bandwidth_server_message}") + print("------------------------------") + + dns_settings_status, dns_settings_message = check_dns_settings(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] DNS Settings Check:" + Style.RESET_ALL) + if dns_settings_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{dns_settings_message}. This router is a DNS server, be careful") + print(Fore.YELLOW + Style.BRIGHT + "[*] Router is acting as a DNS server and should restrict DNS traffic from external sources to prevent DNS Flood attacks") + else: + print(f"[*] {dns_settings_message}") + print("------------------------------") + + ddns_status, ddns_message = check_ddns(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] DDNS Settings Check:" + Style.RESET_ALL) + if ddns_status: + print(Fore.YELLOW + Style.BRIGHT + f"[*] Warning: " + Fore.WHITE + Style.BRIGHT + f"{ddns_message}") + else: + print(f"[*] {ddns_message}") + print("------------------------------") + + upnp_settings_status, upnp_settings_message = check_upnp_settings(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] UPnP Settings Check:" + Style.RESET_ALL) + if upnp_settings_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{upnp_settings_message}. The presence of active UPnP can be indicative of post-exploitation of a compromised RouterOS, and it can also be the cause of an external perimeter breach. Switch it off") + else: + print(f"[*] {upnp_settings_message}") + print("------------------------------") + + ssh_status, ssh_message = check_ssh_settings(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] SSH Settings Check:" + Style.RESET_ALL) + if ssh_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{ssh_message}") + else: + print(f"[*] {ssh_message}") + print("------------------------------") + + filter_rules = extract_firewall_rules(config_lines, 'filter') + print(Fore.GREEN + Style.BRIGHT + "[+] Firewall Filter Rules found:" + Style.RESET_ALL) + if filter_rules: + for rule in filter_rules: + print(Fore.YELLOW + Style.BRIGHT + "[*] Rule:" + Fore.WHITE + Style.BRIGHT + f" {rule}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] No filter rules found.") + print(Style.BRIGHT + Fore.YELLOW + "[!] Don't forget to use the 'Drop All Other' rule on the external interface of the router. This helps protect the router from external perimeter breaches.") + print("------------------------------") + + mangle_rules = extract_firewall_rules(config_lines, 'mangle') + print(Fore.GREEN + Style.BRIGHT + "[+] Firewall Mangle Rules found:" + Style.RESET_ALL) + if mangle_rules: + for rule in mangle_rules: + print(Fore.YELLOW + Style.BRIGHT + "[*] Rule:" + Fore.WHITE + Style.BRIGHT + f" {rule}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] No mangle rules found.") + print(Style.BRIGHT + Fore.YELLOW + "[!] In some scenarios, using the mangle table can help save CPU resources.") + print("------------------------------") + + nat_rules = extract_nat_rules(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] Firewall NAT Rules found:" + Style.RESET_ALL) + if nat_rules: + for rule in nat_rules: + print(Fore.YELLOW + Style.BRIGHT + "[*] Rule:" + Fore.WHITE + Style.BRIGHT + f" {rule}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] No NAT rules found.") + print("------------------------------") + + raw_rules = extract_firewall_rules(config_lines, 'raw') + print(Fore.GREEN + Style.BRIGHT + "[+] Firewall Raw Rules found:" + Style.RESET_ALL) + if raw_rules: + for rule in raw_rules: + print(Fore.YELLOW + Style.BRIGHT + "[*] Rule:" + Fore.WHITE + Style.BRIGHT + f" {rule}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] No raw rules found.") + print("------------------------------") + + routes = extract_routes(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] Routes:" + Style.RESET_ALL) + if routes: + for route in routes: + print(Fore.YELLOW + Style.BRIGHT + f"[*] Route:" + Fore.WHITE + Style.BRIGHT + f" {route}") + else: + print(Fore.YELLOW + Style.BRIGHT + "[*] No routes found.") + print("------------------------------") + + socks_status, socks_message = check_socks_settings(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] SOCKS Settings Check:" + Style.RESET_ALL) + if socks_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{socks_message}") + else: + print(f"[*] {socks_message}") + print("------------------------------") + + ip_services_warnings, ip_services_info = check_ip_services(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] IP Services Check:" + Style.RESET_ALL) + if ip_services_warnings: + for status, message in ip_services_warnings: + if status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{message}") + if ip_services_info: + for status, message in ip_services_info: + if not status: + print(Fore.YELLOW + Style.BRIGHT + f"[*] " + message) + print("------------------------------") + + bpdu_guard_status, bpdu_guard_message = check_bpdu_guard(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] BPDU Guard Settings Check:" + Style.RESET_ALL) + if bpdu_guard_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{bpdu_guard_message}") + else: + print(f"[*] {bpdu_guard_message}") + print("------------------------------") + + romon_status, romon_message = check_romon_settings(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] ROMON Settings Check:" + Style.RESET_ALL) + if romon_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{romon_message}") + else: + print(f"[*] {romon_message}") + print("------------------------------") + + mac_telnet_status, mac_telnet_message = check_mac_telnet_server(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] MAC Telnet Server Check:" + Style.RESET_ALL) + if mac_telnet_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{mac_telnet_message}") + else: + print(f"[*] {mac_telnet_message}") + print("------------------------------") + + mac_winbox_status, mac_winbox_message = check_mac_winbox_server(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] MAC Winbox Server Check:" + Style.RESET_ALL) + if mac_winbox_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{mac_winbox_message}") + else: + print(f"[*] {mac_winbox_message}") + print("------------------------------") + + mac_ping_status, mac_ping_message = check_mac_ping_server(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] MAC Ping Server Check:" + Style.RESET_ALL) + if mac_ping_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{mac_ping_message}") + else: + print(f"[*] {mac_ping_message}") + print("------------------------------") + + dhcp_snooping_status, dhcp_snooping_message = check_dhcp_snooping(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] DHCP Snooping Settings Check:" + Style.RESET_ALL) + if dhcp_snooping_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{dhcp_snooping_message}") + else: + print(f"[*] {dhcp_snooping_message}") + print("------------------------------") + + ntp_client_status, ntp_client_message = check_ntp_client(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] NTP Client Settings Check:" + Style.RESET_ALL) + if ntp_client_status: + print(Fore.YELLOW + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{ntp_client_message}") + else: + print(f"[*] {ntp_client_message}") + print("------------------------------") + + vrrp_auth_status, vrrp_auth_message, vrrp_auth_advice = check_vrrp_authentication(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] VRRP Security Check:" + Style.RESET_ALL) + if vrrp_auth_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{vrrp_auth_message}") + if vrrp_auth_advice: + print(Fore.YELLOW + Style.BRIGHT + f"[!] Advice: {vrrp_auth_advice}") + else: + print(f"[*] {vrrp_auth_message}") + print("------------------------------") + + ospf_auth_status, ospf_passive_status = check_ospf_passive_setting(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] OSPF Security Check:" + Style.RESET_ALL) + if ospf_auth_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + "OSPF authentication is not configured. There is a risk of connecting an illegal OSPF speaker") + if ospf_passive_status: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + "OSPF passive interfaces are not configured. There is a risk of connecting an illegal OSPF speaker") + if not ospf_auth_status and not ospf_passive_status: + print(f"[*] No issues found with OSPF settings.") + print("------------------------------") + + snmp_status, snmp_message = check_snmp_communities(config_lines) + print(Fore.GREEN + Style.BRIGHT + "[+] SNMP Security Check:" + Style.RESET_ALL) + if snmp_status: + for message in snmp_message: + print(Fore.RED + Style.BRIGHT + f"[*] Security Warning: " + Fore.WHITE + Style.BRIGHT + f"{message}") + else: + print(f"[*] {snmp_message}") + print("------------------------------") + +# end of code \ No newline at end of file