Skip to content

Commit

Permalink
Merge pull request #896 from domwhewell-sage/dastardly-scanner
Browse files Browse the repository at this point in the history
Dastardly scanner
  • Loading branch information
TheTechromancer authored Dec 12, 2023
2 parents 15d0798 + 6007edd commit 5339c8e
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 5 deletions.
6 changes: 3 additions & 3 deletions bbot/core/helpers/depsinstaller/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(self, parent_helper):
self.setup_status = self.read_setup_status()

self.no_deps = self.parent_helper.config.get("no_deps", False)
self.ansible_debug = self.parent_helper.config.get("debug", False)
self.ansible_debug = True
self.force_deps = self.parent_helper.config.get("force_deps", False)
self.retry_deps = self.parent_helper.config.get("retry_deps", False)
self.ignore_failed_deps = self.parent_helper.config.get("ignore_failed_deps", False)
Expand Down Expand Up @@ -278,8 +278,8 @@ def ansible_run(self, tasks=None, module=None, args=None, ansible_args=None):
success = res.status == "successful"
err = ""
for e in res.events:
# if self.ansible_debug and not success:
# log.debug(json.dumps(e, indent=4))
if self.ansible_debug and not success:
log.debug(json.dumps(e, indent=4))
if e["event"] == "runner_on_failed":
err = e["event_data"]["res"]["msg"]
break
Expand Down
147 changes: 147 additions & 0 deletions bbot/modules/deadly/dastardly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from lxml import etree
from bbot.modules.base import BaseModule


class dastardly(BaseModule):
watched_events = ["HTTP_RESPONSE"]
produced_events = ["FINDING", "VULNERABILITY"]
flags = ["active", "aggressive", "slow", "web-thorough"]
meta = {"description": "Lightweight web application security scanner"}

deps_pip = ["lxml~=4.9.2"]
deps_ansible = [
{
"name": "Check if Docker is already installed",
"command": "docker --version",
"register": "docker_installed",
"ignore_errors": True,
},
{
"name": "Install Docker (Non-Debian)",
"package": {"name": "docker", "state": "present"},
"become": True,
"when": "ansible_facts['os_family'] != 'Debian' and docker_installed.rc != 0",
},
{
"name": "Install Docker (Debian)",
"package": {
"name": "docker.io",
"state": "present",
},
"become": True,
"when": "ansible_facts['os_family'] == 'Debian' and docker_installed.rc != 0",
},
]
per_host_only = True

async def setup(self):
await self.helpers.run("systemctl", "start", "docker", sudo=True)
await self.helpers.run("docker", "pull", "public.ecr.aws/portswigger/dastardly:latest", sudo=True)
self.output_dir = self.scan.home / "dastardly"
self.helpers.mkdir(self.output_dir)
return True

async def filter_event(self, event):
# Reject redirects. This helps to avoid scanning the same site twice.
is_redirect = str(event.data["status_code"]).startswith("30")
if is_redirect:
return False, "URL is a redirect"
return True

async def handle_event(self, event):
host = event.parsed._replace(path="/").geturl()
self.verbose(f"Running Dastardly scan against {host}")
command, output_file = self.construct_command(host)
finished_proc = await self.helpers.run(command, sudo=True)
self.debug(f'dastardly stdout: {getattr(finished_proc, "stdout", "")}')
self.debug(f'dastardly stderr: {getattr(finished_proc, "stderr", "")}')
for testsuite in self.parse_dastardly_xml(output_file):
url = testsuite.endpoint
for testcase in testsuite.testcases:
for failure in testcase.failures:
if failure.severity == "Info":
self.emit_event(
{
"host": str(event.host),
"url": url,
"description": failure.instance,
},
"FINDING",
event,
)
else:
self.emit_event(
{
"severity": failure.severity,
"host": str(event.host),
"url": url,
"description": failure.instance,
},
"VULNERABILITY",
event,
)

def construct_command(self, target):
date_time = self.helpers.make_date()
file_name = self.helpers.tagify(target)
temp_path = self.output_dir / f"{date_time}_{file_name}.xml"
command = [
"docker",
"run",
"--user",
"0",
"--rm",
"-v",
f"{self.output_dir}:/dastardly",
"-e",
f"BURP_START_URL={target}",
"-e",
f"BURP_REPORT_FILE_PATH=/dastardly/{temp_path.name}",
"public.ecr.aws/portswigger/dastardly:latest",
]
return command, temp_path

