Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSDP boefje to find devices inside of a network #3594

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion boefjes/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ images: # Build the images for the containerized boefjes
# docker build -f images/base.Dockerfile -t ghcr.io/minvws/openkat/dns-records --build-arg BOEFJE_PATH=./boefjes/plugins/kat_dns .
docker build -f ./boefjes/plugins/kat_dnssec/boefje.Dockerfile -t ghcr.io/minvws/openkat/dns-sec:latest .
docker build -f ./boefjes/plugins/kat_nmap_tcp/boefje.Dockerfile -t ghcr.io/minvws/openkat/nmap:latest .

docker build -f ./images/base.Dockerfile -t ghcr.io/minvws/openkat/ssdp:latest --build-arg BOEFJE_PATH=./boefjes/plugins/kat_ssdp .

##
##|------------------------------------------------------------------------|
Expand Down
Empty file.
10 changes: 10 additions & 0 deletions boefjes/boefjes/plugins/kat_ssdp/boefje.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "ssdp",
"name": "SSDP discovery",
"description": "Scan a local network using SSDP",
"consumes": [
"Network"
],
"scan_level": 1,
"oci_image": "ghcr.io/minvws/openkat/ssdp:latest"
}
Binary file added boefjes/boefjes/plugins/kat_ssdp/cover.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions boefjes/boefjes/plugins/kat_ssdp/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SSDP Scanner

The Simple Service Discovery Protocol (SSDP) is a network protocol based on the Internet protocol suite for advertisement and discovery of network services and presence information. It accomplishes this without assistance of server-based configuration mechanisms, such as Dynamic Host Configuration Protocol (DHCP) or Domain Name System (DNS), and without special static configuration of a network host. SSDP is the basis of the discovery protocol of Universal Plug and Play (UPnP) and is intended for use in residential or small office environments

https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol

### Input OOIs

The SSDP scanner runs in a local network only, and needs no input.

### Output OOIs

SSDP returns devices, Urls and IPAddresses.
26 changes: 26 additions & 0 deletions boefjes/boefjes/plugins/kat_ssdp/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import json
from os import getenv

SEARCHTARGET_DEFAULT = "ssdp:all"
TIMEOUT_DEFAULT = 10


def run_ssdp(search_targets: str, timeout: int) -> list[dict[str, str]]:
from ssdpy import SSDPClient

client = SSDPClient()
return client.m_search(st=search_targets, mx=timeout)


def run(_) -> list[tuple[set, bytes | str]]:
return [
(
set(),
json.dumps(
run_ssdp(
getenv("SEARCHTARGET", SEARCHTARGET_DEFAULT),
int(getenv("TIMEOUT", TIMEOUT_DEFAULT)),
)
),
)
]
118 changes: 118 additions & 0 deletions boefjes/boefjes/plugins/kat_ssdp/normalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import json
import logging
from collections.abc import Iterator
from ipaddress import ip_address
from urllib.parse import urlparse

from octopoes.models import OOI
from octopoes.models.ooi.dns.zone import Hostname
from octopoes.models.ooi.network import IPAddressV4, IPAddressV6, IPPort, Network, Protocol
from octopoes.models.ooi.scans import SSDPResponse
from octopoes.models.ooi.service import IPService, Service
from octopoes.models.ooi.web import HostnameHTTPURL, HTTPResource, IPAddressHTTPURL, WebScheme, Website


def run(input_ooi: dict, raw: bytes | str) -> Iterator[OOI]:
"""parse SSDP output and yield relevant devices, urls and ips."""

ssdp_responses: list[dict[str, str]] = json.loads(raw)

network = Network(name=input_ooi["name"])
yield network
network_reference = network.reference

logging.info("Parsing SSDP output for %s.", network)
for response in ssdp_responses:
url = None
try:
url = urlparse(response["location"])
logging.info(url)
except KeyError as e:
logging.info("Probably found a response without location. Missing key: %s", e)

yield SSDPResponse(
network=network.reference,
nt=response["nt"],
nts=response["nts"],
server=response["host"],
usn=response["usn"],
)

continue

ip = None
hostname = None

try:
service = Service(name=url.scheme)
yield service

ip = ip_address(url.netloc.split(":")[0])

ip_ooi = (
IPAddressV4(network=network_reference, address=ip)
if ip.version == 4
else IPAddressV6(network=network_reference, address=ip)
)
yield ip_ooi

