diff --git a/bbot/modules/portscan.py b/bbot/modules/portscan.py index 5ff23dc7b..674242169 100644 --- a/bbot/modules/portscan.py +++ b/bbot/modules/portscan.py @@ -6,6 +6,9 @@ from bbot.modules.base import BaseModule +# TODO: this module is getting big. It should probably be two modules: one for ping and one for SYN. + + class portscan(BaseModule): flags = ["active", "portscan", "safe"] watched_events = ["IP_ADDRESS", "IP_RANGE", "DNS_NAME"] @@ -27,6 +30,8 @@ class portscan(BaseModule): "adapter_ip": "", "adapter_mac": "", "router_mac": "", + "cdn_tags": "cdn-", + "allowed_cdn_ports": None, } options_desc = { "top_ports": "Top ports to scan (default 100) (to override, specify 'ports')", @@ -39,6 +44,8 @@ class portscan(BaseModule): "adapter_ip": "Send packets using this IP address. Not needed unless masscan's autodetection fails", "adapter_mac": "Send packets using this as the source MAC address. Not needed unless masscan's autodetection fails", "router_mac": "Send packets to this MAC address as the destination. Not needed unless masscan's autodetection fails", + "cdn_tags": "Comma-separated list of tags to skip, e.g. 'cdn,cloud'", + "allowed_cdn_ports": "Comma-separated list of ports that are allowed to be scanned for CDNs", } deps_common = ["masscan"] batch_size = 1000000 @@ -60,7 +67,15 @@ async def setup(self): try: self.helpers.parse_port_string(self.ports) except ValueError as e: - return False, f"Error parsing ports: {e}" + return False, f"Error parsing ports '{self.ports}': {e}" + self.cdn_tags = [t.strip() for t in self.config.get("cdn_tags", "").split(",")] + self.allowed_cdn_ports = self.config.get("allowed_cdn_ports", None) + if self.allowed_cdn_ports is not None: + try: + self.allowed_cdn_ports = [int(p.strip()) for p in self.allowed_cdn_ports.split(",")] + except Exception as e: + return False, f"Error parsing allowed CDN ports '{self.allowed_cdn_ports}': {e}" + # whether we've finished scanning our original scan targets self.scanned_initial_targets = False # keeps track of individual scanned IPs and their open ports @@ -227,9 +242,20 @@ async def emit_open_port(self, ip, port, parent_event): parent=parent_event, context=f"{{module}} executed a {scan_type} scan against {parent_event.data} and found: {{event.type}}: {{event.data}}", ) - await self.emit_event(event) + + await self.emit_event(event, abort_if=self.abort_if) return event + def abort_if(self, event): + if self.allowed_cdn_ports is not None: + # if the host is a CDN + for cdn_tag in self.cdn_tags: + if any(t.startswith(str(cdn_tag)) for t in event.tags): + # and if its port isn't in the list of allowed CDN ports + if event.port not in self.allowed_cdn_ports: + return True, "event is a CDN and port is not in the allowed list" + return False + def parse_json_line(self, line): try: j = json.loads(line) diff --git a/bbot/test/test_step_2/module_tests/test_module_portscan.py b/bbot/test/test_step_2/module_tests/test_module_portscan.py index 56536cb5d..d9f55c27f 100644 --- a/bbot/test/test_step_2/module_tests/test_module_portscan.py +++ b/bbot/test/test_step_2/module_tests/test_module_portscan.py @@ -109,10 +109,12 @@ def check(self, module_test, events): if e.type == "DNS_NAME" and e.data == "dummy.asdf.evilcorp.net" and str(e.module) == "dummy_module" ] ) - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.8.8"]) <= 3 - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.4"]) <= 3 - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.5"]) <= 3 - assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.6"]) <= 3 + # the reason these numbers aren't exactly predictable is because we can't predict which one arrives first + # to the portscan module. Sometimes, one that would normally be deduped is force-emitted because it led to a new open port. + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.8.8"]) <= 4 + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.4"]) <= 4 + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.5"]) <= 4 + assert 2 <= len([e for e in events if e.type == "IP_ADDRESS" and e.data == "8.8.4.6"]) <= 4 assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "8.8.8.8:443"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "8.8.4.5:80"]) assert 1 == len([e for e in events if e.type == "OPEN_TCP_PORT" and e.data == "8.8.4.6:631"]) diff --git a/docs/scanning/tips_and_tricks.md b/docs/scanning/tips_and_tricks.md index 32b55448f..913ccca6f 100644 --- a/docs/scanning/tips_and_tricks.md +++ b/docs/scanning/tips_and_tricks.md @@ -77,7 +77,38 @@ You can also pair the web spider with subdomain enumeration: bbot -t evilcorp.com -f subdomain-enum -c spider.yml ``` -### Ingesting BBOT Data Into SIEM (Elastic, Splunk) +### Exclude CDNs from Port Scan + +If you want to exclude CDNs (e.g. Cloudflare) from port scanning, you can set the `allowed_cdn_ports` config option in the `portscan` module. For example, to allow only port 80 (HTTP) and 443 (HTTPS), you can do the following: + +```bash +bbot -t evilcorp.com -m portscan -c modules.portscan.allowed_cdn_ports=80,443 +``` + +By default, if you set `allowed_cdn_ports`, it will skip only providers marked as CDNs. If you want to skip cloud providers as well, you can set `cdn_tags`, which is a comma-separated list of tags to skip (matched against the beginning of each tag). + +```bash +bbot -t evilcorp.com -m portscan -c modules.portscan.allowed_cdn_ports=80,443 modules.portscan.cdn_tags=cdn-,cloud- +``` + +...or via a preset: + +```yaml title="skip_cdns.yml" +modules: + - portscan + +config: + modules: + portscan: + allowed_cdn_ports: 80,443 + cdn_tags: cdn-,cloud- +``` + +```bash +bbot -t evilcorp.com -p skip_cdns.yml +``` + +### Ingest BBOT Data Into SIEM (Elastic, Splunk) If your goal is to feed BBOT data into a SIEM such as Elastic, be sure to enable this option when scanning: