diff --git a/boefjes/Makefile b/boefjes/Makefile index b51f1aacefd..b72e4c58a3d 100644 --- a/boefjes/Makefile +++ b/boefjes/Makefile @@ -40,6 +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 ./boefjes/plugins/kat_nikto/boefje.Dockerfile -t openkat/nikto . docker build -f ./boefjes/plugins/kat_export_http/boefje.Dockerfile -t ghcr.io/minvws/openkat/export-http:latest . diff --git a/boefjes/boefjes/plugins/kat_kat_finding_types/kat_finding_types.json b/boefjes/boefjes/plugins/kat_kat_finding_types/kat_finding_types.json index a8b69e6a0bf..d3db58cd1d1 100644 --- a/boefjes/boefjes/plugins/kat_kat_finding_types/kat_finding_types.json +++ b/boefjes/boefjes/plugins/kat_kat_finding_types/kat_finding_types.json @@ -516,5 +516,11 @@ "source": "Check the nameservers of the host or iprange manually.", "impact": "No resolving can be done for this IP or host. This might cause problems for mailservers or slow down connections.", "recommendation": "Verify that the listed nameservers are reachable and have valid hostnames." + }, + "KAT-OUTDATED-SOFTWARE": { + "description": "A newer version of existing software has been found.", + "risk": "recommendation", + "impact": "Depending on what software is outdated this can be critical.", + "recommendation": "Inspect the software version, determine if additional measures need to be taken and install updates to reduce the attack surface." } } diff --git a/boefjes/boefjes/plugins/kat_nikto/__init__.py b/boefjes/boefjes/plugins/kat_nikto/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/boefjes/boefjes/plugins/kat_nikto/boefje.Dockerfile b/boefjes/boefjes/plugins/kat_nikto/boefje.Dockerfile new file mode 100644 index 00000000000..ea75a179336 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/boefje.Dockerfile @@ -0,0 +1,13 @@ +FROM perl:5.40 + +WORKDIR /app +RUN apt update +RUN apt install -y git +RUN apt install -y nodejs + +RUN git clone https://github.com/sullo/nikto + +ARG BOEFJE_PATH=./boefjes/plugins/kat_nikto +COPY $BOEFJE_PATH ./ + +ENTRYPOINT [ "node", "./" ] diff --git a/boefjes/boefjes/plugins/kat_nikto/boefje.json b/boefjes/boefjes/plugins/kat_nikto/boefje.json new file mode 100644 index 00000000000..6cd39ab1765 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/boefje.json @@ -0,0 +1,15 @@ +{ + "id": "nikto", + "name": "Nikto", + "description": "Uses Nikto", + "consumes": [ + "HostnameHTTPURL" + ], + "environment_keys": [ + "HTTP_PROXY", + "USERAGENT" + ], + "scan_level": 3, + "oci_image": "openkat/nikto", + "oci_arguments": [] +} diff --git a/boefjes/boefjes/plugins/kat_nikto/cover.jpg b/boefjes/boefjes/plugins/kat_nikto/cover.jpg new file mode 100644 index 00000000000..02d906baa83 Binary files /dev/null and b/boefjes/boefjes/plugins/kat_nikto/cover.jpg differ diff --git a/boefjes/boefjes/plugins/kat_nikto/description.md b/boefjes/boefjes/plugins/kat_nikto/description.md new file mode 100644 index 00000000000..0336056dd49 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/description.md @@ -0,0 +1,16 @@ +# Nikto + +Nikto 2.5 is an Open Source (GPL) web server scanner which performs comprehensive tests against web servers for multiple items, including over 7,000 potentially dangerous files/programs, checks for outdated versions of over 1250 servers, and version specific problems on over 270 servers. It also checks for server configuration items such as the presence of multiple index files, HTTP server options, and will attempt to identify installed web servers and software. Scan items and plugins are frequently updated and can be automatically updated. +(taken from [CIRT.net](https://cirt.net/Nikto2)) + +This boefje has been developed by Soufyan Abdellati from Cynalytics, with help from Edward Hasekamp from IP-Zorg. ♥ + +### Input OOIs + +Nikto expects an HostnameHTTPURL OOI. + +### Output OOIs + +This boefje outputs found outdated software and findings about the HostnameHTTPURL. + +**Cat name**: Kitty diff --git a/boefjes/boefjes/plugins/kat_nikto/main.js b/boefjes/boefjes/plugins/kat_nikto/main.js new file mode 100644 index 00000000000..111764a2257 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/main.js @@ -0,0 +1,99 @@ +import fs from "node:fs"; +import { execSync } from "node:child_process"; + +/** + * @param {string} scheme + * @returns {string} + */ +function get_config_content(scheme) { + const IS_USING_PROXY = !!process.env.HTTP_PROXY; + + // Setup config file + try { + let config_contents = + "PROMPTS=no\nUPDATES=no\nCLIOPTS=-404code=301,302,307,308 -o ./output.json"; + + if (scheme == "https") config_contents += " -ssl"; + if (IS_USING_PROXY) config_contents += " -useproxy"; + config_contents += "\n"; + + if (IS_USING_PROXY) { + const PROXY = new URL(process.env.HTTP_PROXY); + const PROXY_HOST = PROXY.hostname; + const PROXY_PORT = PROXY.port || "8080"; + const PROXY_USER = PROXY.username || ""; + const PROXY_PASS = PROXY.password || ""; + + config_contents += `PROXYHOST=${PROXY_HOST}\n`; + config_contents += `PROXYPORT=${PROXY_PORT}\n`; + config_contents += `PROXYUSER=${PROXY_USER}\n`; + config_contents += `PROXYPASS=${PROXY_PASS}\n`; + } + + if (process.env.USERAGENT) + config_contents += `USERAGENT=${process.env.USERAGENT}\n`; + + return config_contents; + } catch (e) { + throw new Error("Something went wrong writing to the config file.\n" + e); + } +} + +/** + * @param {Object} boefje_meta Information about the task + * @param {Object} boefje_meta.arguments + * @param {Object} boefje_meta.arguments.input + * @param {string} boefje_meta.arguments.input.object_type + * @param {"http" | "https"} boefje_meta.arguments.input.scheme + * @param {number} boefje_meta.arguments.input.port + * @param {Object} boefje_meta.arguments.input.netloc + * @param {string} boefje_meta.arguments.input.netloc.name + * @returns {(string | string[])[][]} + */ +export default function (boefje_meta) { + // Depending on what OOI triggered this task, the hostname / address will be in a different location + const hostname = boefje_meta.arguments.input.netloc.name; + + const config_contents = get_config_content( + boefje_meta.arguments.input.scheme, + ); + fs.writeFileSync("./nikto.conf", config_contents); + + // Running nikto and outputting to a file + try { + execSync(`./nikto/program/nikto.pl -h ${hostname} -config ./nikto.conf`, { + stdio: "inherit", + }); + } catch (e) { + throw new Error( + "Something went wrong running the nikto command.\n" + + e + + "\n" + + config_contents, + ); + } + + const raws = []; + + // Reading the file created by nikto + try { + var file_contents = fs.readFileSync("./output.json").toString(); + raws.push([["boefje/nikto-output"], file_contents]); + } catch (e) { + throw new Error( + "Something went wrong reading the file from the nikto command.\n" + e, + ); + } + + // Looking if outdated software has been found + try { + const data = JSON.parse(file_contents); + for (const vulnerability of data["vulnerabilities"]) + if (vulnerability["id"].startsWith("6")) + raws.push([["openkat/finding"], "KAT-OUTDATED-SOFTWARE"]); + } catch (e) { + console.error(e); + } + + return raws; +} diff --git a/boefjes/boefjes/plugins/kat_nikto/normalize.py b/boefjes/boefjes/plugins/kat_nikto/normalize.py new file mode 100644 index 00000000000..5c543a667b5 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/normalize.py @@ -0,0 +1,64 @@ +import json +from collections.abc import Iterable +from typing import Any + +from boefjes.job_models import NormalizerOutput +from octopoes.models import Reference +from octopoes.models.ooi.findings import Finding, KATFindingType +from octopoes.models.ooi.software import Software, SoftwareInstance + +MISSING_HEADER_TO_KAT_FINDING_TYPE = { + "strict-transport-security": "KAT-HSTS-VULNERABILITIES", + "x-content-type-options": "KAT-NO-X-CONTENT-TYPE-OPTIONS", + "content-security-policy": "KAT-CSP-VULNERABILITIES", + "referrer-policy": "KAT-NO-REFERRER-POLICY", + "permissions-policy": "KAT-NO-PERMISSIONS-POLICY", +} + + +def scan_nikto_output(data: list[dict[str, Any]], ooi_ref: Reference) -> Iterable[NormalizerOutput]: + for scan in data: + for vulnerability in scan["vulnerabilities"]: + vulnerability_id: str = vulnerability["id"] + + # If the scanned vulnerability has to do with outdated software + if vulnerability_id.startswith("6"): + # Example of `vulnerability["msg"]` + # @SOFTWARE/@RUNNING_VER appears to be outdated (current is at least @CURRENT_VER) + software_name, found_version = vulnerability["msg"].split()[0].split("/") + + software = Software(name=software_name, version=found_version) + software_instance = SoftwareInstance(ooi=ooi_ref, software=software.reference) + yield software + yield software_instance + + finding_type = KATFindingType(id="KAT-OUTDATED-SOFTWARE") + yield finding_type + yield Finding( + finding_type=finding_type.reference, + ooi=software_instance.reference, + description=vulnerability["msg"], + ) + + # If the scanned vulnerability has to do with security headers missing + elif vulnerability_id == "013587": + missing_header = vulnerability["msg"].split()[-1].strip(".") + + kat_finding_type_id = MISSING_HEADER_TO_KAT_FINDING_TYPE.get(missing_header) + if kat_finding_type_id is None: + kat_finding_type_id = "KAT-MISSING-HEADER" + finding_type = KATFindingType(id=kat_finding_type_id) + yield finding_type + yield Finding(finding_type=finding_type.reference, ooi=ooi_ref, description=vulnerability["msg"]) + # if the site uses TLS and the Strict-Transport-Security HTTP header is not defined + elif vulnerability_id == "999970": + finding_type = KATFindingType(id="KAT-HSTS-VULNERABILITIES") + yield Finding(finding_type=finding_type.reference, ooi=ooi_ref, description=vulnerability["msg"]) + + +def run(input_ooi: dict, raw: bytes) -> Iterable[NormalizerOutput]: + data = json.loads(raw) + + ooi_ref = Reference.from_str(input_ooi["primary_key"]) + + yield from scan_nikto_output(data, ooi_ref) diff --git a/boefjes/boefjes/plugins/kat_nikto/normalizer.json b/boefjes/boefjes/plugins/kat_nikto/normalizer.json new file mode 100644 index 00000000000..78c0e4c4379 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/normalizer.json @@ -0,0 +1,13 @@ +{ + "id": "kat_nikto_normalize", + "name": "Nikto", + "consumes": [ + "boefje/nikto-output" + ], + "produces": [ + "Software", + "SoftwareInstance", + "Finding", + "KATFindingType" + ] +} diff --git a/boefjes/boefjes/plugins/kat_nikto/oci_adapter.js b/boefjes/boefjes/plugins/kat_nikto/oci_adapter.js new file mode 100644 index 00000000000..83ae47927f1 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/oci_adapter.js @@ -0,0 +1,64 @@ +import { execSync } from "node:child_process"; +import run from "./main.js"; + +/** + * @param {string} inp The string input to base64 + * @returns {string} + */ +function b64encode(inp) { + return Buffer.from(inp).toString("base64"); +} + +function main() { + const input_url = process.argv[process.argv.length - 1]; + + // Getting the boefje input + try { + var boefje_input = JSON.parse( + execSync(`curl --request GET --url ${input_url}`).toString(), + ); + } catch (error) { + console.error(`Getting boefje input went wrong with URL: ${input_url}`); + throw new Error(error); + } + + Object.assign(process.env, boefje_input["boefje_meta"]["environment"]); + + let out = undefined; + let output_url = boefje_input.output_url; + try { + // Getting the raw files + const raws = run(boefje_input.boefje_meta); + out = { + status: "COMPLETED", + files: raws.map((x) => ({ + content: b64encode(x[1]), + tags: x[0], + })), + }; + } catch (error) { + out = { + status: "FAILED", + files: [ + { + content: b64encode("Boefje caught an error: " + error.message), + tags: ["error/boefje"], + }, + ], + }; + } + + // Example command + /* + curl --request POST \ + --url http://boefje:8000/api/v0/tasks/7342e8dd-b945-4185-aaec-787205b7b664 \ + --header 'Content-Type: application/json' \ + --data '{"status":"COMPLETED","files":[{"content":"BASE_64_ENCODED_CONTENT","tags":[]}]}' + */ + const out_json = JSON.stringify(out); + const cmd = `curl --request POST --url ${output_url} --header "Content-Type: application/json" --data '${out_json}'`; + + execSync(cmd); +} + +main(); diff --git a/boefjes/boefjes/plugins/kat_nikto/package-lock.json b/boefjes/boefjes/plugins/kat_nikto/package-lock.json new file mode 100644 index 00000000000..daa0ba14207 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "kat-nikto", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "kat-nikto", + "version": "1.0.0", + "dependencies": { + "@types/node": "^22.1.0" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" + } + }, + "dependencies": { + "@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "requires": { + "undici-types": "~6.13.0" + } + }, + "undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" + } + } +} diff --git a/boefjes/boefjes/plugins/kat_nikto/package.json b/boefjes/boefjes/plugins/kat_nikto/package.json new file mode 100644 index 00000000000..890572426c1 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/package.json @@ -0,0 +1,10 @@ +{ + "name": "kat-nikto", + "version": "1.0.0", + "type": "module", + "main": "oci_adapter.js", + "author": "cynalytics", + "dependencies": { + "@types/node": "^22.1.0" + } +} diff --git a/boefjes/boefjes/plugins/kat_nikto/schema.json b/boefjes/boefjes/plugins/kat_nikto/schema.json new file mode 100644 index 00000000000..f5b3b917eee --- /dev/null +++ b/boefjes/boefjes/plugins/kat_nikto/schema.json @@ -0,0 +1,18 @@ +{ + "title": "Arguments", + "type": "object", + "properties": { + "HTTP_PROXY": { + "title": "HTTP_PROXY", + "maxLength": 512, + "type": "string", + "description": "The full URL for the proxy.\nE.g. \"http://user:pass@127.0.0.1:8080/\"" + }, + "USERAGENT": { + "title": "USERAGENT", + "maxLength": 256, + "type": "string" + } + }, + "required": [] +} diff --git a/boefjes/tests/examples/raw/nikto-example.com.json b/boefjes/tests/examples/raw/nikto-example.com.json new file mode 100644 index 00000000000..264320b0cb8 --- /dev/null +++ b/boefjes/tests/examples/raw/nikto-example.com.json @@ -0,0 +1,23 @@ +[ + { + "host": "example.com", + "ip": "178.128.108.228", + "port": "80", + "banner": "", + "vulnerabilities": [ + { + "id": "600575", + "method": "HEAD", + "url": "/", + "msg": "nginx/1.18.0 appears to be outdated (current is at least 1.25.3)." + }, + { + "id": "999103", + "references": "https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/missing-content-type-header/", + "method": "GET", + "url": "/", + "msg": "The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type." + } + ] + } +] diff --git a/boefjes/tests/examples/raw/nikto-non-existing.com.json b/boefjes/tests/examples/raw/nikto-non-existing.com.json new file mode 100644 index 00000000000..ce45231ff33 --- /dev/null +++ b/boefjes/tests/examples/raw/nikto-non-existing.com.json @@ -0,0 +1,16 @@ +[ + { + "host": "non-existing.com", + "ip": "2.1.3.5", + "port": "6667", + "banner": "", + "vulnerabilities": [ + { + "id": "0", + "method": "GET", + "url": "/", + "msg": "Unable to connect to non-existing.com." + } + ] + } +] diff --git a/boefjes/tests/integration/test_api.py b/boefjes/tests/integration/test_api.py index 6c2ae495f47..b7c45c0a95d 100644 --- a/boefjes/tests/integration/test_api.py +++ b/boefjes/tests/integration/test_api.py @@ -130,7 +130,7 @@ def test_add_normalizer(test_client, organisation): assert response.status_code == 201 response = test_client.get(f"/v1/organisations/{organisation.id}/plugins/?plugin_type=normalizer") - assert len(response.json()) == 57 + assert len(response.json()) == 58 response = test_client.get(f"/v1/organisations/{organisation.id}/plugins/test_normalizer") assert response.json() == normalizer.model_dump() diff --git a/boefjes/tests/test_nikto_normalizer.py b/boefjes/tests/test_nikto_normalizer.py new file mode 100644 index 00000000000..2ad15368166 --- /dev/null +++ b/boefjes/tests/test_nikto_normalizer.py @@ -0,0 +1,36 @@ +from unittest import TestCase + +from boefjes.plugins.kat_nikto.normalize import run +from octopoes.models import Reference +from octopoes.models.ooi.findings import KATFindingType +from octopoes.models.ooi.software import Software, SoftwareInstance +from octopoes.models.types import Finding +from tests.loading import get_dummy_data + + +class CVETest(TestCase): + def test_outdated_found(self): + input_ooi = {"primary_key": "Hostname|internet|example.com"} + ooi_ref = Reference.from_str(input_ooi["primary_key"]) + + oois = list(run(input_ooi, get_dummy_data("raw/nikto-example.com.json"))) + + software = Software(name="nginx", version="1.18.0") + finding_type = KATFindingType(id="KAT-OUTDATED-SOFTWARE") + software_instance = SoftwareInstance(ooi=ooi_ref, software=software.reference) + finding = Finding( + finding_type=finding_type.reference, + ooi=software_instance.reference, + description="nginx/1.18.0 appears to be outdated (current is at least 1.25.3).", + ) + + expected = [software, software_instance, finding_type, finding] + + self.assertEqual(expected, oois) + + def test_nothing_found(self): + input_ooi = {"primary_key": "Hostname|internet|non-existing.com"} + + oois = list(run(input_ooi, get_dummy_data("raw/nikto-non-existing.com.json"))) + + self.assertEqual([], oois)