if url.port:
port = url.port
else:
port = 443 if url.scheme == "https" else 80

# Create the accompanying port
ip_port = IPPort(address=ip_ooi.reference, protocol=Protocol.TCP, port=port)
yield ip_port

# create the service
ip_service = IPService(ip_port=ip_port.reference, service=service.reference)
yield ip_service

except ValueError as e:
logging.info("Response probably contains a hostname instead of an ip. %s", e)

hostname = Hostname(name=url.netloc, network=network_reference)
yield hostname

if ip and ip_ooi: # These should always be both assigned or neither should be assigned
url_ooi = IPAddressHTTPURL(
network=network_reference,
scheme=WebScheme(url.scheme),
port=port,
path=url.path,
netloc=ip_ooi.reference,
)
else:
if not hostname:
logging.error(
"Hostname didn't exist while ip also did not exist. This should not be possible. "
"With the location: %s",
response["location"],
)
continue

url_ooi = HostnameHTTPURL(
network=network_reference,
scheme=WebScheme(url.scheme),
port=port,
path=url.path,
netloc=hostname.reference,
)

website = Website(hostname=hostname.reference, ip_service=ip_service.reference)
yield website

httpresource = HTTPResource(website=website.reference, web_url=url_ooi.reference)
yield httpresource

yield url_ooi

yield SSDPResponse(
web_url=url_ooi.reference,
network=network.reference,
nt=response["nt"],
nts=response["nts"],
server=response["host"],
usn=response["usn"],
)
19 changes: 19 additions & 0 deletions boefjes/boefjes/plugins/kat_ssdp/normalizer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"id": "kat_ssdp_normalize",
"name": "SSDP normalizer",
"consumes": [
"boefje/ssdp"
],
"produces": [
"Network",
"SSDPResponse",
"Service",
"IPAddressV4",
"IPAddressV6",
"IPService",
"Hostname",
"Website",
"HTTPResource",
"HostnameHTTPURL"
]
}
1 change: 1 addition & 0 deletions boefjes/boefjes/plugins/kat_ssdp/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssdpy==0.4.1
17 changes: 17 additions & 0 deletions boefjes/boefjes/plugins/kat_ssdp/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"title": "Arguments",
"type": "object",
"properties": {
"SEARCHTARGET": {
"title": "SEARCHTARGET",
"type": "string",
"description": "What search target to use. Defaults to ssdp:all, needs to be a valid ssdp query."
},
"TIMEOUT": {
"title": "TIMEOUT",
"maxLength": 3,
"type": "string",
"description": "How long in seconds should we wait for answers, defaults to 10."
}
}
}
19 changes: 19 additions & 0 deletions octopoes/octopoes/models/ooi/scans.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import Literal

from octopoes.models import OOI, Reference
from octopoes.models.ooi.network import Network
from octopoes.models.ooi.web import WebURL
from octopoes.models.persistence import ReferenceField


class ExternalScan(OOI):
Expand All @@ -15,3 +18,19 @@ class ExternalScan(OOI):
@classmethod
def format_reference_human_readable(cls, reference: Reference) -> str:
return reference.tokenized.name


class SSDPResponse(OOI):
"""OOI holding information about a found response from SSDP. Example response https://wiki.wireshark.org/SSDP"""

object_type: Literal["SSDPService"] = "SSDPService"

_natural_key_attrs = ["network", "server", "usn"]

web_url: Reference | None = ReferenceField(WebURL, default=None)
network: Reference = ReferenceField(Network)

nt: str
nts: str
server: str
usn: str
4 changes: 2 additions & 2 deletions octopoes/octopoes/models/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
Network,
)
from octopoes.models.ooi.question import Question
from octopoes.models.ooi.scans import ExternalScan, SSDPResponse
from octopoes.models.ooi.reports import Report, ReportData, ReportRecipe
from octopoes.models.ooi.scans import ExternalScan
from octopoes.models.ooi.service import IPService, Service, TLSCipher
from octopoes.models.ooi.software import Software, SoftwareInstance
from octopoes.models.ooi.web import (
Expand Down Expand Up @@ -138,7 +138,7 @@
MonitoringType = Application | Incident
ConfigType = Config
ReportsType = ReportData
ScanType = ExternalScan
ScanType = ExternalScan | SSDPResponse

ConcreteOOIType = (
CertificateType
Expand Down
Loading