Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
50a079c
Add ECB mode detection and CBC bit-flipping detection to crypto submo…
liquidsec Mar 14, 2026
cd381f8
Reduce lightfuzz false positives: centralize WAF detection, blacklist…
liquidsec Mar 15, 2026
2563c76
Skip parameter extraction from out-of-scope redirect targets
liquidsec Mar 16, 2026
749a695
Fix lightfuzz false positives: crypto jitter stability, XSS context v…
liquidsec Mar 18, 2026
40f6bb9
Remove duplicate get_waf_strings() definition
liquidsec Mar 20, 2026
abb1719
Skip Error Resolution for non-standard HTTP status codes (>511)
liquidsec Mar 25, 2026
79349dd
Fix excavate redirect test to use e.url instead of e.data
liquidsec Mar 27, 2026
9fc3837
Use YARA for WAF string detection instead of Python string-in loops
liquidsec Apr 1, 2026
ab42ab3
Fix test module refs: httpx → http for blasthttp migration
liquidsec Apr 3, 2026
f3be8cd
Compile serial error YARA rules in lightfuzz setup(), simplify sqli W…
liquidsec Apr 3, 2026
11893a3
Merge branch 'blasthttp-integration-clean' into lightfuzz-improvement…
liquidsec Apr 3, 2026
1b85374
Merge branch 'blasthttp-integration-clean' into lightfuzz-improvement…
liquidsec Apr 3, 2026
52c0cc8
Reduce lightfuzz sqli false positives: url_extension filtering, confi…
liquidsec Apr 13, 2026
70f04cf
Remove probe request descriptions from sqli findings
liquidsec Apr 13, 2026
32ae4e5
Add Azure Application Gateway to WAF detection strings
liquidsec Apr 13, 2026
aee4fc8
Fix test assertions for sqli probe description removal and static URL…
liquidsec Apr 14, 2026
b4fb2b7
Lightfuzz FN-hunt round 1: sqli, path, cmdi
liquidsec Apr 24, 2026
b06096f
Lightfuzz FN-hunt round 2: xss context coverage
liquidsec Apr 24, 2026
8724337
Lightfuzz FN-hunt round 3: ssti direct probes + FP guard
liquidsec Apr 24, 2026
c18daa5
Lightfuzz FN-hunt round 4: ssti velocity coverage
liquidsec Apr 24, 2026
6a4f979
Lightfuzz FN-hunt round 5: serial pickle + skip_envelopes opt-in
liquidsec Apr 24, 2026
14d9fdb
Lightfuzz FN-hunt round 6: serial Java URLDNS OOB
liquidsec Apr 24, 2026
f12cbed
Lightfuzz FN-hunt round 6 fixup: URLDNS flag fix + interactsh NPE
liquidsec Apr 24, 2026
cf5e7ac
Lightfuzz crypto: small cleanup
liquidsec Apr 24, 2026
94e65ec
Lightfuzz FN-hunt round 7: esi remote-include OOB
liquidsec Apr 24, 2026
766e70d
Lightfuzz path: simple non-recursive-strip probes
liquidsec Apr 24, 2026
86cb595
Lightfuzz path: simple double-URL-encoding probes
liquidsec Apr 24, 2026
550ff1f
Lightfuzz: extract register_interactsh_tag helper
liquidsec Apr 24, 2026
18822eb
Merge branch 'blasthttp-integration-clean' into lightfuzz-improvement…
liquidsec Apr 24, 2026
18deb6c
Lightfuzz xss/path: tighten URL-scheme + singledot detectors
liquidsec Apr 25, 2026
6aa8e40
Merge remote-tracking branch 'origin/blasthttp-integration-clean' int…
liquidsec May 1, 2026
f6876ee
lightfuzz/sqli: junk-value control probe to suppress CDN cache-miss f…
liquidsec May 7, 2026
9b2592d
excavate: prefer <option selected> over first option, fall back to fi…
liquidsec May 11, 2026
3b04c69
Merge remote-tracking branch 'origin/blasthttp-integration-clean' int…
liquidsec May 11, 2026
4190110
lightfuzz: emit canonical baseline responses as HTTP_RESPONSE events
liquidsec May 11, 2026
dadc97d
docs: frame lightfuzz as BBOT's DAST module
liquidsec May 11, 2026
2bbc059
excavate: consolidate same-name params into same_param_values; loosen…
liquidsec May 11, 2026
67bf00a
lightfuzz: keystream-reuse detection, from-lightfuzz tag, cookie refresh
liquidsec May 11, 2026
ef11c29
lightfuzz: declare HTTP_RESPONSE in produced_events
liquidsec May 11, 2026
6061f4c
lightfuzz: baseline_probe submits the form properly
liquidsec May 11, 2026
b48d1e4
excavate: route form discovery through opening-tag YARA + bounded bod…
liquidsec May 11, 2026
fe63fe4
docs: document lightfuzz crypto's keystream-reuse / many-time-pad det…
liquidsec May 11, 2026
20023d0
lightfuzz: coerce None to empty string in prepare_request wire serial…
liquidsec May 11, 2026
e208d3b
lightfuzz: dual baseline_probe (form-as-captured + populate-empty-as-…
liquidsec May 11, 2026
fe6c309
lightfuzz: host_url priming + coalesced connectivity_test; excavate: …
liquidsec May 12, 2026
555df35
trim verbose comments/docstrings; fix web_parameters test against pos…
liquidsec May 12, 2026
d8bf489
lightfuzz/crypto: restrict keystream-reuse to hex inputs
liquidsec May 16, 2026
e97980c
lightfuzz/crypto: guard None probe responses and fix invalid confiden…
liquidsec May 17, 2026
9d5c5d5
lightfuzz audit fixes: cache bound, cmdi guard, xss verify, defaults …
liquidsec May 17, 2026
ceb8cf6
lightfuzz/crypto: catch HttpCompareError in padding_oracle_execute
liquidsec May 21, 2026
2cb2ad6
lightfuzz/path: add canary check to suppress search-field FPs
liquidsec May 21, 2026
493043b
Merge remote-tracking branch 'origin/dev' into lightfuzz-improvements…
liquidsec May 21, 2026
649143a
lightfuzz/reflected_parameters: merge probe into existing querystring
liquidsec May 26, 2026
8a7a9d1
excavate: read response body via event.body in ParameterExtractor.pro…
liquidsec May 26, 2026
da49503
Merge branch 'dev' into lightfuzz-improvements-mar-26
liquidsec Jun 7, 2026
6f47bdc
sqli: two-stage scaling-delay confirmation for time-based blind detec…
liquidsec Jun 7, 2026
e2aaa95
Merge pull request #3152 from blacklanternsecurity/sqli-fp-sliding-delay
liquidsec Jun 8, 2026
a8207fe
Merge dev, resolve conflicts in http/excavate/lightfuzz pydantic Conf…
liquidsec Jun 8, 2026
75bf826
Fix keystream-reuse FP, cmdi octal bug, type mutation leak, connectiv…
liquidsec Jun 9, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ For details, see [Configuration](https://www.blacklanternsecurity.com/bbot/Stabl
- [List of Modules](https://www.blacklanternsecurity.com/bbot/Stable/modules/list_of_modules)
- [Nuclei](https://www.blacklanternsecurity.com/bbot/Stable/modules/nuclei)
- [Custom YARA Rules](https://www.blacklanternsecurity.com/bbot/Stable/modules/custom_yara_rules)
- [Lightfuzz](https://www.blacklanternsecurity.com/bbot/Stable/modules/lightfuzz)
- [Lightfuzz (DAST)](https://www.blacklanternsecurity.com/bbot/Stable/modules/lightfuzz)
- **Misc**
- [Contribution](https://www.blacklanternsecurity.com/bbot/Stable/contribution)
- [Release History](https://www.blacklanternsecurity.com/bbot/Stable/release_history)
Expand Down
14 changes: 12 additions & 2 deletions bbot/core/event/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ def parent(self, parent):
self.web_spider_distance = getattr(parent, "web_spider_distance", 0)
event_has_url = getattr(self, "parsed_url", None) is not None
for t in parent.tags:
if t in ("affiliate",):
if t in ("affiliate", "from-lightfuzz"):
self.add_tag(t)
elif t.startswith("mutation-"):
self.add_tag(t)
Expand Down Expand Up @@ -1135,10 +1135,20 @@ def sanitize_data(self, data):


class DictEvent(BaseEvent):
__slots__ = ["url_extension"]

def sanitize_data(self, data):
url = data.get("url", "")
if url:
self.parsed_url = self.validators.validate_url_parsed(url)
# extract url_extension from any dict event with a URL
url_path = self.parsed_url.path
if url_path:
parsed_path_lower = str(url_path).lower()
extension = get_file_extension(parsed_path_lower)
if extension:
self.url_extension = extension
self.add_tag(f"extension-{extension}")
return data

def _data_load(self, data):
Expand Down Expand Up @@ -1366,7 +1376,6 @@ class URL_UNVERIFIED(DictHostEvent):

__slots__ = [
"web_spider_distance",
"url_extension",
"num_redirects",
]

Expand Down Expand Up @@ -1567,6 +1576,7 @@ def _minimize(self):
self._data.pop("original_value", None)
self._data.pop("additional_params", None)
self._data.pop("assigned_cookies", None)
self._data.pop("same_param_values", None)

@property
def children(self):
Expand Down
9 changes: 9 additions & 0 deletions bbot/core/helpers/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def __init__(
headers=None,
cookies=None,
timeout=10,
on_baseline_ready=None,
):
self.parent_helper = parent_helper
self.baseline_url = baseline_url
Expand All @@ -33,6 +34,8 @@ def __init__(
self.headers = headers
self.cookies = cookies
self.timeout = 10
# Optional async callback fired once with baseline_1 after the baseline is established.
self.on_baseline_ready = on_baseline_ready

@staticmethod
def merge_dictionaries(headers1, headers2):
Expand Down Expand Up @@ -130,6 +133,12 @@ async def _baseline(self):
self.baseline_ignore_headers += [x.lower() for x in dynamic_headers]
self._baselined = True

if self.on_baseline_ready is not None:
try:
await self.on_baseline_ready(baseline_1)
except Exception as e:
log.debug(f"on_baseline_ready callback raised: {e}")

def gen_cache_buster(self):
return {self.parent_helper.rand_string(6): "1"}

Expand Down
2 changes: 2 additions & 0 deletions bbot/core/helpers/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def http_compare(
data=None,
json=None,
timeout=10,
on_baseline_ready=None,
):
return HttpCompare(
url,
Expand All @@ -169,6 +170,7 @@ def http_compare(
method=method,
data=data,
json=json,
on_baseline_ready=on_baseline_ready,
)

def temp_filename(self, extension=None):
Expand Down
6 changes: 6 additions & 0 deletions bbot/core/helpers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,12 @@ def get_waf_strings():
return [
"The requested URL was rejected",
"This content has been blocked",
"You don't have permission to access ",
"The URL you requested has been blocked",
"Request unsuccessful. Incapsula incident",
"Access Denied - Sucuri Website Firewall",
"Attention Required! | Cloudflare",
"Microsoft-Azure-Application-Gateway",
]
Comment thread
liquidsec marked this conversation as resolved.


Expand Down
17 changes: 15 additions & 2 deletions bbot/core/helpers/regexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,14 @@
re.DOTALL,
)
post_form_regex_noaction = re.compile(
r"<form[^>]*(?:\baction=[\"']?([^\s\"'<>]+)[\"']?)?[^>]*\bmethod=[\"']?[pP][oO][sS][tT][\"']?[^>]*>([\s\S]*?)<\/form>",
# Negative lookahead rejects forms that already carry action= (those are
# handled by post_form_regex / post_form_regex2 with the real action URL).
# Without it, the action group's `?` makes this regex over-eagerly match
# forms with action, capturing '' and emitting a phantom WEB_PARAMETER
# whose url falls back to event.url — racing the legitimate emission.
# The empty `()` keeps findall's tuple shape (form_action, form_content)
# consistent with the other form regexes consumed by GetForm.extract().
r"<form(?![^>]*\baction=)()[^>]*\bmethod=[\"']?[pP][oO][sS][tT][\"']?[^>]*>([\s\S]*?)<\/form>",
re.DOTALL,
)
generic_form_regex = re.compile(
Expand All @@ -166,9 +173,15 @@
)

select_tag_regex = re.compile(
r"<select[^>]+?name=[\"\']?([_\-\.\w]+)[\"\']?[^>]*>(?:\s*<option[^>]*?value=[\"\']?([_\.\-\w]*)[\"\']?[^>]*>)?",
r"<select[^>]+?\bname=[\"\']?([_\-\.\w]+)[\"\']?[^>]*>((?:\s*<option\b[^>]*>(?:[^<]*(?:</option>)?)?\s*)*)",
re.IGNORECASE | re.DOTALL,
)
option_tag_regex = re.compile(r"<option\b([^>]*)>", re.IGNORECASE)
option_value_regex = re.compile(
r"""\bvalue\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>'"]+))""",
re.IGNORECASE,
)
option_selected_regex = re.compile(r"(?<![\-\w])selected(?![\-\w])", re.IGNORECASE)

textarea_tag_regex = re.compile(
r"<textarea[^>]*?\sname=[\"\']?([\-\._=+\/\w]+)[\"\']?[^>]*?\svalue=[\"\']?([:%\-\._=+\/\w]*)[\"\']?[^>]*?>"
Expand Down
87 changes: 87 additions & 0 deletions bbot/core/helpers/web/response_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Build HTTP_RESPONSE event data dicts from blasthttp.Response objects.

Materializes hash and cert_info eagerly, so only call when about to emit an
HTTP_RESPONSE event (otherwise blasthttp.Response stays lazy).
"""

import re
from urllib.parse import urlparse


_TITLE_REGEX = re.compile(r"<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL)


def response_to_event_dict(response, url_input, method="GET"):
"""Convert a blasthttp.Response into the dict format HTTP_RESPONSE events expect.

Args:
response: a blasthttp.Response (native 0.5+ object with .hash, .cert_info)
url_input: the original scan-target identifier (host:port or full URL)
method: HTTP method used to fetch the response

Returns:
dict with keys url, input, status_code, method, path, host, raw_header,
header, content_type, content_length, title, body, location, hash. Adds
cert_info when the response carried a TLS certificate.
"""
parsed = urlparse(response.url)
path = parsed.path or "/"

status_line = f"HTTP/1.1 {response.status} \r\n"
raw_header = f"{status_line}{response.raw_headers}\r\n\r\n"

header_dict = {}
for k, v in response.headers.items():
key = k.lower().replace("-", "_")
if key in header_dict:
header_dict[key] += f", {v}"
else:
header_dict[key] = v

content_type = header_dict.get("content_type", "")
content_length = int(header_dict.get("content_length", len(response.body_bytes)))
location = header_dict.get("location", "")

body = response.body
title = ""
title_match = _TITLE_REGEX.search(body)
if title_match:
title = title_match.group(1).strip()

j = {
"url": response.url,
"input": url_input,
"status_code": response.status,
"method": method,
"path": path,
"host": parsed.hostname or "",
"raw_header": raw_header,
"header": header_dict,
"content_type": content_type,
"content_length": content_length,
"title": title,
"body": body,
"location": location,
"hash": {
"body_md5": response.hash.body_md5,
"body_mmh3": response.hash.body_mmh3,
"body_sha256": response.hash.body_sha256,
"header_md5": response.hash.header_md5,
"header_mmh3": response.hash.header_mmh3,
"header_sha256": response.hash.header_sha256,
},
}

ci = response.cert_info
if ci is not None:
j["cert_info"] = {
"common_name": ci.common_name,
"sans": ci.sans,
"emails": ci.emails,
"issuer": ci.issuer,
"not_before": ci.not_before,
"not_after": ci.not_after,
"fingerprint_sha256": ci.fingerprint_sha256,
}

return j
15 changes: 15 additions & 0 deletions bbot/defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,23 @@ parameter_blacklist:
- cf_clearance
- _abck
- bm_sz
- bm_sv
- ak_bmsc
- f5_cspm
# CSRF / Anti-Forgery tokens
- authenticity_token
- csrfmiddlewaretoken
- __RequestVerificationToken
- antiforgerytoken
- __csrf_magic
- _wpnonce
# ASP.NET session/identity cookies
- .ASPXANONYMOUS
- .ASPXAUTH
# PKCE (Proof Key for Code Exchange)
- code_verifier
- code_challenge
# Analytics
- _ga
- _gid
- _gat
Expand Down
6 changes: 3 additions & 3 deletions bbot/modules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,13 +868,13 @@ def _event_precheck(self, event):
if self.target_only and "target" not in event.tags:
return False, "it did not meet target_only filter criteria"

# limit js URLs to modules that opt in to receive them
if (not self.accept_url_special) and event.type.startswith("URL"):
# limit events with special URL extensions (e.g. .js) to modules that opt in
if not self.accept_url_special:
extension = getattr(event, "url_extension", "")
if extension in self.scan.url_extension_special:
return (
False,
f"it is a special URL (extension {extension}) but the module does not opt in to receive special URLs",
f"it has a special URL extension ({extension}) but the module does not opt in to receive special URLs",
)

return True, "precheck succeeded"
Expand Down
13 changes: 9 additions & 4 deletions bbot/modules/bypass403.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from bbot.errors import HttpCompareError
from bbot.modules.base import BaseModule
from bbot.core.helpers.misc import get_waf_strings

"""
Port of https://github.com/iamj0ker/bypass-403/ and https://portswigger.net/bappstore/444407b96d9c4de0adb7aed89e826122
Expand Down Expand Up @@ -80,6 +81,10 @@ class bypass403(BaseModule):
meta = {"description": "Check 403 pages for common bypasses", "created_date": "2022-07-05", "author": "@liquidsec"}
in_scope_only = True

async def setup(self):
self.waf_yara_rules = self.helpers.yara.compile_strings(get_waf_strings(), nocase=True)
return True

async def do_checks(self, compare_helper, event, collapse_threshold):
results = set()
error_count = 0
Expand All @@ -105,10 +110,10 @@ async def do_checks(self, compare_helper, event, collapse_threshold):

# In some cases WAFs will respond with a 200 code which causes a false positive
if subject_response is not None:
for waf_string in self.helpers.get_waf_strings():
if waf_string in subject_response.text:
self.debug("Rejecting result based on presence of WAF string")
return
waf_matches = await self.helpers.yara.match(self.waf_yara_rules, subject_response.text)
if waf_matches:
self.debug("Rejecting result based on presence of WAF string")
return

if match is False:
if str(subject_response.status_code)[0] != "4":
Expand Down
72 changes: 2 additions & 70 deletions bbot/modules/http.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import re
from http.cookies import SimpleCookie
from urllib.parse import urlparse

import blasthttp

from bbot.core.helpers.web.web import iter_batch_results
from bbot.core.helpers.web.response_event import response_to_event_dict
from bbot.modules.base import BaseModule
from bbot.core.config.models import BaseModuleConfig, Field

Expand Down Expand Up @@ -100,75 +100,7 @@ def _build_headers(self):

def _response_to_json(self, url_input, response):
"""Convert a blasthttp Response to a dict for HTTP_RESPONSE events."""
parsed = urlparse(response.url)
path = parsed.path or "/"

# Build raw_header string (required by HTTP_RESPONSE validation).
# blasthttp already builds the canonical "Name: Value\r\n..." form
# — reuse it instead of rebuilding.
status_line = f"HTTP/1.1 {response.status} \r\n"
raw_header = f"{status_line}{response.raw_headers}\r\n\r\n"

# Build header dict (lowercase keys, comma-joined for dupes)
header_dict = {}
for k, v in response.headers.items():
key = k.lower().replace("-", "_")
if key in header_dict:
header_dict[key] += f", {v}"
else:
header_dict[key] = v

content_type = header_dict.get("content_type", "")
content_length = int(header_dict.get("content_length", len(response.body_bytes)))

# Location header for redirects (excavate uses event.redirect_location)
location = header_dict.get("location", "")

# Extract title from HTML
title = ""
body = response.body
title_match = re.search(r"<title[^>]*>(.*?)</title>", body, re.IGNORECASE | re.DOTALL)
if title_match:
title = title_match.group(1).strip()

j = {
"url": response.url,
"input": url_input,
"status_code": response.status,
"method": "GET",
"path": path,
"host": parsed.hostname or "",
"raw_header": raw_header,
"header": header_dict,
"content_type": content_type,
"content_length": content_length,
"title": title,
"body": body,
"location": location,
"hash": {
"body_md5": response.hash.body_md5,
"body_mmh3": response.hash.body_mmh3,
"body_sha256": response.hash.body_sha256,
"header_md5": response.hash.header_md5,
"header_mmh3": response.hash.header_mmh3,
"header_sha256": response.hash.header_sha256,
},
}

# Include TLS certificate info when available (HTTPS responses)
ci = response.cert_info
if ci is not None:
j["cert_info"] = {
"common_name": ci.common_name,
"sans": ci.sans,
"emails": ci.emails,
"issuer": ci.issuer,
"not_before": ci.not_before,
"not_after": ci.not_after,
"fingerprint_sha256": ci.fingerprint_sha256,
}

return j
return response_to_event_dict(response, url_input, method="GET")

async def _process_result(self, result, parent_event):
"""Emit URL + HTTP_RESPONSE events for one batch result. Returns True if status was usable."""
Expand Down
Loading
Loading