Skip to content
Draft

Domino #3183

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2b7d7ee
adding domino module
liquidsec Apr 10, 2025
ab24e53
adding rules option, handling for bad rules used
liquidsec Apr 16, 2025
4cea01d
status code filter
liquidsec Apr 16, 2025
9aaf9f0
Merge branch 'dev' into domino
liquidsec Apr 25, 2025
21deab2
adding domino presets
liquidsec Apr 25, 2025
3c06e9d
bug fix + preset adjust
liquidsec Apr 25, 2025
53e7a9b
supress get parameter FINDING optionally
liquidsec Apr 28, 2025
bd0e1bb
fixing logger
liquidsec May 7, 2025
d85284d
preset-fix
liquidsec May 8, 2025
c49e092
Merge branch 'dev' into domino
liquidsec Jun 12, 2026
64f541f
update domino module for bbot 3.0
liquidsec Jun 12, 2026
6770df0
fix: fall back to event url when domino detection_url is null
liquidsec Jun 13, 2026
e32edfd
perf: reuse shared browser instance instead of launching per URL
liquidsec Jun 13, 2026
d46161e
fix: auto-relaunch browser if it crashes during scan
liquidsec Jun 13, 2026
55d1b11
perf: browser pool with configurable instances and timeout failsafe
liquidsec Jun 13, 2026
d09ba11
feat: expose http_body_hash on URL events, deduplicate domino by cont…
liquidsec Jun 13, 2026
71e9e05
fix: use property override for module_threads, workers spawn before s…
liquidsec Jun 13, 2026
635a8ae
Merge remote-tracking branch 'origin/dev' into domino
liquidsec Jun 14, 2026
3744924
fix: launch fresh browser per handle_event to prevent OOM
liquidsec Jun 16, 2026
431f78b
fix: catch Playwright driver death so the scan survives EPIPE
liquidsec Jun 16, 2026
8eea545
Merge remote-tracking branch 'origin/dev' into domino
liquidsec Jun 16, 2026
4545b41
Merge branch 'dev' into domino
liquidsec Jun 18, 2026
73a221f
Merge branch 'dev' into domino
liquidsec Jun 19, 2026
6c1c949
Restore include_logger, fix domino asyncio + body hash field
liquidsec Jun 19, 2026
3809656
Fix noqa directive in domino.py
liquidsec Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bbot/core/event/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,14 @@ def redirect_location(self):
def redirect_location(self, value):
self.data["redirect_location"] = value

@property
def http_body_hash(self):
return self.data.get("http_body_hash", "")

@http_body_hash.setter
def http_body_hash(self, value):
self.data["http_body_hash"] = value


class URL(URL_UNVERIFIED):
def __init__(self, *args, **kwargs):
Expand Down
132 changes: 132 additions & 0 deletions bbot/modules/domino.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from asyncio import wait_for, TimeoutError as AsyncTimeoutError

from .base import BaseModule

from typing import Optional
from pydantic import Field
from bbot.core.config.models import BaseModuleConfig
from domino.DOMino import Domino
from domino.lib.errors import DominoError
from playwright.async_api import async_playwright


class domino(BaseModule):
watched_events = ["URL"]
produced_events = ["FINDING"]
flags = ["active", "safe"]
meta = {
"description": "Check for Client-side Web Vulnerabilities with DOMino",
"created_date": "2025-04-08",
"author": "@liquidsec",
}

class Config(BaseModuleConfig):
rules: Optional[list[str]] = Field(
default=None,
description="List of rules to run. None for all rules (default).",
)
suppress_parameter_discovery_reports: bool = Field(
default=True,
description="Allow parameter discovery to drive rules but suppress reporting the discovery itself",
)
browser_instances: int = Field(
default=2,
description="Number of concurrent browser instances. Each uses ~800-1600 MB of memory under load.",
)

_module_threads = 2
deps_pip = ["playwright", "d0m1n0"]

@property
def module_threads(self):
return self.config.get("browser_instances", 2)

async def setup(self):
import asyncio.base_subprocess # noqa: E402

def quiet_transport_del(self):
try:
self.close()
except Exception:
pass

asyncio.base_subprocess.BaseSubprocessTransport.__del__ = quiet_transport_del

self.rules = self.config.get("rules")

self._browser_count = self.config.get("browser_instances", 2)
low_estimate = self._browser_count * 800
high_estimate = self._browser_count * 1600
self.warning(
f"The domino module uses Chromium, which consumes a significant amount of memory. "
f"Your current settings will launch {self._browser_count} instances, for an estimated "
f"{low_estimate}-{high_estimate} MB. Lower with -c modules.domino.browser_instances=1"
)

self.playwright = await async_playwright().start()
self.suppress_parameter_discovery_reports = self.config.get("suppress_parameter_discovery_reports", True)
return True

async def handle_event(self, event):
url = event.url
self.debug(f"Domino scanning {url}")
browser = None
try:
browser = await self.playwright.chromium.launch(headless=True)
d = Domino(url=url, logger=self.log, json_mode=True, selected_rules=self.rules)
results = await wait_for(d.run(self.playwright, browser), timeout=120)
except AsyncTimeoutError:
self.warning(f"Domino scan timed out after 120s for {url}")
return
except DominoError as e:
self.hugewarning(f"Error running Domino, setting error state: {e}")
self.errored = True
return
except Exception as e:
self.hugewarning(f"Playwright/Domino fatal error ({type(e).__name__}: {e}), disabling module")
self.errored = True
return
finally:
if browser is not None:
try:
await browser.close()
except Exception:
pass

if results:
for result in results:
if self.suppress_parameter_discovery_reports and "GET Parameter Access" in result["rule_name"]:
continue

details = result.get("details", [])
details_string = f" Details: [{','.join(details)}]" if details else ""

interactions = result.get("interactions", [])
interactions_string = f" Interactions: [{','.join(interactions)}]" if interactions else ""

severity = result.get("severity", "medium").upper()
data = {
"name": result["rule_name"],
"description": f"{result['description']}.{details_string} Detection URL: [{result['detection_url']}]{interactions_string}",
"host": str(event.host),
"url": result.get("detection_url") or event.url,
"severity": severity,
"confidence": "CONFIRMED",
}
await self.emit_event(data, "FINDING", event)
self.debug(f"DOMino scan complete for {url}")

async def cleanup(self):
await self.playwright.stop()

def _incoming_dedup_hash(self, event):
body_hash = getattr(event, "http_body_hash", "")
if body_hash:
return hash((event.host, body_hash)), f"body_hash={body_hash}"
return hash(event), ""

async def filter_event(self, event):
if "status-200" not in event.tags:
self.debug(f"Rejecting URL {event.data} due to lack of 200 status code. Tags: {event.tags}")
return False
return True
1 change: 1 addition & 0 deletions bbot/modules/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ async def _process_result(self, result, parent_event):
response_hash = j.get("hash")
if response_hash:
url_event.data["hash"] = response_hash
url_event.http_body_hash = (j.get("hash") or {}).get("body_sha256", "")
if url_event != parent_event:
await self.emit_event(url_event)
content_type = j.get("header", {}).get("content_type", "unspecified").split(";")[0]
Expand Down
1 change: 1 addition & 0 deletions bbot/presets/kitchen-sink.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ include:
- dirbust-light
- web-screenshots
- baddns-heavy
- domino-heavy

config:
modules:
Expand Down
38 changes: 38 additions & 0 deletions bbot/presets/web/domino-heavy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
description: Run domino with all rules on a target, and use spider and wayback to find as many endpoints as possible

include:
- spider

modules:
- http
- domino
- wayback

config:
modules:
domino:
rules:
- reflection
- xss-basic
- xss-onclick-getparam
- xss-onclick-hash
- xss-referer
- xss-windowname
- xss-interaction
- xss-postmessage
- xss-js
- get-parameter-discovery
- get-parameter-reflection
- transform
- get-parameter-transform
- html-injection
- jquery
- eval
- hash-decode
- postmessage
- remote-include
- remote-include-get-parameters
- prototype-pollution
suppress_parameter_discovery_reports: False
wayback:
urls: True
21 changes: 21 additions & 0 deletions bbot/presets/web/domino-light.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
description: Run domino with a minimal set of rules to only alert on confirmed vulnerabilities


modules:
- http
- domino

config:
modules:
domino:
rules:
- xss-basic
- xss-onclick-getparam
- xss-onclick-hash
- xss-referer
- xss-windowname
- xss-interaction
- xss-postmessage
- xss-js
- prototype-pollution
- remote-include
32 changes: 32 additions & 0 deletions bbot/presets/web/domino.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
description: Run domino with all but the most benign rules, utilize the spider to find additional endpoints

include:
- spider

modules:
- http
- domino

config:
modules:
domino:
rules:
- xss-basic
- xss-onclick-getparam
- xss-onclick-hash
- xss-referer
- xss-windowname
- xss-interaction
- xss-postmessage
- xss-js
- get-parameter-discovery
- prototype-pollution
- remote-include
- remote-include-get-parameters
- jquery
- eval
- transform
- get-parameter-transform
- html-injection
- remote-include
- remote-include-get-parameters
Loading