mirror of
https://github.com/casterbyte/Sara.git
synced 2025-06-23 06:28:41 +02:00
v1.2
This commit is contained in:
parent
557586f836
commit
6cf9b1a555
5 changed files with 467 additions and 449 deletions
228
README.md
228
README.md
|
@ -5,13 +5,11 @@ RouterOS configuration analyzer to find security misconfigurations and vulnerabi
|
||||||

|

|
||||||
|
|
||||||
```
|
```
|
||||||
RouterOS Security Inspector. For security engineers
|
RouterOS Security Inspector. Designed for security engineers
|
||||||
Operates remotely using SSH, designed to evaluate RouterOS security
|
|
||||||
|
|
||||||
Author: Magama Bazarov, <magamabazarov@mailbox.org>
|
Author: Magama Bazarov, <magamabazarov@mailbox.org>
|
||||||
Alias: Caster
|
Alias: Caster
|
||||||
Version: 1.1.1
|
Version: 1.2
|
||||||
Codename: Judge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
|
@ -127,28 +125,52 @@ The same principle works for the other checks. Only read the configuration and t
|
||||||
|
|
||||||
Sara performs a security analysis of RouterOS by checking the current firmware version and checking it against a database of known vulnerabilities (CVEs). This process identifies critical vulnerabilities that can be exploited by attackers to compromise the device.
|
Sara performs a security analysis of RouterOS by checking the current firmware version and checking it against a database of known vulnerabilities (CVEs). This process identifies critical vulnerabilities that can be exploited by attackers to compromise the device.
|
||||||
|
|
||||||
## But how does it work?
|
## How does it work?
|
||||||
|
|
||||||
1. Sara extracts the current RouterOS version from the device using the system command (`/system resource print`)
|
Sara has a special module called `cve_analyzer.py`, which creates `routeros_cves.json` based on the NVD NIST database containing information about vulnerabilities, including those in MikroTik RouterOS.
|
||||||
|
Vulnerabilities for the RouterOS version are searched for using the `--cve` argument. The results will show the total number of vulnerabilities, their categorization, as well as the CVE ID and a brief description.
|
||||||
|
|
||||||
2. The check is performed using the built-in `cve_lookup.py` module, which stores a dictionary of known RouterOS vulnerabilities. This module is based on data obtained [from the MITRE CVE database](https://cve.mitre.org/data/downloads) and contains:
|
```bash
|
||||||
|
caster@kali:~$ sara --ip 192.168.88.1 --username admin --password admin --cve
|
||||||
- CVE ID;
|
```
|
||||||
- Vulnerability Description;
|
|
||||||
- Range of vulnerable RouterOS versions
|
|
||||||
|
|
||||||
Sara analyzes the version of the device and determines if it falls into the list of vulnerable versions.
|
|
||||||
|
|
||||||
3. If the RouterOS version contains known vulnerabilities, Sara displays a warning indicating:
|
|
||||||
|
|
||||||
- CVE ID;
|
|
||||||
- Description of the vulnerability and potential risks.
|
|
||||||
|
|
||||||
## Specifics of checking
|
## Specifics of checking
|
||||||
|
|
||||||
- Sara does not verify real-world exploitation of vulnerabilities. It only cross-references the RouterOS version against publicly available CVE databases;
|
- Sara does not verify real-world exploitation of vulnerabilities. It only cross-references the RouterOS version against publicly available CVE databases;
|
||||||
- If the device is running an older version of RouterOS, but vulnerable services have been manually disabled, some warnings may be false positives;
|
- If the device is running an older version of RouterOS, but vulnerable services have been manually disabled, some warnings may be false positives;
|
||||||
- The CVE database is updated over time, so it is recommended to keep an eye out for current patches from MikroTik yourself.
|
- It is recommended to manually validate your version of RouterOS after the audit to ensure there are no false positives.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[+] Detected RouterOS Version: 7.1.1
|
||||||
|
[!] routeros_cves.json not found.
|
||||||
|
[*] Fetching CVEs from NVD...
|
||||||
|
[+] Saved 74 CVEs to routeros_cves.json
|
||||||
|
[*] Total matching CVEs: 4
|
||||||
|
[*] CRITICAL: 1
|
||||||
|
[*] HIGH: 1
|
||||||
|
[*] MEDIUM: 2
|
||||||
|
[*] Vulnerability details:
|
||||||
|
|
||||||
|
→ CVE-2022-45313 [HIGH]
|
||||||
|
Mikrotik RouterOs before stable v7.5 was discovered to contain an out-of-bounds read in the hotspot process. This vulnerability allows attackers to execute arbitrary code via a crafted nova message.
|
||||||
|
CVSS Score: 8.8
|
||||||
|
|
||||||
|
→ CVE-2022-45315 [CRITICAL]
|
||||||
|
Mikrotik RouterOs before stable v7.6 was discovered to contain an out-of-bounds read in the snmp process. This vulnerability allows attackers to execute arbitrary code via a crafted packet.
|
||||||
|
CVSS Score: 9.8
|
||||||
|
|
||||||
|
→ CVE-2023-41570 [MEDIUM]
|
||||||
|
MikroTik RouterOS v7.1 to 7.11 was discovered to contain incorrect access control mechanisms in place for the Rest API.
|
||||||
|
CVSS Score: 5.3
|
||||||
|
|
||||||
|
→ CVE-2024-54772 [MEDIUM]
|
||||||
|
An issue was discovered in the Winbox service of MikroTik RouterOS long-term release v6.43.13 through v6.49.13 and stable v6.43 through v7.17.2. A patch is available in the stable release v6.49.18. A discrepancy in response size between connection attempts made with a valid username and those with an invalid username allows attackers to enumerate for valid accounts.
|
||||||
|
CVSS Score: 5.4
|
||||||
|
```
|
||||||
|
|
||||||
|
> The quality of entries in the NVD leaves much to be desired; in many cases, fields such as `versionEndExcluding` or `versionStartExcluding` have a value of “null.” Therefore, it is also important to validate your RouterOS version to ensure that a particular vulnerability exists.
|
||||||
|
|
||||||
# How to use
|
# How to use
|
||||||
|
|
||||||
|
@ -176,7 +198,7 @@ caster@kali:~$ sara -h
|
||||||
Sara supports the following command line options:
|
Sara supports the following command line options:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
usage: sara.py [-h] [--ip IP] [--username USERNAME] [--password PASSWORD] [--ssh-key SSH_KEY] [--passphrase PASSPHRASE] [--skip-confirmation] [--port PORT]
|
usage: sara.py [-h] [--ip IP] [--username USERNAME] [--password PASSWORD] [--ssh-key SSH_KEY] [--passphrase PASSPHRASE] [--port PORT] [--cve] [--skip-confirmation]
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -186,8 +208,9 @@ options:
|
||||||
--ssh-key SSH_KEY SSH key
|
--ssh-key SSH_KEY SSH key
|
||||||
--passphrase PASSPHRASE
|
--passphrase PASSPHRASE
|
||||||
SSH key passphrase
|
SSH key passphrase
|
||||||
--skip-confirmation Skips the confirmation prompt (disclamer: ensure that your are allowed to use this tool)
|
|
||||||
--port PORT SSH port (default: 22)
|
--port PORT SSH port (default: 22)
|
||||||
|
--cve Check RouterOS version against known CVEs
|
||||||
|
--skip-confirmation Skips legal usage confirmation prompt
|
||||||
```
|
```
|
||||||
|
|
||||||
1. `--ip` - this argument specifies the IP address of the MikroTik device to which Sara is connecting;
|
1. `--ip` - this argument specifies the IP address of the MikroTik device to which Sara is connecting;
|
||||||
|
@ -206,168 +229,11 @@ options:
|
||||||
|
|
||||||
> This only works when using the `--ssh-key` argument.
|
> This only works when using the `--ssh-key` argument.
|
||||||
|
|
||||||
6. `--skip-confirmation` skips the confirmation prompt that asks if you are allowed to use this tool on the target system
|
6. `--port` - allows you to specify a non-standard SSH port for connection. The default is **22**, but if you have changed the SSH port number, it must be specified manually.
|
||||||
|
|
||||||
> Please do ensure the legality of what you're doing.
|
7. `--cve` - launches a vulnerability search using the NIST NVD database.
|
||||||
|
|
||||||
7. `--port` - allows you to specify a non-standard SSH port for connection. The default is **22**, but if you have changed the SSH port number, it must be specified
|
8. `--skip-confirmation` - allows you to skip the audit start confirmation check. Use this if you really have permission to perform a security audit.
|
||||||
|
|
||||||
# Sara's Launch
|
|
||||||
|
|
||||||
```bash
|
|
||||||
caster@kali:~$ python3 sara.py --ip 192.168.88.1 --username admin --password mypass
|
|
||||||
|
|
||||||
_____
|
|
||||||
/ ____|
|
|
||||||
| (___ __ _ _ __ __ _
|
|
||||||
\___ \ / _` | '__/ _` |
|
|
||||||
____) | (_| | | | (_| |
|
|
||||||
|_____/ \__,_|_| \__,_|
|
|
||||||
|
|
||||||
RouterOS Security Inspector. For security engineers
|
|
||||||
Operates remotely using SSH, designed to evaluate RouterOS security
|
|
||||||
|
|
||||||
Author: Magama Bazarov, <caster@exploit.org>
|
|
||||||
Alias: Caster
|
|
||||||
Version: 1.1
|
|
||||||
Codename: Judge
|
|
||||||
Documentation & Usage: https://github.com/casterbyte/Sara
|
|
||||||
|
|
||||||
[!] DISCLAIMER: Use this tool only for auditing your own devices.
|
|
||||||
[!] Unauthorized use on third-party systems is ILLEGAL.
|
|
||||||
[!] The author is not responsible for misuse.
|
|
||||||
|
|
||||||
WARNING: This tool is for security auditing of YOUR OWN RouterOS devices.
|
|
||||||
Unauthorized use may be illegal. Proceed responsibly.
|
|
||||||
|
|
||||||
Do you wish to proceed? [yes/no]: yes
|
|
||||||
[*] Connecting to RouterOS at 192.168.88.1:22
|
|
||||||
[*] Connection successful!
|
|
||||||
========================================
|
|
||||||
[*] Checking RouterOS Version
|
|
||||||
[+] Detected RouterOS Version: 7.15.3
|
|
||||||
[+] No known CVEs found for this version.
|
|
||||||
========================================
|
|
||||||
[*] Checking SMB Service
|
|
||||||
[+] SMB is disabled. No risk detected.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking RMI Services
|
|
||||||
[!] ALERT: TELNET is ENABLED! This is a high security risk.
|
|
||||||
- Account passwords can be intercepted
|
|
||||||
[!] ALERT: FTP is ENABLED! This is a high security risk.
|
|
||||||
- Are you sure you need FTP?
|
|
||||||
[!] ALERT: HTTP is ENABLED! This is a high security risk.
|
|
||||||
- Account passwords can be intercepted
|
|
||||||
[+] OK: SSH is enabled. Good!
|
|
||||||
- Are you using strong passwords and SSH keys for authentication?
|
|
||||||
[!] CAUTION: HTTP-SSL is enabled.
|
|
||||||
- HTTPS detected. Ensure it uses a valid certificate and strong encryption.
|
|
||||||
[!] CAUTION: API is enabled.
|
|
||||||
- RouterOS API is vulnerable to a bruteforce attack. If you need it, make sure you have access to it.
|
|
||||||
[!] CAUTION: WINBOX is enabled.
|
|
||||||
[!] CAUTION: If you're using 'Keep Password' in Winbox, your credentials may be stored in plaintext!
|
|
||||||
- If your PC is compromised, attackers can extract saved credentials.
|
|
||||||
- Consider disabling 'Keep Password' to improve security.
|
|
||||||
[!] CAUTION: API-SSL is enabled.
|
|
||||||
- RouterOS API is vulnerable to a bruteforce attack. If you need it, make sure you have access to it.
|
|
||||||
========================================
|
|
||||||
[*] Checking Default Usernames
|
|
||||||
[!] CAUTION: Default username 'admin' detected! Change it to a unique one.
|
|
||||||
[!] CAUTION: Default username 'engineer' detected! Change it to a unique one.
|
|
||||||
========================================
|
|
||||||
[*] Checking network access to RMI
|
|
||||||
[!] CAUTION: TELNET has no IP restriction set! Please restrict access.
|
|
||||||
[!] CAUTION: FTP has no IP restriction set! Please restrict access.
|
|
||||||
[!] CAUTION: WWW has no IP restriction set! Please restrict access.
|
|
||||||
[+] OK! SSH is restricted to: 192.168.88.0/24
|
|
||||||
[!] CAUTION: WWW-SSL has no IP restriction set! Please restrict access.
|
|
||||||
[!] CAUTION: API has no IP restriction set! Please restrict access.
|
|
||||||
[+] OK! WINBOX is restricted to: 192.168.88.0/24
|
|
||||||
[!] CAUTION: API-SSL has no IP restriction set! Please restrict access.
|
|
||||||
========================================
|
|
||||||
[*] Checking Wi-Fi Security
|
|
||||||
[+] All Wi-Fi interfaces and security profiles have secure settings.
|
|
||||||
[*] If you use WPA-PSK or WPA2-PSK, take care of password strength. So that the handshake cannot be easily brute-forced.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking UPnP Status
|
|
||||||
[+] UPnP is disabled. No risk detected.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking DNS Settings
|
|
||||||
[!] 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.
|
|
||||||
========================================
|
|
||||||
[*] Checking DDNS Settings
|
|
||||||
[+] DDNS is disabled. No risk detected.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking PoE Status
|
|
||||||
[!] CAUTION: PoE is enabled on ether1. Ensure that connected devices support PoE to prevent damage.
|
|
||||||
========================================
|
|
||||||
[*] Checking RouterBOOT Protection
|
|
||||||
[!] CAUTION: RouterBOOT protection is disabled! This can allow unauthorized firmware changes and password resets via Netinstall.
|
|
||||||
========================================
|
|
||||||
[*] Checking SOCKS Proxy Status
|
|
||||||
[+] SOCKS proxy is disabled. No risk detected.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking Bandwidth Server Status
|
|
||||||
[+] Bandwidth server is disabled. No risk detected.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking Neighbor Discovery Protocols
|
|
||||||
[+] No security risks found in Neighbor Discovery Protocol settings.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking Password Policy
|
|
||||||
[!] CAUTION: No minimum password length is enforced! The length of the created passwords must be taken into account.
|
|
||||||
========================================
|
|
||||||
[*] Checking SSH Security
|
|
||||||
[!] 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.
|
|
||||||
[!] CAUTION: strong-crypto is disabled! It is recommended to enable it to enhance security. This will:
|
|
||||||
- Use stronger encryption, HMAC algorithms, and larger DH primes;
|
|
||||||
- Prefer 256-bit encryption, disable null encryption, prefer SHA-256;
|
|
||||||
- Disable MD5, use 2048-bit prime for Diffie-Hellman exchange;
|
|
||||||
========================================
|
|
||||||
[*] Checking Connection Tracking
|
|
||||||
[+] Connection Tracking is properly configured.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking RoMON Status
|
|
||||||
[+] RoMON is disabled. No risk detected.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking Winbox MAC Server Settings
|
|
||||||
[+] MAC Winbox are properly restricted.
|
|
||||||
[+] MAC Telnet are properly restricted.
|
|
||||||
[+] MAC Ping are properly restricted.
|
|
||||||
========================================
|
|
||||||
[*] Checking SNMP Community Strings
|
|
||||||
[+] SNMP community strings checked. No weak values detected.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking Firewall NAT Rules
|
|
||||||
[+] No Destination NAT (dst-nat/netmap) rules detected. No risks found.
|
|
||||||
[+] No issues found.
|
|
||||||
========================================
|
|
||||||
[*] Checking for Malicious Schedulers
|
|
||||||
[*] Checking: 'Unknown' →
|
|
||||||
[+] No malicious schedulers detected.
|
|
||||||
========================================
|
|
||||||
[*] Checking Static DNS Entries
|
|
||||||
[!] WARNING: The following static DNS entries exist:
|
|
||||||
- dc01.myownsummer.org → 192.168.88.71
|
|
||||||
- fake.example.com → 192.168.88.100
|
|
||||||
[*] Were you the one who created those static DNS records? Make sure.
|
|
||||||
[*] Attackers during RouterOS post-exploitation like to tamper with DNS record settings, for example, for phishing purposes.
|
|
||||||
========================================
|
|
||||||
[*] Checking Router Uptime
|
|
||||||
[*] Router Uptime: 64 days, 2 hours, 23 minutes
|
|
||||||
|
|
||||||
[*] Disconnected from RouterOS (192.168.88.1:22)
|
|
||||||
[*] All checks have been completed. Security inspection completed in 3.03 seconds
|
|
||||||
```
|
|
||||||
|
|
||||||
# Copyright
|
# Copyright
|
||||||
|
|
||||||
|
|
281
cve_analyzer.py
Normal file
281
cve_analyzer.py
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
# Auxiliary module cve_analyzer.py for searching CVE using the NIST NVD database
|
||||||
|
|
||||||
|
# Copyright (c) 2025 Magama Bazarov
|
||||||
|
# Licensed under the Apache 2.0 License
|
||||||
|
# This project is not affiliated with or endorsed by MikroTik
|
||||||
|
|
||||||
|
import json, re, os, requests, time
|
||||||
|
from packaging.version import Version, InvalidVersion
|
||||||
|
from colorama import Fore, Style
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
NVD_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
||||||
|
KEYWORD = "routeros"
|
||||||
|
RESULTS_PER_PAGE = 2000
|
||||||
|
OUTPUT_FILE = "routeros_cves.json"
|
||||||
|
|
||||||
|
# Converts version string to a comparable Version object
|
||||||
|
def normalize_version(v):
|
||||||
|
if not v:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return Version(v)
|
||||||
|
except InvalidVersion:
|
||||||
|
# Strip unstable labels like 'rc', 'beta', etc
|
||||||
|
cleaned = re.sub(r'(rc|beta|testing|stable)[\d\-]*', '', v, flags=re.IGNORECASE)
|
||||||
|
try:
|
||||||
|
return Version(cleaned)
|
||||||
|
except InvalidVersion:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract version ranges from CVE descriptions
|
||||||
|
def extract_ranges_from_description(description):
|
||||||
|
description = description.lower()
|
||||||
|
ranges = []
|
||||||
|
|
||||||
|
# Match version ranges in various formats
|
||||||
|
matches = re.findall(r"(?:from\s+)?v?(\d+\.\d+(?:\.\d+)?)\s+to\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
||||||
|
for start, end in matches:
|
||||||
|
ranges.append({"versionStartIncluding": start, "versionEndIncluding": end})
|
||||||
|
|
||||||
|
matches = re.findall(r"before\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
||||||
|
for end in matches:
|
||||||
|
ranges.append({"versionEndExcluding": end})
|
||||||
|
|
||||||
|
matches = re.findall(r"after\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
||||||
|
for start in matches:
|
||||||
|
ranges.append({"versionStartExcluding": start})
|
||||||
|
|
||||||
|
matches = re.findall(r"through\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
||||||
|
for end in matches:
|
||||||
|
ranges.append({"versionEndIncluding": end})
|
||||||
|
|
||||||
|
matches = re.findall(r"v?(\d+\.\d+)\.x", description)
|
||||||
|
for base in matches:
|
||||||
|
ranges.append({"versionEndIncluding": f"{base}.999"})
|
||||||
|
|
||||||
|
matches = re.findall(r"(?:up to|and below)\s+v?(\d+\.\d+(?:\.\d+)?)", description)
|
||||||
|
for end in matches:
|
||||||
|
ranges.append({"versionEndIncluding": end})
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
|
||||||
|
# Determines if the given version falls within a vulnerable range
|
||||||
|
def is_version_affected(current_v, version_info):
|
||||||
|
def get(v_key):
|
||||||
|
return normalize_version(version_info.get(v_key))
|
||||||
|
|
||||||
|
criteria = version_info.get("criteria", "")
|
||||||
|
end_excl_raw = version_info.get("versionEndExcluding", "")
|
||||||
|
|
||||||
|
# RouterOS 6.x vs 7.x false positive prevention
|
||||||
|
if isinstance(end_excl_raw, str) and end_excl_raw.startswith("7") and str(current_v).startswith("6."):
|
||||||
|
return False
|
||||||
|
if isinstance(end_excl_raw, str) and end_excl_raw.startswith("6") and str(current_v).startswith("7."):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Parse version range keys
|
||||||
|
start_incl = get("versionStartIncluding")
|
||||||
|
start_excl = get("versionStartExcluding")
|
||||||
|
end_incl = get("versionEndIncluding")
|
||||||
|
end_excl = get("versionEndExcluding")
|
||||||
|
|
||||||
|
# Fallback: match exact version in criteria if no range info is provided
|
||||||
|
if not any([start_incl, start_excl, end_incl, end_excl]):
|
||||||
|
version_match = re.search(r"routeros:([\w.\-]+)", criteria)
|
||||||
|
if version_match:
|
||||||
|
version_exact = normalize_version(version_match.group(1))
|
||||||
|
return version_exact is not None and current_v == version_exact
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Skip if range is invalid or unparseable
|
||||||
|
for raw, normed in zip(["versionStartIncluding", "versionStartExcluding", "versionEndIncluding", "versionEndExcluding"],
|
||||||
|
[start_incl, start_excl, end_incl, end_excl]):
|
||||||
|
if version_info.get(raw) and normed is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Perform actual version comparisons
|
||||||
|
if start_incl and current_v < start_incl:
|
||||||
|
return False
|
||||||
|
if start_excl and current_v <= start_excl:
|
||||||
|
return False
|
||||||
|
if end_incl and current_v > end_incl:
|
||||||
|
return False
|
||||||
|
if end_excl and current_v >= end_excl:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Downloads and stores all CVEs from NVD
|
||||||
|
def fetch_all_cves():
|
||||||
|
all_cves = []
|
||||||
|
start_index = 0
|
||||||
|
|
||||||
|
print(Fore.CYAN + "[*] Fetching CVEs from NVD...")
|
||||||
|
while True:
|
||||||
|
params = {
|
||||||
|
"keywordSearch": KEYWORD,
|
||||||
|
"startIndex": start_index,
|
||||||
|
"resultsPerPage": RESULTS_PER_PAGE
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.get(NVD_URL, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(Fore.RED + f"[-] HTTP Error: {e}")
|
||||||
|
break
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(Fore.RED + "[-] Failed to parse JSON from NVD.")
|
||||||
|
break
|
||||||
|
|
||||||
|
cve_items = data.get("vulnerabilities", [])
|
||||||
|
total_results = data.get("totalResults", 0)
|
||||||
|
|
||||||
|
# Process each CVE entry
|
||||||
|
for item in cve_items:
|
||||||
|
cve = item.get("cve", {})
|
||||||
|
cve_id = cve.get("id")
|
||||||
|
description = next((d["value"] for d in cve.get("descriptions", []) if d["lang"] == "en"), "")
|
||||||
|
severity = "UNKNOWN"
|
||||||
|
score = "N/A"
|
||||||
|
|
||||||
|
# Extract CVSS score/severity
|
||||||
|
metrics = cve.get("metrics", {})
|
||||||
|
if "cvssMetricV31" in metrics:
|
||||||
|
cvss = metrics["cvssMetricV31"][0]["cvssData"]
|
||||||
|
severity = cvss.get("baseSeverity", "UNKNOWN")
|
||||||
|
score = cvss.get("baseScore", "N/A")
|
||||||
|
elif "cvssMetricV30" in metrics:
|
||||||
|
cvss = metrics["cvssMetricV30"][0]["cvssData"]
|
||||||
|
severity = cvss.get("baseSeverity", "UNKNOWN")
|
||||||
|
score = cvss.get("baseScore", "N/A")
|
||||||
|
|
||||||
|
# Extract affected version ranges
|
||||||
|
affected_versions = []
|
||||||
|
for config in cve.get("configurations", []):
|
||||||
|
for node in config.get("nodes", []):
|
||||||
|
for match in node.get("cpeMatch", []):
|
||||||
|
affected_versions.append({
|
||||||
|
"criteria": match.get("criteria"),
|
||||||
|
"versionStartIncluding": match.get("versionStartIncluding"),
|
||||||
|
"versionStartExcluding": match.get("versionStartExcluding"),
|
||||||
|
"versionEndIncluding": match.get("versionEndIncluding"),
|
||||||
|
"versionEndExcluding": match.get("versionEndExcluding")
|
||||||
|
})
|
||||||
|
|
||||||
|
all_cves.append({
|
||||||
|
"cve_id": cve_id,
|
||||||
|
"description": description,
|
||||||
|
"severity": severity,
|
||||||
|
"cvss_score": score,
|
||||||
|
"affected_versions": affected_versions
|
||||||
|
})
|
||||||
|
|
||||||
|
start_index += RESULTS_PER_PAGE
|
||||||
|
if start_index >= total_results:
|
||||||
|
break
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
# Write results to file
|
||||||
|
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(all_cves, f, indent=2, ensure_ascii=False)
|
||||||
|
print(Fore.GREEN + f"[+] Saved {len(all_cves)} CVEs to {OUTPUT_FILE}")
|
||||||
|
|
||||||
|
# Perform local audit of current RouterOS version against cached CVE data
|
||||||
|
def run_cve_audit(connection):
|
||||||
|
# Banner
|
||||||
|
print(Fore.WHITE + "=" * 60)
|
||||||
|
print(Fore.WHITE + "[!] Checking CVE Vulnerabilities")
|
||||||
|
print(Fore.MAGENTA + "[!] In any case, validate results manually due to potential false positives.")
|
||||||
|
print(Fore.WHITE + "=" * 60)
|
||||||
|
|
||||||
|
# Retrieve RouterOS version from device
|
||||||
|
output = connection.send_command("/system resource print")
|
||||||
|
match = re.search(r"version:\s*([\w.\-]+)", output)
|
||||||
|
if not match:
|
||||||
|
print(Fore.RED + "[-] ERROR: Could not determine RouterOS version.")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_version = match.group(1)
|
||||||
|
current_v = normalize_version(current_version)
|
||||||
|
|
||||||
|
if not current_v:
|
||||||
|
print(Fore.RED + f"[-] ERROR: RouterOS version '{current_version}' is invalid.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(Fore.GREEN + f"[+] Detected RouterOS Version: {current_version}")
|
||||||
|
|
||||||
|
# Load or refresh CVE data
|
||||||
|
if not os.path.isfile(OUTPUT_FILE):
|
||||||
|
print(Fore.YELLOW + f"[!] {OUTPUT_FILE} not found.")
|
||||||
|
fetch_all_cves()
|
||||||
|
else:
|
||||||
|
print(Fore.YELLOW + f"[?] {OUTPUT_FILE} already exists.")
|
||||||
|
answer = input(Fore.YELLOW + " Overwrite it with fresh CVE data? [yes/no]: ").strip().lower()
|
||||||
|
if answer == "yes":
|
||||||
|
fetch_all_cves()
|
||||||
|
|
||||||
|
# Load local CVE file
|
||||||
|
try:
|
||||||
|
with open(OUTPUT_FILE, "r", encoding="utf-8") as f:
|
||||||
|
cve_data = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(Fore.RED + f"[-] Failed to load {OUTPUT_FILE}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# CVE match logic
|
||||||
|
counters = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
|
||||||
|
hits = []
|
||||||
|
|
||||||
|
for cve in cve_data:
|
||||||
|
matched = False
|
||||||
|
affected_versions = cve.get("affected_versions", [])
|
||||||
|
|
||||||
|
# Fallback: try parsing version from description if structured data is missing
|
||||||
|
if not affected_versions:
|
||||||
|
affected_versions = extract_ranges_from_description(cve.get("description", ""))
|
||||||
|
|
||||||
|
for version_info in affected_versions:
|
||||||
|
if is_version_affected(current_v, version_info):
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched:
|
||||||
|
hits.append(cve)
|
||||||
|
severity = cve.get("severity", "UNKNOWN").upper()
|
||||||
|
counters[severity] = counters.get(severity, 0) + 1
|
||||||
|
|
||||||
|
# Display summary
|
||||||
|
total = len(hits)
|
||||||
|
print(Fore.WHITE + f"[*] Total matching CVEs: {total}")
|
||||||
|
for level in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"]:
|
||||||
|
count = counters.get(level, 0)
|
||||||
|
if count > 0:
|
||||||
|
color = {
|
||||||
|
"CRITICAL": Fore.RED + Style.BRIGHT,
|
||||||
|
"HIGH": Fore.RED,
|
||||||
|
"MEDIUM": Fore.YELLOW,
|
||||||
|
"LOW": Fore.CYAN,
|
||||||
|
"UNKNOWN": Fore.WHITE
|
||||||
|
}[level]
|
||||||
|
print(color + f"[*] {level}: {count}")
|
||||||
|
|
||||||
|
# Print vulnerability details
|
||||||
|
if total > 0:
|
||||||
|
print(Fore.WHITE + "[*] Vulnerability details:")
|
||||||
|
for cve in hits:
|
||||||
|
severity = cve.get("severity", "UNKNOWN").upper()
|
||||||
|
description = cve.get("description", "").strip()
|
||||||
|
score = cve.get("cvss_score", "N/A")
|
||||||
|
color = {
|
||||||
|
"CRITICAL": Fore.RED + Style.BRIGHT,
|
||||||
|
"HIGH": Fore.RED,
|
||||||
|
"MEDIUM": Fore.YELLOW,
|
||||||
|
"LOW": Fore.CYAN,
|
||||||
|
"UNKNOWN": Fore.WHITE
|
||||||
|
}.get(severity, Fore.WHITE)
|
||||||
|
|
||||||
|
print(color + f"\n→ {cve['cve_id']} [{severity}]")
|
||||||
|
print(Fore.WHITE + " " + description)
|
||||||
|
print(Fore.WHITE + f" CVSS Score: {score}")
|
|
@ -1,72 +0,0 @@
|
||||||
# Sara's helper module for CVE search based on RouterOS version analysis
|
|
||||||
# Downloaded and adapted from: https://cve.mitre.org/data/downloads
|
|
||||||
# The CVE search thanks to this module is passive and does not involve sending various payloads, launching exploits and so on
|
|
||||||
|
|
||||||
# UPD: It's not the best realization at this point. I need to use the NIST NVD database without having to hardcode CVEs. This was not the best solution.
|
|
||||||
|
|
||||||
cve_routeros_database = {
|
|
||||||
"CVE-2008-0680": "SNMPd in MikroTik RouterOS 3.2 and earlier allows remote attackers to cause a denial of service (daemon crash) via a crafted SNMP SET request.",
|
|
||||||
"CVE-2008-6976": "MikroTik RouterOS 3.x through 3.13 and 2.x through 2.9.51 allows remote attackers to modify Network Management System (NMS) settings via a crafted SNMP set request.",
|
|
||||||
"CVE-2012-6050": "The winbox service in MikroTik RouterOS 5.15 and earlier allows remote attackers to cause a denial of service (CPU consumption), read the router version, and possibly have other impacts via a request to download the router's DLLs or plugins, as demonstrated by roteros.dll",
|
|
||||||
"CVE-2015-2350": "Cross-site request forgery (CSRF) vulnerability in MikroTik RouterOS 5.0 and earlier allows remote attackers to hijack the authentication of administrators for requests that change the administrator password via a request in the status page to /cfg.",
|
|
||||||
"CVE-2017-17537": "MikroTik RouterBOARD v6.39.2 and v6.40.5 allows an unauthenticated remote attacker to cause a denial of service by connecting to TCP port 53 and sending data that begins with many '\0' characters",
|
|
||||||
"CVE-2017-17538": "MikroTik v6.40.5 devices allow remote attackers to cause a denial of service via a flood of ICMP packets.",
|
|
||||||
"CVE-2017-6297": "The L2TP Client in MikroTik RouterOS versions 6.83.3 and 6.37.4 does not enable IPsec encryption after a reboot, which allows man-in-the-middle attackers to view transmitted data unencrypted and gain access to networks on the L2TP server by monitoring the packets for the transmitted data and obtaining the L2TP secret",
|
|
||||||
"CVE-2017-6444": "The MikroTik Router hAP Lite 6.25 has no protection mechanism for unsolicited TCP ACK packets in the case of a fast network connection",
|
|
||||||
"CVE-2017-7285": "A vulnerability in the network stack of MikroTik Version 6.38.5 released 2017-03-09 could allow an unauthenticated remote attacker to exhaust all available CPU via a flood of TCP RST packets",
|
|
||||||
"CVE-2017-8338": "A vulnerability in MikroTik Version 6.38.5 could allow an unauthenticated remote attacker to exhaust all available CPU via a flood of UDP packets on port 500 (used for L2TP over IPsec)",
|
|
||||||
"CVE-2018-10066": "An issue was discovered in MikroTik RouterOS 6.41.4. Missing OpenVPN server certificate verification allows a remote unauthenticated attacker capable of intercepting client traffic to act as a malicious OpenVPN server. This may allow the attacker to gain access to the client's internal network",
|
|
||||||
"CVE-2018-10070": "A vulnerability in MikroTik Version 6.41.4 could allow an unauthenticated remote attacker to exhaust all available CPU and all available RAM by sending a crafted FTP request on port 21 that begins with many '\0' characters",
|
|
||||||
"CVE-2018-1157": "Mikrotik RouterOS before 6.42.7 and 6.40.9 is vulnerable to a memory exhaustion vulnerability. An authenticated remote attacker can crash the HTTP server and in some circumstances reboot the system via a crafted HTTP POST request.",
|
|
||||||
"CVE-2018-1158": "Mikrotik RouterOS before 6.42.7 and 6.40.9 is vulnerable to a stack exhaustion vulnerability. An authenticated remote attacker can crash the HTTP server via recursive parsing of JSON.",
|
|
||||||
"CVE-2018-14847": "MikroTik RouterOS through 6.42 allows unauthenticated remote attackers to read arbitrary files and remote authenticated attackers to write arbitrary files due to a directory traversal vulnerability in the WinBox interface.",
|
|
||||||
"CVE-2018-7445": "A buffer overflow was found in the MikroTik RouterOS SMB service when processing NetBIOS session request messages. Remote attackers with access to the service can exploit this vulnerability and gain code execution on the system. The overflow occurs before authentication takes place",
|
|
||||||
"CVE-2019-13074": "A vulnerability in the FTP daemon on MikroTik routers through 6.44.3 could allow remote attackers to exhaust all available memory, causing the device to reboot because of uncontrolled resource management.",
|
|
||||||
"CVE-2019-15055": "MikroTik RouterOS through 6.44.5 and 6.45.x through 6.45.3 improperly handles the disk name, which allows authenticated users to delete arbitrary files. Attackers can exploit this vulnerability to reset credential storage, which allows them access to the management interface as an administrator without authentication",
|
|
||||||
"CVE-2019-16160": "An integer underflow in the SMB server of MikroTik RouterOS before 6.45.5 allows remote unauthenticated attackers to crash the service.",
|
|
||||||
"CVE-2019-3924": "MikroTik RouterOS before 6.43.12 (stable) and 6.42.12 (long-term) is vulnerable to an intermediary vulnerability. The software will execute user defined network requests to both WAN and LAN clients. A remote unauthenticated attacker can use this vulnerability to bypass the router's firewall or for general network scanning activities.",
|
|
||||||
"CVE-2019-3943": "MikroTik RouterOS versions Stable 6.43.12 and below, Long-term 6.42.12 and below, and Testing 6.44beta75 and below are vulnerable to an authenticated, remote directory traversal via the HTTP or Winbox interfaces. An authenticated, remote attack can use this vulnerability to read and write files outside of the sandbox directory (/rw/disk)",
|
|
||||||
"CVE-2019-3978": "RouterOS versions 6.45.6 Stable, 6.44.5 Long-term, and below allow remote unauthenticated attackers to trigger DNS queries via port 8291. The queries are sent from the router to a server of the attacker's choice. The DNS responses are cached by the router, potentially resulting in cache poisoning",
|
|
||||||
"CVE-2019-3981": "MikroTik Winbox 3.20 and below is vulnerable to man in the middle attacks. A man in the middle can downgrade the client's authentication protocol and recover the user's username and MD5 hashed password.",
|
|
||||||
"CVE-2020-10364": "The SSH daemon on MikroTik routers through 6.44.3 could allow remote attackers to generate CPU activity, trigger refusal of new authorized connections, and cause a reboot via connect and write system calls, because of uncontrolled resource management",
|
|
||||||
"CVE-2020-11881": "An array index error in MikroTik RouterOS 6.41.3 through 6.46.5, and 7.x through 7.0 Beta5, allows an unauthenticated remote attacker to crash the SMB server via modified setup-request packets,",
|
|
||||||
"CVE-2020-20021": "An issue discovered in MikroTik Router 6.46.3 and earlier allows attacker to cause denial of service via misconfiguration in the SSH daemon.",
|
|
||||||
"CVE-2020-20214": "MikroTik RouterOS 6.44.6 (long-term tree) suffers from an assertion failure vulnerability in the btest process. An authenticated remote attacker can cause a Denial of Service due to an assertion failure via a crafted packet.",
|
|
||||||
"CVE-2020-20217": "MikroTik RouterOS before 6.47 (stable tree) suffers from an uncontrolled resource consumption vulnerability in the /nova/bin/route process. An authenticated remote attacker can cause a Denial of Service due to overloading the systems CPU.",
|
|
||||||
"CVE-2020-20220": "MikroTik RouterOS prior to stable 6.47 suffers from a memory corruption vulnerability in the /nova/bin/bfd process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference).",
|
|
||||||
"CVE-2020-20222": "MikroTik RouterOS 6.44.6 (long-term tree) suffers from a memory corruption vulnerability in the /nova/bin/sniffer process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference).",
|
|
||||||
"CVE-2020-20225": "MikroTik RouterOS before 6.47 (stable tree) suffers from an assertion failure vulnerability in the /nova/bin/user process. An authenticated remote attacker can cause a Denial of Service due to an assertion failure via a crafted packet.",
|
|
||||||
"CVE-2020-20227": "MikroTik RouterOS stable 6.47 suffers from a memory corruption vulnerability in the /nova/bin/diskd process. An authenticated remote attacker can cause a Denial of Service due to invalid memory access.",
|
|
||||||
"CVE-2020-20230": "MikroTik RouterOS before stable 6.47 suffers from an uncontrolled resource consumption in the sshd process. An authenticated remote attacker can cause a Denial of Service due to overloading the systems CPU.",
|
|
||||||
"CVE-2020-20231": "MikroTik RouterOS through stable version 6.48.3 suffers from a memory corruption vulnerability in the /nova/bin/detnet process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference).",
|
|
||||||
"CVE-2020-20236": "MikroTik RouterOS 6.46.3 (stable tree) suffers from a memory corruption vulnerability in the /nova/bin/sniffer process. An authenticated remote attacker can cause a Denial of Service due to improper memory access.",
|
|
||||||
"CVE-2020-20237": "Mikrotik RouterOS 6.46.3 (stable tree) suffers from a memory corruption vulnerability in the /nova/bin/sniffer process. An authenticated remote attacker can cause a Denial of Service due to improper memory access.",
|
|
||||||
"CVE-2020-20245": "Mikrotik RouterOS stable 6.46.3 suffers from a memory corruption vulnerability in the log process. An authenticated remote attacker can cause a Denial of Service due to improper memory access.",
|
|
||||||
"CVE-2020-20246": "Mikrotik RouterOS stable 6.46.3 suffers from a memory corruption vulnerability in the mactel process. An authenticated remote attacker can cause a Denial of Service due to improper memory access.",
|
|
||||||
"CVE-2020-20248": "Mikrotik RouterOS before stable 6.47 suffers from an uncontrolled resource consumption in the memtest process. An authenticated remote attacker can cause a Denial of Service due to overloading the systems CPU.",
|
|
||||||
"CVE-2020-20249": "Mikrotik RouterOS before stable 6.47 suffers from a memory corruption vulnerability in the resolver process. By sending a crafted packet",
|
|
||||||
"CVE-2020-20250": "Mikrotik RouterOS before stable version 6.47 suffers from a memory corruption vulnerability in the /nova/bin/lcdstat process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference) NOTE: this is different from CVE-2020-20253 and CVE-2020-20254. All four vulnerabilities in the /nova/bin/lcdstat process are discussed in the CVE-2020-20250",
|
|
||||||
"CVE-2020-20252": "Mikrotik RouterOS before stable version 6.47 suffers from a memory corruption vulnerability in the /nova/bin/lcdstat process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference)",
|
|
||||||
"CVE-2020-20253": "Mikrotik RouterOS before 6.47 (stable tree) suffers from a divison by zero vulnerability in the /nova/bin/lcdstat process. An authenticated remote attacker can cause a Denial of Service due to a divide by zero error.",
|
|
||||||
"CVE-2020-20254": "Mikrotik RouterOS before 6.47 (stable tree) suffers from a memory corruption vulnerability in the /nova/bin/lcdstat process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference).",
|
|
||||||
"CVE-2020-20262": "Mikrotik RouterOS before 6.47 (stable tree) suffers from an assertion failure vulnerability in the /ram/pckg/security/nova/bin/ipsec process. An authenticated remote attacker can cause a Denial of Service due to an assertion failure via a crafted packet.",
|
|
||||||
"CVE-2020-20264": "Mikrotik RouterOS before 6.47 (stable tree) in the /ram/pckg/advanced-tools/nova/bin/netwatch process. An authenticated remote attacker can cause a Denial of Service due to a divide by zero error.",
|
|
||||||
"CVE-2020-20265": "Mikrotik RouterOS before 6.47 (stable tree) suffers from a memory corruption vulnerability in the /ram/pckg/wireless/nova/bin/wireless process. An authenticated remote attacker can cause a Denial of Service due via a crafted packet.",
|
|
||||||
"CVE-2020-20266": "Mikrotik RouterOS before 6.47 (stable tree) suffers from a memory corruption vulnerability in the /nova/bin/dot1x process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference).",
|
|
||||||
"CVE-2020-5720": "MikroTik WinBox before 3.21 is vulnerable to a path traversal vulnerability that allows creation of arbitrary files wherevere WinBox has write permissions. WinBox is vulnerable to this attack if it connects to a malicious endpoint or if an attacker mounts a man in the middle attack.",
|
|
||||||
"CVE-2020-5721": "MikroTik WinBox 3.22 and below stores the user's cleartext password in the settings.cfg.viw configuration file when the Keep Password field is set and no Master Password is set. Keep Password is set by default and",
|
|
||||||
"CVE-2021-27221": "MikroTik RouterOS 6.47.9 allows remote authenticated ftp users to create or overwrite arbitrary .rsc files via the /export command. NOTE: the vendor's position is that this is intended behavior because of how user policies work.",
|
|
||||||
"CVE-2021-3014": "MikroTik RouterOS through 6.48 is vulnerable to XSS in the hotspot login page via the target parameter",
|
|
||||||
"CVE-2021-36613": "MikroTik RouterOS before stable 6.48.2 suffers from a memory corruption vulnerability in the ptp process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference)",
|
|
||||||
"CVE-2021-36614": "MikroTik RouterOS before stable 6.48.2 suffers from a memory corruption vulnerability in the tr069-client process. An authenticated remote attacker can cause a Denial of Service (NULL pointer dereference)",
|
|
||||||
"CVE-2022-34960": "The container package in MikroTik RouterOS 7.4beta4 allows an attacker to create mount points pointing to symbolic links",
|
|
||||||
"CVE-2022-36522": "Mikrotik RouterOS through stable 6.48.3 was discovered to contain an assertion failure in the component /advanced-tools/nova/bin/netwatch. This vulnerability allows attackers to cause a Denial of Service (DoS) via a crafted packet.",
|
|
||||||
"CVE-2022-45313": "Mikrotik RouterOS before stable 7.5 was discovered to contain an out-of-bounds read in the hotspot process. This vulnerability allows attackers to execute arbitrary code via a crafted nova message.",
|
|
||||||
"CVE-2022-45315": "Mikrotik RouterOS before stable 7.6 was discovered to contain an out-of-bounds read in the snmp process. This vulnerability allows attackers to execute arbitrary code via a crafted packet.",
|
|
||||||
"CVE-2023-24094": "An issue in the bridge2 component of MikroTik RouterOS v6.40.5 allows attackers to cause a Denial of Service (DoS) via crafted packets.",
|
|
||||||
"CVE-2023-30799": "MikroTik RouterOS stable before 6.49.7 and long-term through 6.48.6 are vulnerable to a privilege escalation issue. A remote and authenticated attacker can escalate privileges from admin to super-admin on the Winbox or HTTP interface. The attacker can abuse this vulnerability to execute arbitrary code on the system.",
|
|
||||||
"CVE-2023-30800": "The web server used by MikroTik RouterOS version 6 is affected by a heap memory corruption issue. A remote and unauthenticated attacker can corrupt the server's heap memory by sending a crafted HTTP request. As a result",
|
|
||||||
"CVE-2023-41570": "MikroTik RouterOS v7.1 to 7.11 was discovered to contain incorrect access control mechanisms in place for the Rest API.",
|
|
||||||
"CVE-2024-38861": "Improper Certificate Validation in Checkmk Exchange plugin MikroTik allows attackers in MitM position to intercept traffic. This issue affects MikroTik: from 2.0.0 through 2.5.5, from 0.4a_mk through 2.0a.",
|
|
||||||
"CVE-2024-54772": "An issue was discovered in the Winbox service of MikroTik RouterOS long-term release v6.43.13 through v6.49.13 and stable v6.43 through v7.17.2. A patch is available in the stable release v6.49.18. A discrepancy in response size between connection attempts made with a valid username and those with an invalid username allows attackers to enumerate for valid accounts.",
|
|
||||||
}
|
|
331
sara.py
331
sara.py
|
@ -2,17 +2,13 @@
|
||||||
|
|
||||||
# Copyright (c) 2025 Magama Bazarov
|
# Copyright (c) 2025 Magama Bazarov
|
||||||
# Licensed under the Apache 2.0 License
|
# Licensed under the Apache 2.0 License
|
||||||
|
# This project is not affiliated with or endorsed by MikroTik
|
||||||
|
|
||||||
# Connecting required libraries and cve_lookup module
|
import argparse, colorama, time, re, sys
|
||||||
import argparse
|
|
||||||
import colorama
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from netmiko import ConnectHandler
|
from netmiko import ConnectHandler
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
from cve_lookup import cve_routeros_database
|
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
|
from cve_analyzer import run_cve_audit
|
||||||
|
|
||||||
# Initialize colorama for colored console output
|
# Initialize colorama for colored console output
|
||||||
colorama.init(autoreset=True)
|
colorama.init(autoreset=True)
|
||||||
|
@ -28,14 +24,11 @@ def banner():
|
||||||
"""
|
"""
|
||||||
# Display the program banner and metadata
|
# Display the program banner and metadata
|
||||||
print(banner_text)
|
print(banner_text)
|
||||||
print(" " + Fore.YELLOW + "RouterOS Security Inspector. For security engineers")
|
print(" " + Fore.YELLOW + "RouterOS Security Inspector. Designed for security engineers")
|
||||||
print(" " + Fore.YELLOW + "Operates remotely using SSH, designed to evaluate RouterOS security\n")
|
|
||||||
print(" " + Fore.YELLOW + "Author: " + Style.RESET_ALL + "Magama Bazarov, <magamabazarov@mailbox.org>")
|
print(" " + Fore.YELLOW + "Author: " + Style.RESET_ALL + "Magama Bazarov, <magamabazarov@mailbox.org>")
|
||||||
print(" " + Fore.YELLOW + "Alias: " + Style.RESET_ALL + "Caster")
|
print(" " + Fore.YELLOW + "Alias: " + Style.RESET_ALL + "Caster")
|
||||||
print(" " + Fore.YELLOW + "Version: " + Style.RESET_ALL + "1.1.0")
|
print(" " + Fore.YELLOW + "Version: " + Style.RESET_ALL + "1.2")
|
||||||
print(" " + Fore.YELLOW + "Codename: " + Style.RESET_ALL + "Judge")
|
print(" " + Fore.YELLOW + "Documentation & Usage: " + Style.RESET_ALL + "https://github.com/casterbyte/Sara\n")
|
||||||
print(" " + Fore.YELLOW + "Documentation & Usage: " + Style.RESET_ALL + "https://github.com/casterbyte/Sara")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Display a legal disclaimer to emphasize responsible usage
|
# Display a legal disclaimer to emphasize responsible usage
|
||||||
print(" " + Fore.YELLOW + "[!] DISCLAIMER: Use this tool only for auditing your own devices.")
|
print(" " + Fore.YELLOW + "[!] DISCLAIMER: Use this tool only for auditing your own devices.")
|
||||||
|
@ -55,7 +48,7 @@ def connect_to_router(ip, username, password, port, key_file, passphrase):
|
||||||
"passphrase": passphrase,
|
"passphrase": passphrase,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
print(Fore.GREEN + Style.BRIGHT + f"[*] Connecting to RouterOS at {ip}:{port}")
|
print(Fore.WHITE + f"[*] Connecting to RouterOS at {ip}:{port}")
|
||||||
connection = ConnectHandler(**device)
|
connection = ConnectHandler(**device)
|
||||||
print(Fore.WHITE + "[*] Connection successful!")
|
print(Fore.WHITE + "[*] Connection successful!")
|
||||||
return connection
|
return connection
|
||||||
|
@ -72,28 +65,7 @@ def parse_version(version_str):
|
||||||
# Parses a version string into a comparable Version object. Example: "6.49.7" → Version(6.49.7)
|
# Parses a version string into a comparable Version object. Example: "6.49.7" → Version(6.49.7)
|
||||||
return Version(version_str)
|
return Version(version_str)
|
||||||
|
|
||||||
def extract_version_from_cve(description):
|
# Retrieves the RouterOS version
|
||||||
# Case: "X.Y to Z.W"
|
|
||||||
range_match = re.search(r"v?(\d+\.\d+(?:\.\d+)?)\s*to\s*v?(\d+\.\d+(?:\.\d+)?)", description, re.IGNORECASE)
|
|
||||||
if range_match:
|
|
||||||
start_version, end_version = range_match.groups()
|
|
||||||
return "range", parse_version(start_version), parse_version(end_version)
|
|
||||||
|
|
||||||
# Case: "before X.Y.Z", "after X.Y.Z", "through X.Y.Z", "and below X.Y.Z"
|
|
||||||
keyword_match = re.search(r"(before|through|after|and below)?\s*v?(\d+\.\d+(?:\.\d+)?)", description, re.IGNORECASE)
|
|
||||||
if keyword_match:
|
|
||||||
keyword, version = keyword_match.groups()
|
|
||||||
return keyword, None, parse_version(version)
|
|
||||||
|
|
||||||
# Case: "6.49.x" (Wildcard notation)
|
|
||||||
wildcard_match = re.search(r"v?(\d+\.\d+)\.x", description, re.IGNORECASE)
|
|
||||||
if wildcard_match:
|
|
||||||
base_version = wildcard_match.group(1) # Example: "6.49"
|
|
||||||
return "before", None, parse_version(base_version + ".999") # Treat as "6.49.999" for comparison
|
|
||||||
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
# Retrieves the RouterOS version and checks for known CVEs
|
|
||||||
def check_routeros_version(connection):
|
def check_routeros_version(connection):
|
||||||
# Separator outlet
|
# Separator outlet
|
||||||
separator("Checking RouterOS Version")
|
separator("Checking RouterOS Version")
|
||||||
|
@ -104,36 +76,6 @@ def check_routeros_version(connection):
|
||||||
if match:
|
if match:
|
||||||
routeros_version = parse_version(match.group(1))
|
routeros_version = parse_version(match.group(1))
|
||||||
print(Fore.GREEN + f"[+] Detected RouterOS Version: {routeros_version}")
|
print(Fore.GREEN + f"[+] Detected RouterOS Version: {routeros_version}")
|
||||||
|
|
||||||
found_cves = []
|
|
||||||
|
|
||||||
for cve, description in cve_routeros_database.items():
|
|
||||||
keyword, start_version, end_version = extract_version_from_cve(description)
|
|
||||||
|
|
||||||
if keyword == "range" and start_version and end_version:
|
|
||||||
if start_version <= routeros_version <= end_version:
|
|
||||||
found_cves.append((cve, description))
|
|
||||||
|
|
||||||
elif keyword and end_version:
|
|
||||||
if keyword == "before" and routeros_version < end_version:
|
|
||||||
found_cves.append((cve, description))
|
|
||||||
elif keyword == "through" and routeros_version <= end_version:
|
|
||||||
found_cves.append((cve, description))
|
|
||||||
elif keyword == "after" and routeros_version > end_version:
|
|
||||||
found_cves.append((cve, description))
|
|
||||||
elif keyword == "and below" and routeros_version <= end_version:
|
|
||||||
found_cves.append((cve, description))
|
|
||||||
|
|
||||||
# Direct version match
|
|
||||||
elif str(routeros_version) in description:
|
|
||||||
found_cves.append((cve, description))
|
|
||||||
|
|
||||||
if found_cves:
|
|
||||||
print(Fore.YELLOW + f"[!] CAUTION: Found {len(found_cves)} CVEs affecting RouterOS {routeros_version}!")
|
|
||||||
for cve, description in found_cves:
|
|
||||||
print(Fore.RED + f" - {cve}: {description}")
|
|
||||||
else:
|
|
||||||
print(Fore.GREEN + "[+] No known CVEs found for this version.")
|
|
||||||
else:
|
else:
|
||||||
print(Fore.RED + Style.BRIGHT + "[-] ERROR: Could not determine RouterOS version.")
|
print(Fore.RED + Style.BRIGHT + "[-] ERROR: Could not determine RouterOS version.")
|
||||||
|
|
||||||
|
@ -145,10 +87,10 @@ def check_smb(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "enabled: yes" in output:
|
if "enabled: yes" in output:
|
||||||
print(Fore.RED + Style.BRIGHT + "[*] CAUTION: SMB service is enabled! Did you turn it on? Do you need SMB? Also avoid CVE-2018-7445")
|
print(Fore.RED + "[*] CAUTION: SMB service is enabled! Did you turn it on? Do you need SMB? Also avoid CVE-2018-7445")
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] SMB is disabled. No risk detected.")
|
print(Fore.GREEN + "[+] SMB is disabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Check for high-risk remote management interfaces (RMI)
|
# Check for high-risk remote management interfaces (RMI)
|
||||||
def check_rmi_services(connection):
|
def check_rmi_services(connection):
|
||||||
|
@ -173,7 +115,7 @@ def check_rmi_services(connection):
|
||||||
display_name = service_name.upper().replace("WWW", "HTTP").replace("WWW-SSL", "HTTPS")
|
display_name = service_name.upper().replace("WWW", "HTTP").replace("WWW-SSL", "HTTPS")
|
||||||
|
|
||||||
if service_name in high_risk:
|
if service_name in high_risk:
|
||||||
print(Fore.RED + Style.BRIGHT + f"[!] ALERT: {display_name} is ENABLED! This is a high security risk.")
|
print(Fore.RED + f"[!] ALERT: {display_name} is ENABLED! This is a high security risk.")
|
||||||
if service_name == "ftp":
|
if service_name == "ftp":
|
||||||
print(Fore.RED + " - Are you sure you need FTP?")
|
print(Fore.RED + " - Are you sure you need FTP?")
|
||||||
if service_name == "telnet":
|
if service_name == "telnet":
|
||||||
|
@ -183,13 +125,13 @@ def check_rmi_services(connection):
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
elif service_name in moderate_risk:
|
elif service_name in moderate_risk:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + f"[!] CAUTION: {display_name} is enabled.")
|
print(Fore.YELLOW + f"[!] CAUTION: {display_name} is enabled.")
|
||||||
if service_name in ["api", "api-ssl"]:
|
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.")
|
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":
|
elif service_name == "www-ssl":
|
||||||
print(Fore.GREEN + " - HTTPS detected. Ensure it uses a valid certificate and strong encryption.")
|
print(Fore.GREEN + " - HTTPS detected. Ensure it uses a valid certificate and strong encryption.")
|
||||||
elif service_name == "winbox":
|
elif service_name == "winbox":
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: If you're using 'Keep Password' in Winbox, your credentials may be stored in plaintext!")
|
print(Fore.RED + "[!] CAUTION: If you're using 'Keep Password' in Winbox, your credentials may be stored in plaintext!")
|
||||||
print(Fore.YELLOW + " - If your PC is compromised, attackers can extract saved credentials.")
|
print(Fore.YELLOW + " - If your PC is compromised, attackers can extract saved credentials.")
|
||||||
print(Fore.YELLOW + " - Consider disabling 'Keep Password' to improve security.")
|
print(Fore.YELLOW + " - Consider disabling 'Keep Password' to improve security.")
|
||||||
|
|
||||||
|
@ -199,7 +141,7 @@ def check_rmi_services(connection):
|
||||||
|
|
||||||
if not risks_found:
|
if not risks_found:
|
||||||
print(Fore.GREEN + "[+] No high-risk RMI services enabled.")
|
print(Fore.GREEN + "[+] No high-risk RMI services enabled.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Check for default usernames that could be security risks
|
# Check for default usernames that could be security risks
|
||||||
def check_default_users(connection):
|
def check_default_users(connection):
|
||||||
|
@ -216,7 +158,7 @@ def check_default_users(connection):
|
||||||
if match:
|
if match:
|
||||||
username = match.group(1).lower()
|
username = match.group(1).lower()
|
||||||
if username in default_users:
|
if username in default_users:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + f"[!] CAUTION: Default username '{username}' detected! Change it to a unique one.")
|
print(Fore.YELLOW + f"[!] CAUTION: Default username '{username}' detected! Change it to a unique one.")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
if not risks_found:
|
if not risks_found:
|
||||||
print(Fore.GREEN + "[+] No default usernames found.")
|
print(Fore.GREEN + "[+] No default usernames found.")
|
||||||
|
@ -240,12 +182,12 @@ def checking_access_to_RMI(connection):
|
||||||
if address_match:
|
if address_match:
|
||||||
address_list = address_match.group(1).split(",")
|
address_list = address_match.group(1).split(",")
|
||||||
if not address_list or address_list == [""] or "0.0.0.0/0" in address_list:
|
if not address_list or address_list == [""] or "0.0.0.0/0" in address_list:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + f"[!] CAUTION: {service_name.upper()} is exposed to the entire network! Restrict access to trusted IP ranges.")
|
print(Fore.YELLOW + f"[!] CAUTION: {service_name.upper()} is exposed to the entire network! Restrict access to trusted IP ranges.")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + f"[+] OK! {service_name.upper()} is restricted to: {', '.join(address_list)}")
|
print(Fore.GREEN + f"[+] OK! {service_name.upper()} is restricted to: {', '.join(address_list)}")
|
||||||
else:
|
else:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + f"[!] CAUTION: {service_name.upper()} has no IP restriction set! Please restrict access.")
|
print(Fore.RED + f"[!] CAUTION: {service_name.upper()} has no IP restriction set! Please restrict access.")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
if not risks_found:
|
if not risks_found:
|
||||||
|
@ -284,7 +226,7 @@ def check_wifi_security(connection):
|
||||||
wps = wps_match.group(1) if wps_match else None # Fix: If WPS is not found, set None
|
wps = wps_match.group(1) if wps_match else None # Fix: If WPS is not found, set None
|
||||||
|
|
||||||
if pmkid == "no":
|
if pmkid == "no":
|
||||||
print(Fore.RED + Style.BRIGHT + f"[!] ALERT: Wi-Fi '{name}' has insecure settings!")
|
print(Fore.RED + f"[!] ALERT: Wi-Fi '{name}' has insecure settings!")
|
||||||
print(Fore.RED + " - PMKID attack is possible (disable-pmkid=no)")
|
print(Fore.RED + " - PMKID attack is possible (disable-pmkid=no)")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
|
@ -308,7 +250,7 @@ def check_wifi_security(connection):
|
||||||
pmkid = pmkid_match.group(1) if pmkid_match else "unknown"
|
pmkid = pmkid_match.group(1) if pmkid_match else "unknown"
|
||||||
|
|
||||||
if pmkid == "no":
|
if pmkid == "no":
|
||||||
print(Fore.RED + Style.BRIGHT + f"[!] ALERT: Security Profile '{profile_name}' allows PMKID attack! (disable-pmkid=no)")
|
print(Fore.RED + f"[!] ALERT: Security Profile '{profile_name}' allows PMKID attack! (disable-pmkid=no)")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
# /interface wifi security print (ROS v7.10+ only)
|
# /interface wifi security print (ROS v7.10+ only)
|
||||||
|
@ -327,7 +269,7 @@ def check_wifi_security(connection):
|
||||||
wps = wps_match.group(1) if wps_match else None # Fix: Avoid "WPS is enabled (unknown)"
|
wps = wps_match.group(1) if wps_match else None # Fix: Avoid "WPS is enabled (unknown)"
|
||||||
|
|
||||||
if pmkid == "no":
|
if pmkid == "no":
|
||||||
print(Fore.RED + Style.BRIGHT + f"[!] ALERT: Wi-Fi security profile '{sec_name}' has insecure settings!")
|
print(Fore.RED + f"[!] ALERT: Wi-Fi security profile '{sec_name}' has insecure settings!")
|
||||||
print(Fore.RED + " - PMKID attack is possible (disable-pmkid=no)")
|
print(Fore.RED + " - PMKID attack is possible (disable-pmkid=no)")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
|
@ -345,7 +287,7 @@ def check_wifi_security(connection):
|
||||||
if not risks_found:
|
if not risks_found:
|
||||||
print(Fore.GREEN + "[+] All Wi-Fi interfaces and security profiles have secure settings.")
|
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.")
|
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.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Check if UPnP is enabled
|
# Check if UPnP is enabled
|
||||||
def check_upnp_status(connection):
|
def check_upnp_status(connection):
|
||||||
|
@ -355,10 +297,10 @@ def check_upnp_status(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "enabled: yes" in output:
|
if "enabled: yes" in output:
|
||||||
print(Fore.RED + Style.BRIGHT + "[!] 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?")
|
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?")
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] UPnP is disabled. No risk detected.")
|
print(Fore.GREEN + "[+] UPnP is disabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Check if the router is acting as a DNS server
|
# Check if the router is acting as a DNS server
|
||||||
def check_dns_status(connection):
|
def check_dns_status(connection):
|
||||||
|
@ -368,10 +310,10 @@ def check_dns_status(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "allow-remote-requests: yes" in output:
|
if "allow-remote-requests: yes" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] 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.")
|
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.")
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] DNS remote requests are disabled. No risk detected.")
|
print(Fore.GREEN + "[+] DNS remote requests are disabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Check DDNS Settings
|
# Check DDNS Settings
|
||||||
def check_ddns_status(connection):
|
def check_ddns_status(connection):
|
||||||
|
@ -381,10 +323,10 @@ def check_ddns_status(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "ddns-enabled: yes" in output:
|
if "ddns-enabled: yes" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: Dynamic DNS is enabled! Are you sure you need it?")
|
print(Fore.YELLOW + "[!] CAUTION: Dynamic DNS is enabled! Are you sure you need it?")
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] DDNS is disabled. No risk detected.")
|
print(Fore.GREEN + "[+] DDNS is disabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Detect active PoE interfaces that might pose a risk to connected devices
|
# Detect active PoE interfaces that might pose a risk to connected devices
|
||||||
def check_poe_status(connection):
|
def check_poe_status(connection):
|
||||||
|
@ -403,12 +345,12 @@ def check_poe_status(connection):
|
||||||
poe = poe_match.group(1) if poe_match else "none"
|
poe = poe_match.group(1) if poe_match else "none"
|
||||||
|
|
||||||
if poe in ["auto-on", "forced-on"]:
|
if poe in ["auto-on", "forced-on"]:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + f"[!] CAUTION: PoE is enabled on {name}. Ensure that connected devices support PoE to prevent damage.")
|
print(Fore.YELLOW + f"[!] CAUTION: PoE is enabled on {name}. Ensure that connected devices support PoE to prevent damage.")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
if not risks_found:
|
if not risks_found:
|
||||||
print(Fore.GREEN + "[+] No PoE-enabled interfaces detected.")
|
print(Fore.GREEN + "[+] No PoE-enabled interfaces detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Checking RouterBOOT
|
# Checking RouterBOOT
|
||||||
def check_routerboot_protection(connection):
|
def check_routerboot_protection(connection):
|
||||||
|
@ -418,10 +360,10 @@ def check_routerboot_protection(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "protected-routerboot: disabled" in output:
|
if "protected-routerboot: disabled" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: RouterBOOT protection is disabled! This can allow unauthorized firmware changes and password resets via Netinstall.")
|
print(Fore.YELLOW + "[!] CAUTION: RouterBOOT protection is disabled! This can allow unauthorized firmware changes and password resets via Netinstall.")
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] RouterBOOT protection is enabled. No risk detected.")
|
print(Fore.GREEN + "[+] RouterBOOT protection is enabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
def check_socks_status(connection):
|
def check_socks_status(connection):
|
||||||
separator("Checking SOCKS Proxy Status")
|
separator("Checking SOCKS Proxy Status")
|
||||||
|
@ -429,10 +371,10 @@ def check_socks_status(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "enabled: yes" in output:
|
if "enabled: yes" in output:
|
||||||
print(Fore.RED + Style.BRIGHT + "[!] ALERT: SOCKS proxy is enabled! This may indicate a possible compromise of the device, the entry point to the internal network.")
|
print(Fore.RED + "[!] ALERT: SOCKS proxy is enabled! This may indicate a possible compromise of the device, the entry point to the internal network.")
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] SOCKS proxy is disabled. No risk detected.")
|
print(Fore.GREEN + "[+] SOCKS proxy is disabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Verify if RouterBOOT protection is enabled to prevent unauthorized firmware modifications
|
# Verify if RouterBOOT protection is enabled to prevent unauthorized firmware modifications
|
||||||
def check_bandwidth_server_status(connection):
|
def check_bandwidth_server_status(connection):
|
||||||
|
@ -442,10 +384,10 @@ def check_bandwidth_server_status(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "enabled: yes" in output:
|
if "enabled: yes" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: Bandwidth server is enabled! Possible unwanted traffic, possible CPU load.")
|
print(Fore.YELLOW + "[!] CAUTION: Bandwidth server is enabled! Possible unwanted traffic, possible CPU load.")
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] Bandwidth server is disabled. No risk detected.")
|
print(Fore.GREEN + "[+] Bandwidth server is disabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Analyze discovery protocols (CDP, LLDP, MNDP) that might expose network information
|
# Analyze discovery protocols (CDP, LLDP, MNDP) that might expose network information
|
||||||
def check_neighbor_discovery(connection):
|
def check_neighbor_discovery(connection):
|
||||||
|
@ -455,15 +397,15 @@ def check_neighbor_discovery(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "discover-interface-list: all" in output:
|
if "discover-interface-list: all" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: RouterOS sends Discovery protocol packets to all interfaces. This can be used by an attacker to gather data about RouterOS.")
|
print(Fore.YELLOW + "[!] CAUTION: RouterOS sends Discovery protocol packets to all interfaces. This can be used by an attacker to gather data about RouterOS.")
|
||||||
|
|
||||||
protocol_match = re.search(r'protocol: ([\w,]+)', output)
|
protocol_match = re.search(r'protocol: ([\w,]+)', output)
|
||||||
if protocol_match:
|
if protocol_match:
|
||||||
protocols = protocol_match.group(1)
|
protocols = protocol_match.group(1)
|
||||||
print(Fore.YELLOW + Style.BRIGHT + f"[!] Neighbor Discovery Protocols enabled: {protocols}")
|
print(Fore.YELLOW + f"[!] Neighbor Discovery Protocols enabled: {protocols}")
|
||||||
if "discover-interface-list: all" not in output and not protocol_match:
|
if "discover-interface-list: all" not in output and not protocol_match:
|
||||||
print(Fore.GREEN + "[+] No security risks found in Neighbor Discovery Protocol settings.")
|
print(Fore.GREEN + "[+] No security risks found in Neighbor Discovery Protocol settings.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Ensure a minimum password length policy is enforced
|
# Ensure a minimum password length policy is enforced
|
||||||
def check_password_length_policy(connection):
|
def check_password_length_policy(connection):
|
||||||
|
@ -473,10 +415,10 @@ def check_password_length_policy(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "minimum-password-length: 0" in output:
|
if "minimum-password-length: 0" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: No minimum password length is enforced! The length of the created passwords must be taken into account.")
|
print(Fore.YELLOW + "[!] CAUTION: No minimum password length is enforced! The length of the created passwords must be taken into account.")
|
||||||
if "minimum-password-length: 0" not in output:
|
if "minimum-password-length: 0" not in output:
|
||||||
print(Fore.GREEN + "[+] Password policy is enforced. No risk detected.")
|
print(Fore.GREEN + "[+] Password policy is enforced. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Analyze SSH security settings, including strong encryption and port forwarding risks
|
# Analyze SSH security settings, including strong encryption and port forwarding risks
|
||||||
def check_ssh_security(connection):
|
def check_ssh_security(connection):
|
||||||
|
@ -486,15 +428,15 @@ def check_ssh_security(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "forwarding-enabled: both" in output:
|
if "forwarding-enabled: both" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] 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.")
|
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.")
|
||||||
if "strong-crypto: no" in output:
|
if "strong-crypto: no" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: strong-crypto is disabled! It is recommended to enable it to enhance security. This will:")
|
print(Fore.YELLOW + "[!] CAUTION: strong-crypto is disabled! It is recommended to enable it to enhance security. This will:")
|
||||||
print(Fore.YELLOW + " - Use stronger encryption, HMAC algorithms, and larger DH primes;")
|
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 + " - Prefer 256-bit encryption, disable null encryption, prefer SHA-256;")
|
||||||
print(Fore.YELLOW + " - Disable MD5, use 2048-bit prime for Diffie-Hellman exchange;")
|
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:
|
if "forwarding-enabled: both" not in output and "strong-crypto: no" not in output:
|
||||||
print(Fore.GREEN + "[+] SSH security settings are properly configured.")
|
print(Fore.GREEN + "[+] SSH security settings are properly configured.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Check if connection tracking is enabled, which may impact performance
|
# Check if connection tracking is enabled, which may impact performance
|
||||||
def check_connection_tracking(connection):
|
def check_connection_tracking(connection):
|
||||||
|
@ -503,12 +445,12 @@ def check_connection_tracking(connection):
|
||||||
command = "/ip firewall connection tracking print"
|
command = "/ip firewall connection tracking print"
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
if "enabled: auto" in output or "enabled: on" in output:
|
if "enabled: auto" in output or "enabled: on" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: Connection Tracking is enabled! This means RouterOS is tracks connection statuses.")
|
print(Fore.YELLOW + "[!] CAUTION: Connection Tracking is enabled! This means RouterOS is tracks connection statuses.")
|
||||||
print(Fore.YELLOW + " - If this device is a transit router and does NOT use NAT, consider disabling connection tracking to reduce CPU load.")
|
print(Fore.YELLOW + " - If this device is a transit router and does NOT use NAT, consider disabling connection tracking to reduce CPU load.")
|
||||||
|
|
||||||
if "enabled: auto" not in output and "enabled: on" not in output:
|
if "enabled: auto" not in output and "enabled: on" not in output:
|
||||||
print(Fore.GREEN + "[+] Connection Tracking is properly configured.")
|
print(Fore.GREEN + "[+] Connection Tracking is properly configured.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Verify if RoMON is enabled, which might expose Layer 2 management access
|
# Verify if RoMON is enabled, which might expose Layer 2 management access
|
||||||
def check_romon_status(connection):
|
def check_romon_status(connection):
|
||||||
|
@ -518,40 +460,63 @@ def check_romon_status(connection):
|
||||||
output = connection.send_command(command)
|
output = connection.send_command(command)
|
||||||
|
|
||||||
if "enabled: yes" in output:
|
if "enabled: yes" in output:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: RoMON is enabled! This allows Layer 2 management access, which may expose the router to unauthorized control.")
|
print(Fore.YELLOW + "[!] CAUTION: RoMON is enabled! This allows Layer 2 management access, which may expose the router to unauthorized control.")
|
||||||
print(Fore.YELLOW + " - If RoMON is not required, disable it to reduce attack surface.")
|
print(Fore.YELLOW + " - If RoMON is not required, disable it to reduce attack surface.")
|
||||||
if "enabled: yes" not in output:
|
if "enabled: yes" not in output:
|
||||||
print(Fore.GREEN + "[+] RoMON is disabled. No risk detected.")
|
print(Fore.GREEN + "[+] RoMON is disabled. No risk detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Analyze MAC-based Winbox access settings
|
# Analyze MAC-based Winbox access settings
|
||||||
def check_mac_winbox_security(connection):
|
def check_mac_winbox_security(connection):
|
||||||
# Separator outlet
|
|
||||||
separator("Checking Winbox MAC Server Settings")
|
separator("Checking Winbox MAC Server Settings")
|
||||||
|
|
||||||
# MAC-Winbox Server
|
# MAC-Winbox Server
|
||||||
command = "tool mac-server mac-winbox print"
|
try:
|
||||||
output = connection.send_command(command)
|
command = "/tool mac-server mac-winbox print"
|
||||||
if "allowed-interface-list: all" in output:
|
output = connection.send_command(command)
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: MAC Winbox access is enabled on all interfaces. This compromises the security of the Winbox interface.")
|
|
||||||
else:
|
if "allowed-interface-list" in output:
|
||||||
print(Fore.GREEN + "[+] MAC Winbox are properly restricted.")
|
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}")
|
||||||
|
|
||||||
# MAC-Server
|
# MAC-Server
|
||||||
command = "tool mac-server print"
|
try:
|
||||||
output = connection.send_command(command)
|
command = "/tool mac-server print"
|
||||||
if "allowed-interface-list: all" in output:
|
output = connection.send_command(command)
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: MAC Telnet access is enabled on all interfaces. This compromises the security of the Winbox interface.")
|
|
||||||
else:
|
if "allowed-interface-list" in output:
|
||||||
print(Fore.GREEN + "[+] MAC Telnet are properly restricted.")
|
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}")
|
||||||
|
|
||||||
# MAC Ping
|
# MAC Ping
|
||||||
command = "tool mac-server ping print"
|
try:
|
||||||
output = connection.send_command(command)
|
command = "/tool mac-server ping print"
|
||||||
if "enabled: yes" in output:
|
output = connection.send_command(command)
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: MAC Ping is enabled. Possible unwanted traffic.")
|
if "enabled: yes" in output:
|
||||||
else:
|
print(Fore.YELLOW + "[!] CAUTION: MAC Ping is enabled. Possible unwanted traffic.")
|
||||||
print(Fore.GREEN + "[+] MAC Ping are properly restricted.")
|
else:
|
||||||
|
print(Fore.GREEN + "[+] MAC Ping is properly restricted.")
|
||||||
|
except Exception as e:
|
||||||
|
print(Fore.RED + f"[-] ERROR while checking MAC Ping: {e}")
|
||||||
|
|
||||||
# Check for weak SNMP community strings that could be exploited
|
# Check for weak SNMP community strings that could be exploited
|
||||||
def check_snmp(connection):
|
def check_snmp(connection):
|
||||||
|
@ -568,12 +533,12 @@ def check_snmp(connection):
|
||||||
if match:
|
if match:
|
||||||
community_name = match.group(1).lower()
|
community_name = match.group(1).lower()
|
||||||
if community_name in bad_names:
|
if community_name in bad_names:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + f"[!] CAUTION: Weak SNMP community string detected: '{community_name}'. Change it to a secure, unique value.")
|
print(Fore.YELLOW + f"[!] CAUTION: Weak SNMP community string detected: '{community_name}'. Change it to a secure, unique value.")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
if not risks_found:
|
if not risks_found:
|
||||||
print(Fore.GREEN + "[+] SNMP community strings checked. No weak values detected.")
|
print(Fore.GREEN + "[+] SNMP community strings checked. No weak values detected.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Detect and analyze firewall NAT rules that could expose internal services
|
# Detect and analyze firewall NAT rules that could expose internal services
|
||||||
def check_dst_nat_rules(connection):
|
def check_dst_nat_rules(connection):
|
||||||
|
@ -586,14 +551,14 @@ def check_dst_nat_rules(connection):
|
||||||
if "action=dst-nat" in line or "action=netmap" in line:
|
if "action=dst-nat" in line or "action=netmap" in line:
|
||||||
dst_nat_rules.append(line.strip())
|
dst_nat_rules.append(line.strip())
|
||||||
if dst_nat_rules:
|
if dst_nat_rules:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] CAUTION: Destination NAT (dst-nat/netmap) rules detected! Exposing devices to the internet can be dangerous.")
|
print(Fore.YELLOW + "[!] CAUTION: Destination NAT (dst-nat/netmap) rules detected! Exposing devices to the internet can be dangerous.")
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[*] Similar rules can also be created by the attacker. Did you really create these rules yourself?")
|
print(Fore.YELLOW + "[*] Similar rules can also be created by the attacker. Did you really create these rules yourself?")
|
||||||
print(Fore.YELLOW + " - Review the following NAT rules:")
|
print(Fore.YELLOW + " - Review the following NAT rules:")
|
||||||
for rule in dst_nat_rules:
|
for rule in dst_nat_rules:
|
||||||
print(Fore.YELLOW + f" {rule}")
|
print(Fore.YELLOW + f" {rule}")
|
||||||
if not dst_nat_rules:
|
if not dst_nat_rules:
|
||||||
print(Fore.GREEN + "[+] No Destination NAT (dst-nat/netmap) rules detected. No risks found.")
|
print(Fore.GREEN + "[+] No Destination NAT (dst-nat/netmap) rules detected. No risks found.")
|
||||||
print(Fore.GREEN + "[+] No issues found.")
|
print("[" + Fore.GREEN + "+" + Fore.WHITE + "] No issues found.")
|
||||||
|
|
||||||
# Identify potentially malicious scheduled tasks
|
# Identify potentially malicious scheduled tasks
|
||||||
def detect_malicious_schedulers(connection):
|
def detect_malicious_schedulers(connection):
|
||||||
|
@ -631,7 +596,7 @@ def detect_malicious_schedulers(connection):
|
||||||
if "import" in event and import_match:
|
if "import" in event and import_match:
|
||||||
imported_file = import_match.group(1).strip(";")
|
imported_file = import_match.group(1).strip(";")
|
||||||
if imported_file in fetch_files:
|
if imported_file in fetch_files:
|
||||||
print(Fore.RED + Style.BRIGHT + f"[!] ALERT: '{name}' is a BACKDOOR!")
|
print(Fore.RED + f"[!] ALERT: '{name}' is a BACKDOOR!")
|
||||||
print(Fore.RED + " - This scheduler imports a previously fetched script.")
|
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 + " - Attacker can inject any command remotely via this script.")
|
||||||
print(Fore.RED + f" - Interval: {interval_value}{interval_unit}, ensuring persistence.")
|
print(Fore.RED + f" - Interval: {interval_value}{interval_unit}, ensuring persistence.")
|
||||||
|
@ -640,7 +605,7 @@ def detect_malicious_schedulers(connection):
|
||||||
# High privileges checking
|
# High privileges checking
|
||||||
dangerous_policies = {"password", "sensitive", "sniff", "ftp"}
|
dangerous_policies = {"password", "sensitive", "sniff", "ftp"}
|
||||||
if any(p in dangerous_policies for p in policy):
|
if any(p in dangerous_policies for p in policy):
|
||||||
print(Fore.RED + Style.BRIGHT + f"[!] ALERT: '{name}' has HIGH PRIVILEGES!")
|
print(Fore.RED + f"[!] ALERT: '{name}' has HIGH PRIVILEGES!")
|
||||||
print(Fore.RED + f" - It has dangerous permissions: {', '.join(policy)}")
|
print(Fore.RED + f" - It has dangerous permissions: {', '.join(policy)}")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
|
@ -657,7 +622,7 @@ def detect_malicious_schedulers(connection):
|
||||||
|
|
||||||
# Frequent execution detection
|
# Frequent execution detection
|
||||||
if interval_value and interval_unit in ["s", "m", "h"] and interval_value < 25:
|
if interval_value and interval_unit in ["s", "m", "h"] and interval_value < 25:
|
||||||
print(Fore.RED + Style.BRIGHT + f"[!] ALERT: '{name}' executes TOO FREQUENTLY ({interval_value}{interval_unit})!")
|
print(Fore.RED + f"[!] ALERT: '{name}' executes TOO FREQUENTLY ({interval_value}{interval_unit})!")
|
||||||
print(Fore.RED + " - This indicates botnet-like persistence.")
|
print(Fore.RED + " - This indicates botnet-like persistence.")
|
||||||
risks_found = True
|
risks_found = True
|
||||||
|
|
||||||
|
@ -693,35 +658,16 @@ def check_static_dns_entries(connection):
|
||||||
else:
|
else:
|
||||||
print(Fore.GREEN + "[+] No static DNS entries found.")
|
print(Fore.GREEN + "[+] No static DNS entries found.")
|
||||||
|
|
||||||
# Retrieve router uptime
|
|
||||||
def get_router_uptime(connection):
|
|
||||||
# Separator outlet
|
|
||||||
separator("Checking Router Uptime")
|
|
||||||
command = "/system resource print"
|
|
||||||
output = connection.send_command(command)
|
|
||||||
|
|
||||||
# Extract uptime value
|
# Require user confirmation before proceeding, emphasizing legal responsibility
|
||||||
match = re.search(r"uptime:\s*([\w\d\s]+)", output)
|
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()
|
||||||
|
|
||||||
if match:
|
if response.lower() != "yes":
|
||||||
uptime_raw = match.group(1)
|
print("\nOperation aborted. Exiting...")
|
||||||
weeks = days = hours = minutes = 0
|
sys.exit(0)
|
||||||
|
|
||||||
# Extract individual time units
|
|
||||||
if "w" in uptime_raw:
|
|
||||||
weeks = int(re.search(r"(\d+)w", uptime_raw).group(1))
|
|
||||||
if "d" in uptime_raw:
|
|
||||||
days = int(re.search(r"(\d+)d", uptime_raw).group(1))
|
|
||||||
if "h" in uptime_raw:
|
|
||||||
hours = int(re.search(r"(\d+)h", uptime_raw).group(1))
|
|
||||||
if "m" in uptime_raw:
|
|
||||||
minutes = int(re.search(r"(\d+)m", uptime_raw).group(1))
|
|
||||||
|
|
||||||
# Convert weeks to days and format output
|
|
||||||
total_days = weeks * 7 + days
|
|
||||||
print(Fore.GREEN + Style.BRIGHT + f"[*] Router Uptime: {total_days} days, {hours} hours, {minutes} minutes")
|
|
||||||
else:
|
|
||||||
print(Fore.RED + "[-] ERROR: Could not retrieve uptime.")
|
|
||||||
|
|
||||||
# Require user confirmation before proceeding, emphasizing legal responsibility
|
# Require user confirmation before proceeding, emphasizing legal responsibility
|
||||||
def confirm_legal_usage():
|
def confirm_legal_usage():
|
||||||
|
@ -735,56 +681,54 @@ def prompt_legal_usage():
|
||||||
print("\nOperation aborted. Exiting...")
|
print("\nOperation aborted. Exiting...")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# # Parse command-line arguments, establish connection, and execute all security checks
|
# Main func
|
||||||
def main():
|
def main():
|
||||||
# Print banner
|
|
||||||
banner()
|
banner()
|
||||||
|
|
||||||
# Argument parsing
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--ip", help="The address of your MikroTik router")
|
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("--username", help="SSH username (RO account can be used)")
|
||||||
parser.add_argument("--password", help="SSH password")
|
parser.add_argument("--password", help="SSH password")
|
||||||
parser.add_argument("--ssh-key", help="SSH key")
|
parser.add_argument("--ssh-key", help="SSH key")
|
||||||
parser.add_argument("--passphrase", help="SSH key passphrase")
|
parser.add_argument("--passphrase", help="SSH key passphrase")
|
||||||
parser.add_argument("--skip-confirmation", action='store_true', help='Skips the confirmation prompt (disclamer: ensure that your are allowed to use this tool)')
|
|
||||||
parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)")
|
parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)")
|
||||||
|
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")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if len(sys.argv) == 2 and sys.argv[1] in ["-h", "--help"]:
|
if len(sys.argv) == 2 and sys.argv[1] in ["-h", "--help"]:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if not args.ip:
|
if not args.ip or not args.username or (not args.password and not args.ssh_key):
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] ERROR: Missing required arguments")
|
print(Fore.YELLOW + "[!] ERROR: Missing required arguments")
|
||||||
print(Fore.YELLOW + "[!] Use 'sara --help' for more information")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not args.username or (not args.password and not args.ssh_key):
|
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] ERROR: Missing required arguments")
|
|
||||||
print(Fore.YELLOW + "[!] Use 'sara --help' for more information")
|
print(Fore.YELLOW + "[!] Use 'sara --help' for more information")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.password and args.ssh_key:
|
if args.password and args.ssh_key:
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] ERROR: Can't use both password & ssh_key authentication")
|
print(Fore.YELLOW + "[!] ERROR: Can't use both password & ssh_key authentication")
|
||||||
print(Fore.YELLOW + "[!] Use 'sara --help' for more information")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.passphrase and not args.ssh_key:
|
|
||||||
print(Fore.YELLOW + Style.BRIGHT + "[!] ERROR: The passphrase argument can't be used when not specifying a ssh_key")
|
|
||||||
print(Fore.YELLOW + "[!] Use 'sara --help' for more information")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
confirm_legal_usage()
|
if args.passphrase and not args.ssh_key:
|
||||||
|
print(Fore.YELLOW + "[!] ERROR: Passphrase requires --ssh-key")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Legal warning (interactive only if not skipped)
|
||||||
if not args.skip_confirmation:
|
if not args.skip_confirmation:
|
||||||
|
# disclaimer text
|
||||||
|
confirm_legal_usage()
|
||||||
|
# yes or no
|
||||||
prompt_legal_usage()
|
prompt_legal_usage()
|
||||||
|
else:
|
||||||
|
confirm_legal_usage()
|
||||||
|
|
||||||
# Start timer
|
# Start timer
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Connecting to the router
|
# Connect to RouterOS
|
||||||
connection = connect_to_router(args.ip,
|
connection = connect_to_router(
|
||||||
|
args.ip,
|
||||||
args.username,
|
args.username,
|
||||||
args.password,
|
args.password,
|
||||||
args.port,
|
args.port,
|
||||||
|
@ -792,7 +736,13 @@ def main():
|
||||||
args.passphrase
|
args.passphrase
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute all implemented security checks in sequence
|
# Run only CVE check if --cve is used
|
||||||
|
if args.cve:
|
||||||
|
run_cve_audit(connection)
|
||||||
|
connection.disconnect()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run full audit
|
||||||
check_routeros_version(connection)
|
check_routeros_version(connection)
|
||||||
check_smb(connection)
|
check_smb(connection)
|
||||||
check_rmi_services(connection)
|
check_rmi_services(connection)
|
||||||
|
@ -816,22 +766,15 @@ def main():
|
||||||
check_dst_nat_rules(connection)
|
check_dst_nat_rules(connection)
|
||||||
detect_malicious_schedulers(connection)
|
detect_malicious_schedulers(connection)
|
||||||
check_static_dns_entries(connection)
|
check_static_dns_entries(connection)
|
||||||
get_router_uptime(connection)
|
|
||||||
|
|
||||||
# Print a blank line for better output formatting
|
print()
|
||||||
print ()
|
|
||||||
|
|
||||||
# Close the SSH connection to the router
|
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
print(Fore.GREEN + Style.BRIGHT + f"[*] Disconnected from RouterOS ({args.ip}:{args.port})")
|
print(Fore.WHITE + f"[*] Disconnected from RouterOS ({args.ip}:{args.port})")
|
||||||
|
|
||||||
# Measure and display the total execution time
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
total_time = round(end_time - start_time, 2)
|
total_time = round(end_time - start_time, 2)
|
||||||
|
print(Fore.WHITE + f"[*] All checks have been completed. Security inspection completed in {total_time} seconds\n")
|
||||||
# Print a closing message emphasizing continuous security improvements
|
|
||||||
print(Fore.GREEN + Style.BRIGHT + f"[*] All checks have been completed. Security inspection completed in {total_time} seconds\n")
|
|
||||||
print(Fore.MAGENTA + Style.BRIGHT + "[*] " + Fore.WHITE + "Remember: " + Fore.RED + "Security" + Fore.WHITE + " is a " + Fore.GREEN + "process" + Fore.WHITE + ", not a " + Fore.YELLOW + "state.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
4
setup.py
4
setup.py
|
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="sara",
|
name="sara",
|
||||||
version="1.1.1",
|
version="1.2",
|
||||||
url="https://github.com/casterbyte/Sara",
|
url="https://github.com/casterbyte/Sara",
|
||||||
author="Magama Bazarov",
|
author="Magama Bazarov",
|
||||||
author_email="magamabazarov@mailbox.org",
|
author_email="magamabazarov@mailbox.org",
|
||||||
|
@ -18,7 +18,7 @@ setup(
|
||||||
'netmiko',
|
'netmiko',
|
||||||
'packaging',
|
'packaging',
|
||||||
],
|
],
|
||||||
py_modules=['cve_lookup'],
|
py_modules=['cve_analyzer'],
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": ["sara = sara:main"],
|
"console_scripts": ["sara = sara:main"],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue