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

Wordpress Security Scanner #66

Merged
merged 21 commits into from
Mar 11, 2024
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ AGPL-3.0.
Uses https://github.com/sqlmapproject/sqlmap under the hood. Finds SQL injection vulnerabilities and is
licensed under GPL-2.0.

### `wpscan`
Uses https://github.com/wpscanteam/wpscan under the hood. Finds vulnerabilities on sites that use WordPress.
By using this module you confirm that you have read carefully the terms and conditions of the license in
https://github.com/wpscanteam/wpscan/blob/master/LICENSE and agree to respect them, in particular in
ensuring no conflict with the commercialization clause. For the avoidance of doubt, in any case, you
remain solely liable for how you use this module and your compliance with wpscan’s license, and
NASK is relieved of such liability to the fullest extent possible.

The module is disabled by default - to enable it, add `-f Artemis-modules-extra/docker-compose.additional.wpscan.yml` to
the `docker compose up` command.

## Testing
To run the tests, run:

Expand Down
Empty file.
85 changes: 85 additions & 0 deletions autoreporter_addons/wpscan/reporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from pathlib import Path
from typing import Any, Callable, Dict, List

from artemis.reporting.base.language import Language
from artemis.reporting.base.normal_form import (
NormalForm,
get_url_normal_form,
get_url_score,
)
from artemis.reporting.base.report import Report
from artemis.reporting.base.report_type import ReportType
from artemis.reporting.base.reporter import Reporter
from artemis.reporting.base.templating import ReportEmailTemplateFragment
from artemis.reporting.utils import get_top_level_target


class WPScanReporter(Reporter): # type: ignore
WPSCAN_VULNERABILITY = ReportType("wpscan_vulnerability")
WPSCAN_INTERESTING_URL = ReportType("wpscan_interesting_url")

@staticmethod
def create_reports(task_result: Dict[str, Any], language: Language) -> List[Report]:
if task_result["headers"]["receiver"] != "wpscan":
return []

if not isinstance(task_result["result"], dict):
return []

result = []
for item in task_result["result"]["vulnerabilities"]:
result.append(
Report(
top_level_target=get_top_level_target(task_result),
target=task_result["target_string"],
report_type=WPScanReporter.WPSCAN_VULNERABILITY,
additional_data={"vulnerability": item},
timestamp=task_result["created_at"],
)
)
for item in task_result["result"]["interesting_urls"]:
result.append(
Report(
top_level_target=get_top_level_target(task_result),
target=task_result["target_string"],
report_type=WPScanReporter.WPSCAN_INTERESTING_URL,
additional_data={"url": item},
timestamp=task_result["created_at"],
)
)
return result

@staticmethod
def get_email_template_fragments() -> List[ReportEmailTemplateFragment]:
return [
ReportEmailTemplateFragment.from_file(
str(Path(__file__).parents[0] / "template_wpscan_vulnerability.jinja2"), priority=7
),
ReportEmailTemplateFragment.from_file(
str(Path(__file__).parents[0] / "template_wpscan_interesting_url.jinja2"), priority=3
),
]

@staticmethod
def get_scoring_rules() -> Dict[ReportType, Callable[[Report], List[int]]]:
"""See the docstring in the parent class."""
return {report_type: WPScanReporter.scoring_rule for report_type in WPScanReporter.get_report_types()}

@staticmethod
def get_normal_form_rules() -> Dict[ReportType, Callable[[Report], NormalForm]]:
"""See the docstring in the Reporter class."""
return {report_type: WPScanReporter.normal_form_rule for report_type in WPScanReporter.get_report_types()}

@staticmethod
def scoring_rule(report: Report) -> List[int]:
return [get_url_score(report.target)]

@staticmethod
def normal_form_rule(report: Report) -> NormalForm:
return Reporter.dict_to_tuple(
{
"type": report.report_type,
"target": get_url_normal_form(report.target),
"additional_data": report.additional_data,
}
)
19 changes: 19 additions & 0 deletions autoreporter_addons/wpscan/template_wpscan_interesting_url.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% if "wpscan_interesting_url" in data.contains_type %}
<li>{% trans %}We identified interesting locations that were accessible in your WordPress application:{% endtrans %}
<ul>
{% for report in data.reports %}
{% if report.report_type == "wpscan_interesting_url" %}
<li>
{{ report.target }}: {{ _(report.additional_data.url) }}
{{ report_meta(report) }}
</li>
{% endif %}
{% endfor %}
</ul>
<p>
{% trans trimmed %}
Please verify the configuration and if the locations do not have to be accessible, change your configuration accordingly.
{% endtrans %}
</p>
</li>
{% endif %}
23 changes: 23 additions & 0 deletions autoreporter_addons/wpscan/template_wpscan_vulnerability.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% if "wpscan_vulnerability" in data.contains_type %}
<li>{% trans %}We identified the following vulnerabilities on the scanned WordPress instances:{% endtrans %}
<ul>
{% for report in data.reports %}
{% if report.report_type == "wpscan_vulnerability" %}
<li>
{{ report.target }}: {{ _(report.additional_data.vulnerability) }}
{{ report_meta(report) }}
</li>
{% endif %}
{% endfor %}
</ul>
<p>
{% trans trimmed %}
Please verify the configuration, and check whether your instance is vulnerable.

If a site is not used anymore, we recommend shutting it down to avoid the risk of exploitation of
known vulnerabilities. If it is used, we recommend enabling WordPress core, plugin and
theme automatic updates.
{% endtrans %}
</p>
</li>
{% endif %}
19 changes: 19 additions & 0 deletions docker-compose.additional.wpscan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3"