def parse_dastardly_xml(self, xml_file):
try:
with open(xml_file, "rb") as f:
et = etree.parse(f)
for testsuite in et.iter("testsuite"):
yield TestSuite(testsuite)
except Exception as e:
self.warning(f"Error parsing Dastardly XML at {xml_file}: {e}")


class Failure:
def __init__(self, xml):
self.etree = xml

# instance information
self.instance = self.etree.attrib.get("message", "")
self.severity = self.etree.attrib.get("type", "")
self.text = self.etree.text


class TestCase:
def __init__(self, xml):
self.etree = xml

# title information
self.title = self.etree.attrib.get("name", "")

# findings / failures(as dastardly names them)
self.failures = []
for failure in self.etree.findall("failure"):
self.failures.append(Failure(failure))


class TestSuite:
def __init__(self, xml):
self.etree = xml

# endpoint information
self.endpoint = self.etree.attrib.get("name", "")

# test cases
self.testcases = []
for testcase in self.etree.findall("testcase"):
self.testcases.append(TestCase(testcase))
14 changes: 14 additions & 0 deletions bbot/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ def bbot_httpserver_ssl():
server.clear()


@pytest.fixture
def bbot_httpserver_allinterfaces():
server = HTTPServer(host="0.0.0.0", port=5556)
server.start()

yield server

server.clear()
if server.is_running():
server.stop()
server.check_assertions()
server.clear()


@pytest.fixture
def interactsh_mock_instance():
interactsh_mock = Interactsh_mock()
Expand Down
4 changes: 2 additions & 2 deletions bbot/test/test.conf
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ scope_search_distance: 0
scope_report_distance: 0
scope_dns_search_distance: 1
plumbus: asdf
dns_debug: true
dns_debug: false
user_agent: "BBOT Test User-Agent"
http_debug: false
agent_url: ws://127.0.0.1:8765
Expand All @@ -48,4 +48,4 @@ dns_wildcard_ignore:
- google
- google.com
- example.com
- evilcorp.com
- evilcorp.com
64 changes: 64 additions & 0 deletions bbot/test/test_step_2/module_tests/test_module_dastardly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import json
from werkzeug import Response

from .base import ModuleTestBase


class TestDastardly(ModuleTestBase):
targets = ["http://127.0.0.1:5556/"]
modules_overrides = ["httpx", "dastardly"]

web_response = """<!DOCTYPE html>
<html>
<body>
<a href="/test?test=yes">visit this<a/>
</body>
</html>"""

def xss_handler(self, request):
response = f"""<!DOCTYPE html>
<html>
<head>
<title>Email Form</title>
</head>
<body>
{request.args.get("test", "")}
</body>
</html>"""
return Response(response, content_type="text/html")

async def get_docker_ip(self, module_test):
docker_ip = "172.17.0.1"
try:
ip_output = await module_test.scan.helpers.run(["ip", "-j", "-4", "a", "show", "dev", "docker0"])
interface_json = json.loads(ip_output.stdout)
docker_ip = interface_json[0]["addr_info"][0]["local"]
except Exception:
pass
return docker_ip

async def setup_after_prep(self, module_test):
httpserver = module_test.request_fixture.getfixturevalue("bbot_httpserver_allinterfaces")
httpserver.expect_request("/").respond_with_data(self.web_response)
httpserver.expect_request("/test").respond_with_handler(self.xss_handler)

# get docker IP
docker_ip = await self.get_docker_ip(module_test)
module_test.scan.target.add_target(docker_ip)

# replace 127.0.0.1 with docker host IP to allow dastardly access to local http server
old_filter_event = module_test.module.filter_event

def new_filter_event(event):
self.new_url = f"http://{docker_ip}:5556/"
event.data["url"] = self.new_url
event.parsed = module_test.scan.helpers.urlparse(self.new_url)
return old_filter_event(event)

module_test.monkeypatch.setattr(module_test.module, "filter_event", new_filter_event)

def check(self, module_test, events):
assert 1 == len([e for e in events if e.type == "VULNERABILITY"])
assert 1 == len(
[e for e in events if e.type == "VULNERABILITY" and f"{self.new_url}test" in e.data["description"]]
)

0 comments on commit 5339c8e

Please sign in to comment.