2024-08-17 20:00:26 +05:00
#!/usr/bin/env python3
2025-03-02 22:35:33 +05:00
# Copyright (c) 2025 Magama Bazarov
# Licensed under the Apache 2.0 License
2025-05-29 22:01:57 +05:00
# This project is not affiliated with or endorsed by MikroTik
2025-03-02 22:35:33 +05:00
2025-05-29 22:01:57 +05:00
import argparse , colorama , time , re , sys
2025-03-02 22:35:33 +05:00
from netmiko import ConnectHandler
2024-08-17 20:00:26 +05:00
from colorama import Fore , Style
2025-03-02 22:35:33 +05:00
from packaging . version import Version
2025-05-29 22:01:57 +05:00
from cve_analyzer import run_cve_audit
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
# Initialize colorama for colored console output
2024-08-17 20:00:26 +05:00
colorama . init ( autoreset = True )
def banner ( ) :
banner_text = r """
_____
/ ____ |
| ( ___ __ _ _ __ __ _
\___ \ / _ ` | ' __/ _` |
____ ) | ( _ | | | | ( _ | |
2025-03-02 22:35:33 +05:00
| _____ / \__ , _ | _ | \__ , _ |
2024-08-17 20:00:26 +05:00
"""
2025-03-02 22:35:33 +05:00
# Display the program banner and metadata
2024-08-17 20:00:26 +05:00
print ( banner_text )
2025-05-29 22:01:57 +05:00
print ( " " + Fore . YELLOW + " RouterOS Security Inspector. Designed for security engineers " )
2025-03-28 03:22:08 +05:00
print ( " " + Fore . YELLOW + " Author: " + Style . RESET_ALL + " Magama Bazarov, <magamabazarov@mailbox.org> " )
2025-03-02 22:35:33 +05:00
print ( " " + Fore . YELLOW + " Alias: " + Style . RESET_ALL + " Caster " )
2025-05-29 22:01:57 +05:00
print ( " " + Fore . YELLOW + " Version: " + Style . RESET_ALL + " 1.2 " )
print ( " " + Fore . YELLOW + " Documentation & Usage: " + Style . RESET_ALL + " https://github.com/casterbyte/Sara \n " )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
# Display a legal disclaimer to emphasize responsible usage
print ( " " + Fore . YELLOW + " [!] DISCLAIMER: Use this tool only for auditing your own devices. " )
print ( " " + Fore . YELLOW + " [!] Unauthorized use on third-party systems is ILLEGAL. " )
print ( " " + Fore . YELLOW + " [!] The author is not responsible for misuse. " )
print ( )
# Establish SSH connection to the RouterOS device using Netmiko
2025-05-25 11:23:49 +04:00
def connect_to_router ( ip , username , password , port , key_file , passphrase ) :
2025-03-02 22:35:33 +05:00
device = {
" device_type " : " mikrotik_routeros " ,
" host " : ip ,
" username " : username ,
" password " : password ,
" port " : port ,
2025-05-25 11:05:23 +04:00
" key_file " : key_file ,
2025-05-25 11:23:49 +04:00
" passphrase " : passphrase ,
2024-08-17 20:00:26 +05:00
}
2025-03-02 22:35:33 +05:00
try :
2025-05-29 22:01:57 +05:00
print ( Fore . WHITE + f " [*] Connecting to RouterOS at { ip } : { port } " )
2025-03-02 22:35:33 +05:00
connection = ConnectHandler ( * * device )
print ( Fore . WHITE + " [*] Connection successful! " )
return connection
except Exception as e :
print ( Fore . RED + f " [-] Connection failed: { e } " )
exit ( 1 )
# Print a visual separator for better readability in the output
def separator ( title ) :
2025-05-22 23:49:12 +05:00
print ( Fore . WHITE + Style . BRIGHT + ' = ' * 50 )
2025-03-02 22:35:33 +05:00
print ( Fore . WHITE + Style . BRIGHT + f " [*] { title } " )
def parse_version ( version_str ) :
# Parses a version string into a comparable Version object. Example: "6.49.7" → Version(6.49.7)
return Version ( version_str )
2025-05-29 22:01:57 +05:00
# Retrieves the RouterOS version
2025-03-02 22:35:33 +05:00
def check_routeros_version ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking RouterOS Version " )
command = " /system resource print "
output = connection . send_command ( command )
match = re . search ( r " version: \ s*([ \ d.]+) " , output )
if match :
routeros_version = parse_version ( match . group ( 1 ) )
print ( Fore . GREEN + f " [+] Detected RouterOS Version: { routeros_version } " )
else :
print ( Fore . RED + Style . BRIGHT + " [-] ERROR: Could not determine RouterOS version. " )
# Check if SMB service is enabled (potential security risk)
def check_smb ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking SMB Service " )
command = " /ip smb print "
output = connection . send_command ( command )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
if " enabled: yes " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + " [*] CAUTION: SMB service is enabled! Did you turn it on? Do you need SMB? Also avoid CVE-2018-7445 " )
2025-03-02 22:35:33 +05:00
else :
print ( Fore . GREEN + " [+] SMB is disabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Check for high-risk remote management interfaces (RMI)
def check_rmi_services ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking RMI Services " )
command = " /ip service print "
output = connection . send_command ( command )
high_risk = [ " telnet " , " ftp " , " www " ]
moderate_risk = [ " api " , " api-ssl " , " winbox " , " www-ssl " ]
safe = [ " ssh " ]
risks_found = False
for line in output . splitlines ( ) :
line = line . strip ( )
if re . search ( r " ^ \ d+ \ s+X " , line ) :
continue
match = re . search ( r " ( \ S+) \ s+ \ d+ " , line )
2024-08-17 20:00:26 +05:00
if match :
2025-03-02 22:35:33 +05:00
service_name = match . group ( 1 ) . lower ( )
display_name = service_name . upper ( ) . replace ( " WWW " , " HTTP " ) . replace ( " WWW-SSL " , " HTTPS " )
if service_name in high_risk :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] ALERT: { display_name } is ENABLED! This is a high security risk. " )
2025-03-02 22:35:33 +05:00
if service_name == " ftp " :
print ( Fore . RED + " - Are you sure you need FTP? " )
if service_name == " telnet " :
print ( Fore . RED + " - Account passwords can be intercepted " )
if service_name == " www " :
print ( Fore . RED + " - Account passwords can be intercepted " )
risks_found = True
elif service_name in moderate_risk :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + f " [!] CAUTION: { display_name } is enabled. " )
2025-03-02 22:35:33 +05:00
if service_name in [ " api " , " api-ssl " ] :
print ( Fore . YELLOW + " - RouterOS API is vulnerable to a bruteforce attack. If you need it, make sure you have access to it. " )
elif service_name == " www-ssl " :
print ( Fore . GREEN + " - HTTPS detected. Ensure it uses a valid certificate and strong encryption. " )
elif service_name == " winbox " :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + " [!] CAUTION: If you ' re using ' Keep Password ' in Winbox, your credentials may be stored in plaintext! " )
2025-03-02 22:35:33 +05:00
print ( Fore . YELLOW + " - If your PC is compromised, attackers can extract saved credentials. " )
print ( Fore . YELLOW + " - Consider disabling ' Keep Password ' to improve security. " )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
elif service_name in safe :
print ( Fore . GREEN + f " [+] OK: { display_name } is enabled. Good! " )
print ( Fore . GREEN + " - Are you using strong passwords and SSH keys for authentication? " )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
if not risks_found :
print ( Fore . GREEN + " [+] No high-risk RMI services enabled. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
# Check for default usernames that could be security risks
def check_default_users ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking Default Usernames " )
command = " /user print detail "
output = connection . send_command ( command )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
default_users = { " admin " , " engineer " , " user " , " test " , " root " , " mikrotik " , " routeros " }
risks_found = False
for line in output . split ( " \n \n " ) :
match = re . search ( r " name= \" ?( \ w+) \" ? " , line )
if match :
username = match . group ( 1 ) . lower ( )
if username in default_users :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + f " [!] CAUTION: Default username ' { username } ' detected! Change it to a unique one. " )
2025-03-02 22:35:33 +05:00
risks_found = True
if not risks_found :
print ( Fore . GREEN + " [+] No default usernames found. " )
# Verify whether critical services have restricted network access
def checking_access_to_RMI ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking network access to RMI " )
command = " /ip service print detail "
output = connection . send_command ( command )
risks_found = False
for line in output . split ( " \n \n " ) :
service_match = re . search ( r ' name= " ([^ " ]+) " ' , line )
address_match = re . search ( r ' address=([ \ d./,]+) ' , line )
if service_match :
service_name = service_match . group ( 1 )
if address_match :
address_list = address_match . group ( 1 ) . split ( " , " )
if not address_list or address_list == [ " " ] or " 0.0.0.0/0 " in address_list :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + f " [!] CAUTION: { service_name . upper ( ) } is exposed to the entire network! Restrict access to trusted IP ranges. " )
2025-03-02 22:35:33 +05:00
risks_found = True
else :
print ( Fore . GREEN + f " [+] OK! { service_name . upper ( ) } is restricted to: { ' , ' . join ( address_list ) } " )
else :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] CAUTION: { service_name . upper ( ) } has no IP restriction set! Please restrict access. " )
2025-03-02 22:35:33 +05:00
risks_found = True
if not risks_found :
print ( Fore . GREEN + " [+] All services have proper IP restrictions. " )
# Analyze Wi-Fi security settings, including WPS and PMKID vulnerabilities
2025-05-22 23:49:12 +05:00
# I think this is the most unstable feature of the whole Sara, need more feedback from users to get it perfect
2025-03-02 22:35:33 +05:00
def check_wifi_security ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
separator ( " Checking WLAN Security " )
2025-03-02 22:35:33 +05:00
risks_found = False
try :
# Retrieve RouterOS version to determine supported commands
command = " /system resource print "
output = connection . send_command ( command )
version_match = re . search ( r " version: \ s*([ \ d.]+) " , output )
routeros_version = Version ( version_match . group ( 1 ) ) if version_match else Version ( " 0.0.0 " )
# Wi-Fi (ROS v6/v7)
commands = [ " /interface wifi print detail " , " /interface wireless print detail " ]
found_valid_output = False
for command in commands :
output = connection . send_command ( command )
if " bad command name " not in output . lower ( ) and output . strip ( ) :
found_valid_output = True
interfaces = output . split ( " \n \n " )
for interface in interfaces :
name_match = re . search ( r ' name= " ([^ " ]+) " ' , interface )
default_name_match = re . search ( r ' default-name= " ([^ " ]+) " ' , interface )
pmkid_match = re . search ( r ' disable-pmkid=( \ S+) ' , interface )
wps_match = re . search ( r ' wps=( \ S+) ' , interface )
name = name_match . group ( 1 ) if name_match else ( default_name_match . group ( 1 ) if default_name_match else " Unknown " )
pmkid = pmkid_match . group ( 1 ) if pmkid_match else " unknown "
wps = wps_match . group ( 1 ) if wps_match else None # Fix: If WPS is not found, set None
if pmkid == " no " :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] ALERT: Wi-Fi ' { name } ' has insecure settings! " )
2025-03-02 22:35:33 +05:00
print ( Fore . RED + " - PMKID attack is possible (disable-pmkid=no) " )
risks_found = True
# Fix: Do not report WPS if it's completely missing in the output
if wps is not None and wps != " disable " :
print ( Fore . RED + f " [!] ALERT: Wi-Fi ' { name } ' has WPS enabled ( { wps } ), Risk of PIN bruteforcing and Pixie Dust attacks. " )
risks_found = True
if not found_valid_output :
print ( Fore . RED + " [-] ERROR: Unable to retrieve Wi-Fi interface settings. Unsupported RouterOS version or missing interface. " )
# Security profiles (ROS v6)
security_profiles_output = connection . send_command ( " /interface wireless security-profiles print detail " )
if security_profiles_output . strip ( ) :
profiles = security_profiles_output . split ( " \n \n " )
for profile in profiles :
profile_name_match = re . search ( r ' name= " ([^ " ]+) " ' , profile )
pmkid_match = re . search ( r ' disable-pmkid=( \ S+) ' , profile )
profile_name = profile_name_match . group ( 1 ) if profile_name_match else " Unknown "
pmkid = pmkid_match . group ( 1 ) if pmkid_match else " unknown "
if pmkid == " no " :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] ALERT: Security Profile ' { profile_name } ' allows PMKID attack! (disable-pmkid=no) " )
2025-03-02 22:35:33 +05:00
risks_found = True
# /interface wifi security print (ROS v7.10+ only)
if routeros_version > = Version ( " 7.10 " ) :
security_output = connection . send_command ( " /interface wifi security print " )
if security_output . strip ( ) :
securities = security_output . split ( " \n \n " )
for security in securities :
sec_name_match = re . search ( r ' name= " ([^ " ]+) " ' , security )
pmkid_match = re . search ( r ' disable-pmkid=( \ S+) ' , security )
wps_match = re . search ( r ' wps=( \ S+) ' , security )
if sec_name_match and ( pmkid_match or wps_match ) :
sec_name = sec_name_match . group ( 1 )
pmkid = pmkid_match . group ( 1 ) if pmkid_match else " unknown "
wps = wps_match . group ( 1 ) if wps_match else None # Fix: Avoid "WPS is enabled (unknown)"
if pmkid == " no " :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] ALERT: Wi-Fi security profile ' { sec_name } ' has insecure settings! " )
2025-03-02 22:35:33 +05:00
print ( Fore . RED + " - PMKID attack is possible (disable-pmkid=no) " )
risks_found = True
if wps is not None and wps != " disable " :
print ( Fore . RED + f " [!] ALERT: Wi-Fi security profile ' { sec_name } ' has WPS enabled ( { wps } ), Risk of PIN bruteforcing and Pixie Dust attacks. " )
risks_found = True
else :
print ( Fore . RED + " [-] ERROR: Unable to retrieve Wi-Fi security settings. " )
else :
print ( Fore . CYAN + " [*] Skipping `/interface wifi security print` (not supported in this version) " )
except Exception as e :
print ( Fore . RED + f " [-] ERROR: Failed to check Wi-Fi settings: { e } " )
if not risks_found :
print ( Fore . GREEN + " [+] All Wi-Fi interfaces and security profiles have secure settings. " )
print ( Fore . YELLOW + " [*] If you use WPA-PSK or WPA2-PSK, take care of password strength. So that the handshake cannot be easily brute-forced. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Check if UPnP is enabled
def check_upnp_status ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking UPnP Status " )
command = " /ip upnp print "
output = connection . send_command ( command )
if " enabled: yes " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + " [!] ALERT: UPnP is ENABLED! This is a very insecure protocol that automatically pushes internal hosts to the Internet. This protocol is used for automatic port forwarding and may also indicate a potential router compromise. Did you enable UPnP yourself? " )
2025-03-02 22:35:33 +05:00
else :
print ( Fore . GREEN + " [+] UPnP is disabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Check if the router is acting as a DNS server
def check_dns_status ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking DNS Settings " )
command = " /ip dns print "
output = connection . send_command ( command )
if " allow-remote-requests: yes " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: Router is acting as a DNS server! This is just a warning. The DNS port on your RouterOS should not be on the external interface. " )
2025-03-02 22:35:33 +05:00
else :
print ( Fore . GREEN + " [+] DNS remote requests are disabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Check DDNS Settings
def check_ddns_status ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking DDNS Settings " )
command = " /ip cloud print "
output = connection . send_command ( command )
if " ddns-enabled: yes " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: Dynamic DNS is enabled! Are you sure you need it? " )
2025-03-02 22:35:33 +05:00
else :
print ( Fore . GREEN + " [+] DDNS is disabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Detect active PoE interfaces that might pose a risk to connected devices
def check_poe_status ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking PoE Status " )
command = " /interface ethernet print detail "
output = connection . send_command ( command )
risks_found = False
interfaces = output . split ( " \n \n " )
for interface in interfaces :
name_match = re . search ( r ' name= " ([^ " ]+) " ' , interface )
poe_match = re . search ( r ' poe-out=( \ S+) ' , interface )
name = name_match . group ( 1 ) if name_match else " Unknown "
poe = poe_match . group ( 1 ) if poe_match else " none "
if poe in [ " auto-on " , " forced-on " ] :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + f " [!] CAUTION: PoE is enabled on { name } . Ensure that connected devices support PoE to prevent damage. " )
2025-03-02 22:35:33 +05:00
risks_found = True
if not risks_found :
print ( Fore . GREEN + " [+] No PoE-enabled interfaces detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Checking RouterBOOT
def check_routerboot_protection ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking RouterBOOT Protection " )
command = " /system routerboard settings print "
output = connection . send_command ( command )
if " protected-routerboot: disabled " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: RouterBOOT protection is disabled! This can allow unauthorized firmware changes and password resets via Netinstall. " )
2025-03-02 22:35:33 +05:00
else :
print ( Fore . GREEN + " [+] RouterBOOT protection is enabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
def check_socks_status ( connection ) :
separator ( " Checking SOCKS Proxy Status " )
command = " /ip socks print "
output = connection . send_command ( command )
if " enabled: yes " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + " [!] ALERT: SOCKS proxy is enabled! This may indicate a possible compromise of the device, the entry point to the internal network. " )
2025-03-02 22:35:33 +05:00
else :
print ( Fore . GREEN + " [+] SOCKS proxy is disabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Verify if RouterBOOT protection is enabled to prevent unauthorized firmware modifications
def check_bandwidth_server_status ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking Bandwidth Server Status " )
command = " /tool bandwidth-server print "
output = connection . send_command ( command )
if " enabled: yes " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: Bandwidth server is enabled! Possible unwanted traffic, possible CPU load. " )
2025-03-02 22:35:33 +05:00
else :
print ( Fore . GREEN + " [+] Bandwidth server is disabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Analyze discovery protocols (CDP, LLDP, MNDP) that might expose network information
def check_neighbor_discovery ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking Neighbor Discovery Protocols " )
command = " /ip neighbor discovery-settings print "
output = connection . send_command ( command )
if " discover-interface-list: all " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: RouterOS sends Discovery protocol packets to all interfaces. This can be used by an attacker to gather data about RouterOS. " )
2025-03-02 22:35:33 +05:00
protocol_match = re . search ( r ' protocol: ([ \ w,]+) ' , output )
if protocol_match :
protocols = protocol_match . group ( 1 )
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + f " [!] Neighbor Discovery Protocols enabled: { protocols } " )
2025-03-02 22:35:33 +05:00
if " discover-interface-list: all " not in output and not protocol_match :
print ( Fore . GREEN + " [+] No security risks found in Neighbor Discovery Protocol settings. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Ensure a minimum password length policy is enforced
def check_password_length_policy ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking Password Policy " )
command = " /user settings print "
output = connection . send_command ( command )
if " minimum-password-length: 0 " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: No minimum password length is enforced! The length of the created passwords must be taken into account. " )
2025-03-02 22:35:33 +05:00
if " minimum-password-length: 0 " not in output :
print ( Fore . GREEN + " [+] Password policy is enforced. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Analyze SSH security settings, including strong encryption and port forwarding risks
def check_ssh_security ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking SSH Security " )
command = " /ip ssh print "
output = connection . send_command ( command )
if " forwarding-enabled: both " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: SSH Dynamic Port Forwarding is enabled! This could indicate a RouterOS compromise, and SSH DPF could also be used by an attacker as a pivoting technique. " )
2025-03-02 22:35:33 +05:00
if " strong-crypto: no " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: strong-crypto is disabled! It is recommended to enable it to enhance security. This will: " )
2025-03-02 22:35:33 +05:00
print ( Fore . YELLOW + " - Use stronger encryption, HMAC algorithms, and larger DH primes; " )
print ( Fore . YELLOW + " - Prefer 256-bit encryption, disable null encryption, prefer SHA-256; " )
print ( Fore . YELLOW + " - Disable MD5, use 2048-bit prime for Diffie-Hellman exchange; " )
if " forwarding-enabled: both " not in output and " strong-crypto: no " not in output :
print ( Fore . GREEN + " [+] SSH security settings are properly configured. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Check if connection tracking is enabled, which may impact performance
def check_connection_tracking ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking Connection Tracking " )
command = " /ip firewall connection tracking print "
output = connection . send_command ( command )
if " enabled: auto " in output or " enabled: on " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: Connection Tracking is enabled! This means RouterOS is tracks connection statuses. " )
2025-03-02 22:35:33 +05:00
print ( Fore . YELLOW + " - If this device is a transit router and does NOT use NAT, consider disabling connection tracking to reduce CPU load. " )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
if " enabled: auto " not in output and " enabled: on " not in output :
print ( Fore . GREEN + " [+] Connection Tracking is properly configured. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Verify if RoMON is enabled, which might expose Layer 2 management access
def check_romon_status ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking RoMON Status " )
command = " /tool romon print "
output = connection . send_command ( command )
if " enabled: yes " in output :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: RoMON is enabled! This allows Layer 2 management access, which may expose the router to unauthorized control. " )
2025-03-02 22:35:33 +05:00
print ( Fore . YELLOW + " - If RoMON is not required, disable it to reduce attack surface. " )
if " enabled: yes " not in output :
print ( Fore . GREEN + " [+] RoMON is disabled. No risk detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Analyze MAC-based Winbox access settings
def check_mac_winbox_security ( connection ) :
separator ( " Checking Winbox MAC Server Settings " )
# MAC-Winbox Server
2025-05-29 22:01:57 +05:00
try :
command = " /tool mac-server mac-winbox print "
output = connection . send_command ( command )
if " allowed-interface-list " in output :
if " allowed-interface-list: all " in output :
print ( Fore . YELLOW + " [!] CAUTION: MAC Winbox access is enabled on all interfaces. " )
else :
print ( Fore . GREEN + " [+] MAC Winbox is properly restricted. " )
else :
# Fallback for older versions: look for "INTERFACE" column and value "all"
if re . search ( r " \ bINTERFACE \ s* \ n.* \ ball \ b " , output , re . DOTALL | re . IGNORECASE ) :
print ( Fore . YELLOW + " [!] CAUTION: MAC Winbox access is enabled on all interfaces " )
else :
print ( Fore . GREEN + " [+] MAC Winbox is properly restricted (legacy format). " )
except Exception as e :
print ( Fore . RED + f " [-] ERROR while checking MAC Winbox: { e } " )
2025-03-02 22:35:33 +05:00
# MAC-Server
2025-05-29 22:01:57 +05:00
try :
command = " /tool mac-server print "
output = connection . send_command ( command )
if " allowed-interface-list " in output :
if " allowed-interface-list: all " in output :
print ( Fore . YELLOW + " [!] CAUTION: MAC Telnet access is enabled on all interfaces. " )
else :
print ( Fore . GREEN + " [+] MAC Telnet is properly restricted. " )
else :
if re . search ( r " \ bINTERFACE \ s* \ n.* \ ball \ b " , output , re . DOTALL | re . IGNORECASE ) :
print ( Fore . YELLOW + " [!] CAUTION: MAC Telnet access is enabled on all interfaces " )
else :
print ( Fore . GREEN + " [+] MAC Telnet is properly restricted (legacy format). " )
except Exception as e :
print ( Fore . RED + f " [-] ERROR while checking MAC Telnet: { e } " )
2025-03-02 22:35:33 +05:00
# MAC Ping
2025-05-29 22:01:57 +05:00
try :
command = " /tool mac-server ping print "
output = connection . send_command ( command )
if " enabled: yes " in output :
print ( Fore . YELLOW + " [!] CAUTION: MAC Ping is enabled. Possible unwanted traffic. " )
else :
print ( Fore . GREEN + " [+] MAC Ping is properly restricted. " )
except Exception as e :
print ( Fore . RED + f " [-] ERROR while checking MAC Ping: { e } " )
2025-03-02 22:35:33 +05:00
# Check for weak SNMP community strings that could be exploited
def check_snmp ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking SNMP Community Strings " )
command = " /snmp community print "
output = connection . send_command ( command )
bad_names = [ " public " , " private " , " admin " , " mikrotik " , " mikrotik_admin " , " root " , " routeros " , " zabbix " ]
risks_found = False
for line in output . splitlines ( ) :
match = re . search ( r ' ^ \ s* \ d+ \ s+[*X]? \ s*([ \ w-]+) ' , line )
if match :
community_name = match . group ( 1 ) . lower ( )
if community_name in bad_names :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + f " [!] CAUTION: Weak SNMP community string detected: ' { community_name } ' . Change it to a secure, unique value. " )
2025-03-02 22:35:33 +05:00
risks_found = True
if not risks_found :
print ( Fore . GREEN + " [+] SNMP community strings checked. No weak values detected. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Detect and analyze firewall NAT rules that could expose internal services
def check_dst_nat_rules ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking Firewall NAT Rules " )
command = " /ip firewall nat print "
output = connection . send_command ( command )
dst_nat_rules = [ ]
for line in output . splitlines ( ) :
if " action=dst-nat " in line or " action=netmap " in line :
dst_nat_rules . append ( line . strip ( ) )
if dst_nat_rules :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] CAUTION: Destination NAT (dst-nat/netmap) rules detected! Exposing devices to the internet can be dangerous. " )
print ( Fore . YELLOW + " [*] Similar rules can also be created by the attacker. Did you really create these rules yourself? " )
2025-03-02 22:35:33 +05:00
print ( Fore . YELLOW + " - Review the following NAT rules: " )
for rule in dst_nat_rules :
print ( Fore . YELLOW + f " { rule } " )
if not dst_nat_rules :
print ( Fore . GREEN + " [+] No Destination NAT (dst-nat/netmap) rules detected. No risks found. " )
2025-05-29 22:01:57 +05:00
print ( " [ " + Fore . GREEN + " + " + Fore . WHITE + " ] No issues found. " )
2025-03-02 22:35:33 +05:00
# Identify potentially malicious scheduled tasks
def detect_malicious_schedulers ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking for Malicious Schedulers " )
command = " /system scheduler print detail "
output = connection . send_command ( command )
risks_found = False
fetch_files = set ( )
for task in output . split ( " \n \n " ) :
name_match = re . search ( r ' name= " ?([^ " ]+) " ? ' , task )
event_match = re . search ( r ' on-event= " ?([^ " \ n]+) " ? ' , task )
policy_match = re . search ( r ' policy=([ \ w,]+) ' , task )
interval_match = re . search ( r ' interval=( \ d+)([smhd]) ' , task )
name = name_match . group ( 1 ) if name_match else " Unknown "
event = event_match . group ( 1 ) . strip ( ) if event_match else " "
policy = policy_match . group ( 1 ) . split ( " , " ) if policy_match else [ ]
interval_value , interval_unit = ( int ( interval_match . group ( 1 ) ) , interval_match . group ( 2 ) ) if interval_match else ( None , None )
# DEBUG
print ( Fore . CYAN + f " [*] Checking: ' { name } ' → { event } " )
# Fetch detection
fetch_match = re . search ( r ' dst-path=([ \ S]+) ' , event )
if " fetch " in event and fetch_match :
fetched_file = fetch_match . group ( 1 ) . strip ( " ; " )
fetch_files . add ( fetched_file )
print ( Fore . YELLOW + f " [!] Noted fetched file: { fetched_file } " )
# Import detection (checks if imported file was fetched earlier)
import_match = re . search ( r ' import \ s+([ \ S]+) ' , event )
if " import " in event and import_match :
imported_file = import_match . group ( 1 ) . strip ( " ; " )
if imported_file in fetch_files :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] ALERT: ' { name } ' is a BACKDOOR! " )
2025-03-02 22:35:33 +05:00
print ( Fore . RED + " - This scheduler imports a previously fetched script. " )
print ( Fore . RED + " - Attacker can inject any command remotely via this script. " )
print ( Fore . RED + f " - Interval: { interval_value } { interval_unit } , ensuring persistence. " )
risks_found = True
# High privileges checking
dangerous_policies = { " password " , " sensitive " , " sniff " , " ftp " }
if any ( p in dangerous_policies for p in policy ) :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] ALERT: ' { name } ' has HIGH PRIVILEGES! " )
2025-03-02 22:35:33 +05:00
print ( Fore . RED + f " - It has dangerous permissions: { ' , ' . join ( policy ) } " )
risks_found = True
# Reboot detection (Anti-forensics & persistence check)
if " reboot " in event :
if interval_value and interval_unit in [ " s " , " m " , " h " ] and interval_value < 12 :
print ( Fore . RED + f " [!] ALERT: ' { name } ' reboots router TOO FREQUENTLY ( { interval_value } { interval_unit } )! " )
print ( Fore . RED + " - This may be an attempt to prevent log analysis (anti-forensics). " )
risks_found = True
else :
print ( Fore . YELLOW + f " [!] CAUTION: ' { name } ' schedules a reboot. " )
print ( Fore . YELLOW + " - Ensure this is intentional and not used to hide attacks. " )
continue
# Frequent execution detection
if interval_value and interval_unit in [ " s " , " m " , " h " ] and interval_value < 25 :
2025-05-29 22:01:57 +05:00
print ( Fore . RED + f " [!] ALERT: ' { name } ' executes TOO FREQUENTLY ( { interval_value } { interval_unit } )! " )
2025-03-02 22:35:33 +05:00
print ( Fore . RED + " - This indicates botnet-like persistence. " )
risks_found = True
if not risks_found :
print ( Fore . GREEN + " [+] No malicious schedulers detected. " )
# Checking DNS Static Entries
def check_static_dns_entries ( connection ) :
2025-05-22 23:49:12 +05:00
# Separator outlet
2025-03-02 22:35:33 +05:00
separator ( " Checking Static DNS Entries " )
command = " /ip dns static print detail "
output = connection . send_command ( command )
dns_entries = [ ]
entry_blocks = output . split ( " \n \n " )
for entry in entry_blocks :
name_match = re . search ( r ' name= " ([^ " ]+) " ' , entry )
address_match = re . search ( r ' address=([ \ d.]+) ' , entry )
if name_match and address_match :
name = name_match . group ( 1 )
address = address_match . group ( 1 )
dns_entries . append ( ( name , address ) )
if dns_entries :
print ( Fore . YELLOW + " [!] WARNING: The following static DNS entries exist: " )
for name , address in dns_entries :
print ( Fore . CYAN + f " - { name } → { address } " )
print ( Fore . YELLOW + " [*] Were you the one who created those static DNS records? Make sure. " )
print ( Fore . YELLOW + " [*] Attackers during RouterOS post-exploitation like to tamper with DNS record settings, for example, for phishing purposes. " )
else :
print ( Fore . GREEN + " [+] No static DNS entries found. " )
2025-05-29 22:01:57 +05:00
# Require user confirmation before proceeding, emphasizing legal responsibility
def confirm_legal_usage ( ) :
print ( " " + " WARNING: This tool is for security auditing of YOUR OWN RouterOS devices. " )
print ( " " + " Unauthorized use may be illegal. Proceed responsibly. \n " )
response = input ( " " + " Do you wish to proceed? [yes/no]: " ) . strip ( )
2024-08-17 20:00:26 +05:00
2025-05-29 22:01:57 +05:00
if response . lower ( ) != " yes " :
print ( " \n Operation aborted. Exiting... " )
sys . exit ( 0 )
2025-03-02 22:35:33 +05:00
# Require user confirmation before proceeding, emphasizing legal responsibility
def confirm_legal_usage ( ) :
print ( " " + " WARNING: This tool is for security auditing of YOUR OWN RouterOS devices. " )
print ( " " + " Unauthorized use may be illegal. Proceed responsibly. \n " )
2025-05-25 11:40:27 +04:00
def prompt_legal_usage ( ) :
2025-03-02 22:35:33 +05:00
response = input ( " " + " Do you wish to proceed? [yes/no]: " ) . strip ( )
2024-08-17 20:00:26 +05:00
2025-03-02 22:35:33 +05:00
if response . lower ( ) != " yes " :
print ( " \n Operation aborted. Exiting... " )
sys . exit ( 0 )
2024-08-17 20:00:26 +05:00
2025-05-29 22:01:57 +05:00
# Main func
2024-08-17 20:00:26 +05:00
def main ( ) :
banner ( )
2025-05-29 22:01:57 +05:00
2025-03-02 22:35:33 +05:00
parser = argparse . ArgumentParser ( )
parser . add_argument ( " --ip " , help = " The address of your MikroTik router " )
parser . add_argument ( " --username " , help = " SSH username (RO account can be used) " )
parser . add_argument ( " --password " , help = " SSH password " )
2025-05-25 11:05:23 +04:00
parser . add_argument ( " --ssh-key " , help = " SSH key " )
2025-05-25 11:23:49 +04:00
parser . add_argument ( " --passphrase " , help = " SSH key passphrase " )
2025-03-02 22:35:33 +05:00
parser . add_argument ( " --port " , type = int , default = 22 , help = " SSH port (default: 22) " )
2025-05-29 22:01:57 +05:00
parser . add_argument ( " --cve " , action = " store_true " , help = " Check RouterOS version against known CVEs " )
parser . add_argument ( " --skip-confirmation " , action = ' store_true ' , help = " Skips legal usage confirmation prompt " )
2025-03-02 22:35:33 +05:00
args = parser . parse_args ( )
if len ( sys . argv ) == 2 and sys . argv [ 1 ] in [ " -h " , " --help " ] :
parser . print_help ( )
sys . exit ( 0 )
2025-05-29 22:01:57 +05:00
if not args . ip or not args . username or ( not args . password and not args . ssh_key ) :
print ( Fore . YELLOW + " [!] ERROR: Missing required arguments " )
2025-05-25 11:05:23 +04:00
print ( Fore . YELLOW + " [!] Use ' sara --help ' for more information " )
sys . exit ( 1 )
if args . password and args . ssh_key :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] ERROR: Can ' t use both password & ssh_key authentication " )
2025-05-25 11:05:23 +04:00
sys . exit ( 1 )
2025-05-29 22:01:57 +05:00
2025-05-25 11:23:49 +04:00
if args . passphrase and not args . ssh_key :
2025-05-29 22:01:57 +05:00
print ( Fore . YELLOW + " [!] ERROR: Passphrase requires --ssh-key " )
2025-05-25 11:23:49 +04:00
sys . exit ( 1 )
2025-05-25 11:05:23 +04:00
2025-05-29 22:01:57 +05:00
# Legal warning (interactive only if not skipped)
2025-05-25 11:40:27 +04:00
if not args . skip_confirmation :
2025-05-29 22:01:57 +05:00
# disclaimer text
confirm_legal_usage ( )
# yes or no
2025-05-25 11:40:27 +04:00
prompt_legal_usage ( )
2025-05-29 22:01:57 +05:00
else :
confirm_legal_usage ( )
2025-03-02 22:35:33 +05:00
# Start timer
start_time = time . time ( )
2025-05-29 22:01:57 +05:00
# Connect to RouterOS
connection = connect_to_router (
args . ip ,
2025-05-25 11:23:49 +04:00
args . username ,
args . password ,
args . port ,
args . ssh_key ,
args . passphrase
)
2025-03-02 22:35:33 +05:00
2025-05-29 22:01:57 +05:00
# Run only CVE check if --cve is used
if args . cve :
run_cve_audit ( connection )
connection . disconnect ( )
return
# Run full audit
2025-03-02 22:35:33 +05:00
check_routeros_version ( connection )
check_smb ( connection )
check_rmi_services ( connection )
check_default_users ( connection )
checking_access_to_RMI ( connection )
check_wifi_security ( connection )
check_upnp_status ( connection )
check_dns_status ( connection )
check_ddns_status ( connection )
check_poe_status ( connection )
check_routerboot_protection ( connection )
check_socks_status ( connection )
check_bandwidth_server_status ( connection )
check_neighbor_discovery ( connection )
check_password_length_policy ( connection )
check_ssh_security ( connection )
check_connection_tracking ( connection )
check_romon_status ( connection )
check_mac_winbox_security ( connection )
check_snmp ( connection )
check_dst_nat_rules ( connection )
detect_malicious_schedulers ( connection )
check_static_dns_entries ( connection )
2025-05-29 22:01:57 +05:00
print ( )
2025-03-02 22:35:33 +05:00
connection . disconnect ( )
2025-05-29 22:01:57 +05:00
print ( Fore . WHITE + f " [*] Disconnected from RouterOS ( { args . ip } : { args . port } ) " )
2025-03-02 22:35:33 +05:00
end_time = time . time ( )
total_time = round ( end_time - start_time , 2 )
2025-05-29 22:01:57 +05:00
print ( Fore . WHITE + f " [*] All checks have been completed. Security inspection completed in { total_time } seconds \n " )
2024-08-17 20:00:26 +05:00
if __name__ == " __main__ " :
2025-05-29 22:01:57 +05:00
main ( )