Skip to content

Commit db3e012

Browse files
authored
Merge pull request #3000 from corest/feature/exclude-proxy-config
Add option to exclude ip/hostname from proxy
2 parents b99f5c6 + 770a1eb commit db3e012

17 files changed

Lines changed: 1169 additions & 1039 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ bbot -t evilcorp.com -p spider
9191
description: Recursive web spider
9292

9393
modules:
94-
- httpx
94+
- http
9595

9696
blacklist:
9797
# Prevent spider from invalidating sessions by logging out
@@ -223,6 +223,13 @@ include:
223223
- web-screenshots
224224
- baddns-heavy
225225

226+
config:
227+
modules:
228+
dnsbrute:
229+
recursive_mutations: true
230+
dnscommonsrv:
231+
recursive_mutations: true
232+
226233
```
227234

228235
</details>

bbot/core/helpers/web/web.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __init__(self, parent_helper):
7676
self._http_timeout = self.web_config.get("http_timeout", 20)
7777
self._http_retries = self.web_config.get("http_retries", 1)
7878
self._http_proxy = self.web_config.get("http_proxy", None)
79+
self._http_proxy_exclude = self.web_config.get("http_proxy_exclude", []) or []
7980
ua = self.web_config.get("user_agent", "BBOT")
8081
ua_suffix = self.web_config.get("user_agent_suffix") or ""
8182
self._user_agent = f"{ua} {ua_suffix}".strip()
@@ -116,6 +117,7 @@ def _build_blasthttp_kwargs(self, url, **kwargs):
116117
follow_redirects = kwargs.pop("follow_redirects", None)
117118
max_redirects = kwargs.pop("max_redirects", None)
118119
proxy = kwargs.pop("proxy", self._http_proxy)
120+
no_proxy = kwargs.pop("no_proxy", self._http_proxy_exclude)
119121
retries = kwargs.pop("retries", self._http_retries)
120122
params = kwargs.pop("params", None)
121123
cookies = kwargs.pop("cookies", None)
@@ -211,6 +213,11 @@ def _build_blasthttp_kwargs(self, url, **kwargs):
211213
blast_kwargs["max_redirects"] = int(max_redirects)
212214
if proxy:
213215
blast_kwargs["proxy"] = proxy
216+
# no_proxy lists hosts that bypass the proxy; it only has an effect
217+
# alongside a proxy (blasthttp errors if it's set without one), so
218+
# only forward it when a proxy is actually in play.
219+
if no_proxy:
220+
blast_kwargs["no_proxy"] = list(no_proxy)
214221
if max_body_size is not None:
215222
blast_kwargs["max_body_size"] = int(max_body_size)
216223
if request_target is not None:

bbot/defaults.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ dns:
9292
web:
9393
# HTTP proxy
9494
http_proxy:
95+
# Hosts/CIDRs to exclude from HTTP proxy (NO_PROXY equivalent)
96+
# Examples: ["localhost", "*.internal.corp", "10.0.0.0/8", "elastic.mycompany.com"]
97+
http_proxy_exclude: []
9598
# Web user-agent
9699
user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.2151.97
97100
# Suffix to append to user-agent (e.g. for tracking or identification)

bbot/scanner/preset/args.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ def preset_from_args(self):
180180
if self.parsed.proxy:
181181
args_preset.core.merge_custom({"web": {"http_proxy": self.parsed.proxy}})
182182

183+
if self.parsed.no_proxy:
184+
args_preset.core.merge_custom({"web": {"http_proxy_exclude": self.parsed.no_proxy}})
185+
183186
if self.parsed.custom_headers:
184187
args_preset.core.merge_custom({"web": {"http_headers": self.parsed.custom_headers}})
185188

@@ -376,6 +379,13 @@ def create_parser(self, *args, **kwargs):
376379
misc = p.add_argument_group(title="Misc")
377380
misc.add_argument("--version", action="store_true", help="show BBOT version and exit")
378381
misc.add_argument("--proxy", help="Use this proxy for all HTTP requests", metavar="HTTP_PROXY")
382+
misc.add_argument(
383+
"--no-proxy",
384+
nargs="+",
385+
default=[],
386+
help="Exclude these hosts from proxy (e.g. localhost *.internal.corp 10.0.0.0/8)",
387+
metavar="HOST",
388+
)
379389
misc.add_argument(
380390
"-H",
381391
"--custom-headers",

bbot/scanner/preset/environ.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ def prepare(self):
125125
environ.pop("HTTP_PROXY", None)
126126
environ.pop("HTTPS_PROXY", None)
127127

128+
# handle proxy exclusions (NO_PROXY)
129+
http_proxy_exclude = self.preset.config.get("web", {}).get("http_proxy_exclude", [])
130+
if http_proxy_exclude:
131+
environ["NO_PROXY"] = ",".join(str(x) for x in http_proxy_exclude)
132+
else:
133+
environ.pop("NO_PROXY", None)
134+
128135
# ssl verification
129136
import urllib3
130137

bbot/scanner/scanner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ def __init__(
237237
max_redirects = web_config.get("http_max_redirects", 5)
238238
self.web_max_redirects = max(max_redirects, self.web_spider_distance)
239239
self.http_proxy = web_config.get("http_proxy", "")
240+
self.http_proxy_exclude = web_config.get("http_proxy_exclude", [])
240241
self.http_timeout = web_config.get("http_timeout", 10)
241242
self.blasthttp_timeout = web_config.get("blasthttp_timeout", 5)
242243
self.http_retries = web_config.get("http_retries", 1)

bbot/test/test_step_1/test_web.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,70 @@ async def test_http_proxy(bbot_scanner, bbot_httpserver, proxy_server):
450450
await scan._cleanup()
451451

452452

453+
@pytest.mark.asyncio
454+
async def test_http_proxy_exclude(bbot_scanner, bbot_httpserver, proxy_server):
455+
"""Verify that requests to excluded hosts bypass the proxy."""
456+
endpoint = "/test_http_proxy_exclude"
457+
url = bbot_httpserver.url_for(endpoint)
458+
bbot_httpserver.expect_request(uri=endpoint).respond_with_data("proxy_exclude_works")
459+
460+
proxy_address = f"http://127.0.0.1:{proxy_server.server_address[1]}"
461+
# Exclude 127.0.0.1 from proxy
462+
scan = bbot_scanner(
463+
"127.0.0.1",
464+
config={
465+
"web": {
466+
"http_proxy": proxy_address,
467+
"http_proxy_exclude": ["127.0.0.1"],
468+
}
469+
},
470+
)
471+
472+
await scan._prep()
473+
474+
proxy_server.RequestHandlerClass.urls.clear()
475+
r = await scan.helpers.request(url)
476+
477+
# Request should NOT go through proxy
478+
assert len(proxy_server.RequestHandlerClass.urls) == 0, "Request should have bypassed proxy but went through it"
479+
assert r.status_code == 200 and r.text == "proxy_exclude_works"
480+
481+
await scan._cleanup()
482+
483+
484+
@pytest.mark.asyncio
485+
async def test_http_proxy_exclude_passthrough(bbot_scanner, bbot_httpserver, proxy_server):
486+
"""Verify that non-excluded hosts still go through the proxy."""
487+
endpoint = "/test_proxy_passthrough"
488+
url = bbot_httpserver.url_for(endpoint)
489+
bbot_httpserver.expect_request(uri=endpoint).respond_with_data("passthrough_works")
490+
491+
proxy_address = f"http://127.0.0.1:{proxy_server.server_address[1]}"
492+
# Exclude a different host, not the one we're requesting
493+
scan = bbot_scanner(
494+
"127.0.0.1",
495+
config={
496+
"web": {
497+
"http_proxy": proxy_address,
498+
"http_proxy_exclude": ["10.0.0.0/8"],
499+
}
500+
},
501+
)
502+
503+
await scan._prep()
504+
505+
proxy_server.RequestHandlerClass.urls.clear()
506+
r = await scan.helpers.request(url)
507+
508+
# Request SHOULD go through proxy (127.0.0.1 not in exclusion list)
509+
assert len(proxy_server.RequestHandlerClass.urls) == 1, (
510+
f"Request to {url} should have gone through proxy but didn't"
511+
)
512+
assert r.status_code == 200 and r.text == "passthrough_works"
513+
514+
await scan._cleanup()
515+
516+
453517
@pytest.mark.asyncio
454518
async def test_http_ssl(bbot_scanner, bbot_httpserver_ssl):
455519
endpoint = "/test_http_ssl"

0 commit comments

Comments
 (0)