From c976d097e08c5aee4ddf4176d7a461002eed6715 Mon Sep 17 00:00:00 2001 From: Frigo Date: Fri, 17 Jan 2025 15:02:44 +0100 Subject: [PATCH] Add network scanner detector --- include/Host.h | 10 +++++- include/host_alerts/NetworkScannerAlert.h | 26 ++++++++++++++++ include/host_alerts_includes.h | 1 + include/host_checks/NetworkScanner.h | 27 ++++++++++++++++ include/host_checks_includes.h | 1 + include/ntop_typedefs.h | 2 ++ scripts/locales/en.lua | 3 ++ .../host/host_alert_network_scanner.lua | 31 +++++++++++++++++++ .../modules/alert_keys/host_alert_keys.lua | 1 + .../host/network_scanner.lua | 27 ++++++++++++++++ src/Flow.cpp | 2 ++ src/Host.cpp | 16 +++++++++- src/HostChecksLoader.cpp | 1 + src/host_alerts/NetworkScannerAlert.cpp | 16 ++++++++++ src/host_checks/NetworkScanner.cpp | 28 +++++++++++++++++ 15 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 include/host_alerts/NetworkScannerAlert.h create mode 100644 include/host_checks/NetworkScanner.h create mode 100644 scripts/lua/modules/alert_definitions/host/host_alert_network_scanner.lua create mode 100644 scripts/lua/modules/check_definitions/host/network_scanner.lua create mode 100644 src/host_alerts/NetworkScannerAlert.cpp create mode 100644 src/host_checks/NetworkScanner.cpp diff --git a/include/Host.h b/include/Host.h index 16220e6ff6ca..acd15c194f5d 100644 --- a/include/Host.h +++ b/include/Host.h @@ -124,6 +124,12 @@ class Host : public GenericHashEntry, char *msg; } customHostAlert; + struct { + std::deque tokens; + u_int32_t capacity; + time_t timeWindow; + } netscanFlow; + Mutex m; u_int32_t mac_last_seen; u_int8_t num_resolve_attempts; @@ -227,7 +233,8 @@ class Host : public GenericHashEntry, inline u_int32_t getNumBlacklistedAsSrvReset() const { return getNumBlacklistedAsSrv() - getCheckpointBlacklistedAsSrv(); } - + inline u_int32_t getNetscanTokens() { return (netscanFlow.tokens.size()); }; + inline void resetNetscanTokens() { netscanFlow.tokens.clear(); }; inline bool isDhcpServer() const { return (host_services_bitmap & (1 << HOST_IS_DHCP_SERVER)); } @@ -543,6 +550,7 @@ class Host : public GenericHashEntry, void updateSNMPAlertsCounter(time_t when, bool snmp_sent); void updateSynAckAlertsCounter(time_t when, bool synack_sent); void updateFinAckAlertsCounter(time_t when, bool finack_sent); + void networkScan(time_t t, IpAddress *_srv_ip); virtual void updateNetworkRTT(u_int32_t rtt_msecs) { return; } inline void updateRoundTripTime(u_int32_t rtt_msecs) { diff --git a/include/host_alerts/NetworkScannerAlert.h b/include/host_alerts/NetworkScannerAlert.h new file mode 100644 index 000000000000..2eebec1481d2 --- /dev/null +++ b/include/host_alerts/NetworkScannerAlert.h @@ -0,0 +1,26 @@ +#ifndef _NETWORK_SCANNER_ALERT_H_ +#define _NETWORK_SCANNER_ALERT_H_ +#include "ntop_includes.h" + +class NetworkScannerAlert : public FlowHitsAlert { + private: + u_int16_t num_flows_tokens; + u_int64_t threshold; + ndpi_serializer* getAlertJSON(ndpi_serializer* serializer); + + public: + static HostAlertType getClassType(){ + return { host_alert_network_scanner, alert_category_network }; + } + + NetworkScannerAlert(HostCheck *c, Host *h, risk_percentage cli_pctg, + u_int16_t _num_flows_tokens, u_int64_t threshold, bool is_attacker); + + ~NetworkScannerAlert() {}; + + HostAlertType getAlertType() const { return getClassType(); } + + u_int8_t getAlertScore() const { return SCORE_LEVEL_WARNING; } +}; + +#endif /* _NETWORK_SCANNER_ALERT_H_ */ \ No newline at end of file diff --git a/include/host_alerts_includes.h b/include/host_alerts_includes.h index b6952ae354ab..c3f7bbfca052 100644 --- a/include/host_alerts_includes.h +++ b/include/host_alerts_includes.h @@ -47,6 +47,7 @@ #include "host_alerts/ScanDetectionAlert.h" #include "host_alerts/TrafficVolumeAlert.h" #include "host_alerts/HostScannerAlert.h" +#include "host_alerts/NetworkScannerAlert.h" /* Pro Alerts - do NOT use #ifdef as alerts must always be available */ diff --git a/include/host_checks/NetworkScanner.h b/include/host_checks/NetworkScanner.h new file mode 100644 index 000000000000..0674ea75ef45 --- /dev/null +++ b/include/host_checks/NetworkScanner.h @@ -0,0 +1,27 @@ +#ifndef _NETWORKSCANNER_H_ +#define _NETWORKSCANNER_H_ + +#include "ntop_includes.h" + +class NetworkScanner : public HostCheck{ + protected: + u_int64_t threshold; + public: + NetworkScanner(); + ~NetworkScanner(){}; + + NetworkScannerAlert *allocAlert(HostCheck* c, Host* h, + risk_percentage cli_pctg, + u_int16_t num_flows_tokens, + u_int64_t _threshold, + bool _is_attacker) { + return new NetworkScannerAlert(c, h, cli_pctg, num_flows_tokens, _threshold, _is_attacker); + }; + + bool loadConfiguration(json_object *config); + void periodicUpdate(Host *h, HostAlert *engaged_alert); + HostCheckID getID() const { return host_check_network_scanner; } + std::string getName() const { return (std::string("network_scanner")); } +}; + +#endif \ No newline at end of file diff --git a/include/host_checks_includes.h b/include/host_checks_includes.h index b0b62e98a45d..f9ac27cdaf3c 100644 --- a/include/host_checks_includes.h +++ b/include/host_checks_includes.h @@ -45,6 +45,7 @@ #include "host_checks/UnexpectedGateway.h" #include "host_checks/DomainNamesContacts.h" #include "host_checks/ScanDetection.h" +#include "host_checks/NetworkScanner.h" #ifdef NTOPNG_PRO #include "host_checks/DNSFlood.h" diff --git a/include/ntop_typedefs.h b/include/ntop_typedefs.h index a583505ac0bb..f410fce90997 100644 --- a/include/ntop_typedefs.h +++ b/include/ntop_typedefs.h @@ -584,6 +584,7 @@ typedef enum { host_alert_external_script = 27, /* Triggered from Lua (see rest/v2/trigger/host/alert.lua) */ host_alert_host_scanner = 28, host_alert_server_ports_contacts = 29, + host_alert_network_scanner = 30, MAX_DEFINED_HOST_ALERT_TYPE, /* Leave it as last member */ MAX_HOST_ALERT_TYPE = 32 /* Constrained by HostAlertBitmap */ @@ -636,6 +637,7 @@ typedef enum { host_check_rx_only_host_scan, host_check_server_ports_contacts, host_check_unexpected_gateway, + host_check_network_scanner, NUM_DEFINED_HOST_CHECKS, /* Leave it as last member */ } HostCheckID; diff --git a/scripts/locales/en.lua b/scripts/locales/en.lua index 9b019e8f88d7..ffbf10f4e1ec 100644 --- a/scripts/locales/en.lua +++ b/scripts/locales/en.lua @@ -1578,6 +1578,9 @@ local lang = { ["top_srv"] = "Hosts as server with most alerts", ["top_vlan"] = "VLANs with most alerts", }, + ["network_scanner_description"] = "Trigger an alert when a host is potentially performing network scanning", + ["network_scanner_title"] = "Network Scanner detector", + ["network_scanner_message"] = "Potentially a network scanner", }, ["alerts_thresholds_config"] = { ["active_local_hosts"] = "Local Hosts Alert", diff --git a/scripts/lua/modules/alert_definitions/host/host_alert_network_scanner.lua b/scripts/lua/modules/alert_definitions/host/host_alert_network_scanner.lua new file mode 100644 index 000000000000..24657c7835d8 --- /dev/null +++ b/scripts/lua/modules/alert_definitions/host/host_alert_network_scanner.lua @@ -0,0 +1,31 @@ +local host_alert_keys = require "host_alert_keys" +local json = require("dkjson") +local alert_creators = require "alert_creators" +local classes = require "classes" +local alert = require "alert" + +local host_alert_network_scanner = classes.class(alert) + +host_alert_network_scanner.meta = { + alert_key = host_alert_keys.host_alert_network_scanner, + i18n_title = "alerts_dashboard.network_scanner_title", + icon = "fas fa-fw fa-life-ring", +} + +function host_alert_network_scanner:init(metric, value, operator, threshold) + self.super:init() + self.alert_type_params = alert_creators.createThresholdCross(metric, value, operator, threshold) +end + +function host_alert_network_scanner.format(ifid, alert, alert_type_params) + local alert_consts = require("alert_consts") + local entity = alert_consts.formatHostAlert(ifid, alert["ip"], alert["vlan_id"]) + local value = string.format("%u", math.ceil(alert_type_params.num_flows_tokens or 0)) + return i18n("alerts_dashboard.http_contacts_message", { + entity = entity, + value = value, + threshold = alert_type_params.threshold or 0, + }) +end + +return host_alert_network_scanner \ No newline at end of file diff --git a/scripts/lua/modules/alert_keys/host_alert_keys.lua b/scripts/lua/modules/alert_keys/host_alert_keys.lua index 04e6c522dbff..a08afbd48fb6 100644 --- a/scripts/lua/modules/alert_keys/host_alert_keys.lua +++ b/scripts/lua/modules/alert_keys/host_alert_keys.lua @@ -36,6 +36,7 @@ local host_alert_keys = { host_alert_external_script = 27, host_alert_host_scanner = 28, host_alert_server_ports_contacts = 29, + host_alert_network_scanner = 30, -- NOTE: Keep in sync with HostAlertTypeEnum in ntop_typedefs.h } diff --git a/scripts/lua/modules/check_definitions/host/network_scanner.lua b/scripts/lua/modules/check_definitions/host/network_scanner.lua new file mode 100644 index 000000000000..1a72bcac5bf8 --- /dev/null +++ b/scripts/lua/modules/check_definitions/host/network_scanner.lua @@ -0,0 +1,27 @@ +local checks = require("checks") +local host_alert_keys = require "host_alert_keys" +local alert_consts = require("alert_consts") +local field_units = require "field_units" + +local network_scanner = { + category = checks.check_categories.network, + severity = alert_consts.get_printable_severities().warning, + default_enabled = false, + alert_id = host_alert_keys.host_alert_network_scanner, + default_value = { + operator = "gt", + threshold = 5, + }, + gui = { + i18n_title = "alerts_dashboard.network_scanner_title", + i18n_description = "alerts_dashboard.network_scanner_description", + i18n_field_unit = checks.field_units.count, + input_builder = "threshold_cross", + i18n_field_unit = field_units.flows, + field_max = 65535, + field_min = 1, + field_operator = "gt", + } +} + +return network_scanner \ No newline at end of file diff --git a/src/Flow.cpp b/src/Flow.cpp index 72a6be18f8c3..d36b384db44e 100644 --- a/src/Flow.cpp +++ b/src/Flow.cpp @@ -156,6 +156,8 @@ Flow::Flow(NetworkInterface *_iface, if(cli_host) { cli_host->incUses(), cli_host->incNumFlows(last_seen, true, isTCP()); cli_host->incCliContactedPorts(_srv_port); + if(isTCP() || isUDP() || isICMP()) + cli_host->networkScan(last_seen,srv_host->get_ip()); cli_ip_addr = cli_host->get_ip(); } else { /* diff --git a/src/Host.cpp b/src/Host.cpp index 8e6593f24b46..3fca4ccac1b3 100644 --- a/src/Host.cpp +++ b/src/Host.cpp @@ -264,7 +264,8 @@ void Host::housekeep(time_t t) { void Host::initialize(Mac *_mac, int32_t _iface_idx, u_int16_t _vlanId, u_int16_t observation_point_id) { if(_vlanId == (u_int16_t)-1) _vlanId = 0; - + netscanFlow.capacity = 50; + netscanFlow.timeWindow = 2; vlan_id = _vlanId & 0xFFF; /* Cleanup any possible junk */ iface_index = _iface_idx; host_pool_id_is_from_mac = false; @@ -1494,6 +1495,19 @@ void Host::incNumFlows(time_t t, bool as_client, bool isTCP) { /* *************************************** */ +void Host::networkScan(time_t t, IpAddress *_srv_ip){ + while (!netscanFlow.tokens.empty() && (t - netscanFlow.tokens.front()) > netscanFlow.timeWindow) { + netscanFlow.tokens.pop_front(); + } + if(_srv_ip->isPrivateAddress()){ + if (netscanFlow.tokens.size() < netscanFlow.capacity) { + netscanFlow.tokens.push_back(t); + } + } +} + +/* *************************************** */ + void Host::decNumFlows(time_t t, bool as_client, bool isTCP, u_int16_t isTwhOver) { if(as_client) diff --git a/src/HostChecksLoader.cpp b/src/HostChecksLoader.cpp index 1d1b387a5fe8..334a52a2021f 100644 --- a/src/HostChecksLoader.cpp +++ b/src/HostChecksLoader.cpp @@ -78,6 +78,7 @@ void HostChecksLoader::registerChecks() { if ((fcb = new DomainNamesContacts())) registerCheck(fcb); if ((fcb = new ICMPFlood())) registerCheck(fcb); if ((fcb = new ScanDetection())) registerCheck(fcb); + if ((fcb = new NetworkScanner())) registerCheck(fcb); #ifdef NTOPNG_PRO if ((fcb = new ScoreAnomaly())) registerCheck(fcb); diff --git a/src/host_alerts/NetworkScannerAlert.cpp b/src/host_alerts/NetworkScannerAlert.cpp new file mode 100644 index 000000000000..74608dec0ac3 --- /dev/null +++ b/src/host_alerts/NetworkScannerAlert.cpp @@ -0,0 +1,16 @@ +#include "host_alerts_includes.h" + +NetworkScannerAlert::NetworkScannerAlert(HostCheck* c, Host* h, risk_percentage cli_pctg, + u_int16_t _num_flows_tokens, u_int64_t _threshold, + bool _is_attacker) + : FlowHitsAlert(c, h, cli_pctg,_num_flows_tokens, _threshold, _is_attacker) { + num_flows_tokens = _num_flows_tokens; + threshold = _threshold; +}; + +ndpi_serializer* NetworkScannerAlert::getAlertJSON(ndpi_serializer* serializer) { + if (serializer == NULL) return NULL; + ndpi_serialize_string_uint32(serializer, "num_flows_tokens", (u_int32_t)num_flows_tokens); + ndpi_serialize_string_uint64(serializer, "threshold", threshold); + return serializer; +} \ No newline at end of file diff --git a/src/host_checks/NetworkScanner.cpp b/src/host_checks/NetworkScanner.cpp new file mode 100644 index 000000000000..49f6f8b8d162 --- /dev/null +++ b/src/host_checks/NetworkScanner.cpp @@ -0,0 +1,28 @@ +#include "ntop_includes.h" +#include "host_checks_includes.h" +NetworkScanner::NetworkScanner() + : HostCheck(ntopng_edition_community, + false /* All interfaces */, + false /* Don't exclude for nEdge */, + false /* NOT only for nEdge */){}; + +void NetworkScanner::periodicUpdate(Host *h, HostAlert *engaged_alert) { + HostAlert *alert = engaged_alert; + u_int32_t num_flows_tokens = 0; + num_flows_tokens = h->getNetscanTokens(); + if (num_flows_tokens > threshold) { + if (!alert) + alert = allocAlert(this, h, CLIENT_FAIR_RISK_PERCENTAGE, num_flows_tokens, threshold,true); + if (alert) { + h->triggerAlert(alert); + h->resetNetscanTokens(); + } + } +} +bool NetworkScanner::loadConfiguration(json_object *config) { + json_object *json_threshold; + HostCheck::loadConfiguration(config); + if (json_object_object_get_ex(config, "threshold", &json_threshold)) + threshold = json_object_get_int64(json_threshold); + return (true); +} \ No newline at end of file