services:
# Before using this docker-compose.yml file, please read the license disclaimer in README.md.
karton-wpscan:
build:
context: Artemis-modules-extra
dockerfile: karton_wpscan/Dockerfile
volumes:
- "./docker/karton.ini:/etc/karton/karton.ini"
- "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"
depends_on: [karton-system]
env_file: .env
restart: always
command: "python3 -m artemis.modules.karton_wpscan"

autoreporter:
volumes:
- ./Artemis-modules-extra/autoreporter_addons/wpscan/:/opt/artemis/reporting/modules/wpscan/
12 changes: 9 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ services:
build:
context: Artemis-modules-extra
dockerfile: karton_dns_reaper/Dockerfile
volumes: ["./docker/karton.ini:/etc/karton/karton.ini", "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"]
volumes:
- "./docker/karton.ini:/etc/karton/karton.ini"
- "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"
depends_on: [karton-system]
env_file: .env
restart: always
Expand All @@ -15,7 +17,9 @@ services:
build:
context: Artemis-modules-extra
dockerfile: karton_sqlmap/Dockerfile
volumes: ["./docker/karton.ini:/etc/karton/karton.ini", "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"]
volumes:
- "./docker/karton.ini:/etc/karton/karton.ini"
- "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"
depends_on: [karton-system]
env_file: .env
restart: always
Expand All @@ -25,7 +29,9 @@ services:
build:
context: Artemis-modules-extra
dockerfile: karton_ssl_checks/Dockerfile
volumes: ["./docker/karton.ini:/etc/karton/karton.ini", "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"]
volumes:
- "./docker/karton.ini:/etc/karton/karton.ini"
- "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"
depends_on: [karton-system]
env_file: .env
restart: always
Expand Down
3 changes: 3 additions & 0 deletions extra_modules_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,6 @@ class ExtraModulesConfig:
]
),
)

# WPScan API key
WPSCAN_API_KEY = decouple.config("WPSCAN_API_KEY", default=None)
9 changes: 9 additions & 0 deletions karton_wpscan/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM certpl/artemis:latest

RUN apk add --no-cache git ruby ruby-dev libc-dev build-base make linux-headers

RUN git clone https://github.com/wpscanteam/wpscan.git /wpscan
RUN cd /wpscan && gem install bundler && bundle install && rake install && wpscan --update

COPY karton_wpscan/karton_wpscan.py /opt/artemis/modules/
COPY extra_modules_config.py /opt/
118 changes: 118 additions & 0 deletions karton_wpscan/karton_wpscan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
import json
import subprocess
import urllib.parse

from artemis.binds import TaskStatus, TaskType
from artemis.module_base import ArtemisBase
from karton.core import Task

from extra_modules_config import ExtraModulesConfig


class ScanningException(Exception):
pass


class WPScan(ArtemisBase): # type: ignore
"""
Runs WPScan -> WordPress Vulnerability Scanner
"""

identity = "wpscan"
filters = [
{"type": TaskType.WEBAPP.value, "webapp": "wordpress"},
]

def run(self, current_task: Task) -> None:
target_url = current_task.payload["url"]

wpscan_api_key = ExtraModulesConfig.WPSCAN_API_KEY
if not wpscan_api_key:
# Run WPScan and get the JSON output without API key
data = subprocess.run(
[
"wpscan",
"--url",
target_url,
"--no-update",
"--disable-tls-checks",
"--format",
"json",
"--random-user-agent",
],
capture_output=True,
)
elif wpscan_api_key:
# Run WPScan and get the JSON output with API key

data = subprocess.run(
[
"wpscan",
"--url",
target_url,
"--no-update",
"--disable-tls-checks",
"--format",
"json",
"--random-user-agent",
"--api-token",
wpscan_api_key,
],
capture_output=True,
)

try:
result = json.loads(data.stdout.decode("utf-8"))
except json.JSONDecodeError:
result = {"error": "Failed to decode JSON output from WPScan."}

if result.get("scan_aborted", None):
raise ScanningException(result["scan_aborted"])

if not result:
result = {"error": "No JSON output from WPScan."}

interesting_urls = []
if "interesting_findings" in result:
for entry in result["interesting_findings"]:
if "url" in entry and urllib.parse.urlparse(entry["url"]).path.strip("/") != "":
interesting_urls.append(entry["url"])

vulnerabilities = []
for entry in result.get("plugins", {}).values():
for vulnerability in entry["vulnerabilities"]:
vulnerabilities.append(vulnerability["title"])
for entry in result.get("themes", {}).values():
for vulnerability in entry["vulnerabilities"]:
vulnerabilities.append(vulnerability["title"])
for vulnerability in result.get("main_theme", {}).get("vulnerabilities", []):
vulnerabilities.append(vulnerability["title"])

wp_version = result.get("version", {}).get("number", "")

messages = [
f"Vulnerabilities: {vulnerabilities}",
f"Interesting URLs: {interesting_urls}",
f"WP Version: {wp_version}",
]

# Determine the task status based on the messages
if vulnerabilities or interesting_urls:
status = TaskStatus.INTERESTING
status_reason = ", ".join(messages)
else:
status = TaskStatus.OK
status_reason = None

# Save the task result to the database
self.db.save_task_result(
task=current_task,
status=status,
status_reason=status_reason,
data={"vulnerabilities": vulnerabilities, "interesting_urls": interesting_urls, "result": result},
)


if __name__ == "__main__":
WPScan().loop()
Loading