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

Support local files/folders and mobile apps as targets #2093

Merged
merged 4 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ Targets can be any of the following:
- `IP_RANGE` (`1.2.3.0/24`)
- `OPEN_TCP_PORT` (`192.168.0.1:80`)
- `URL` (`https://www.evilcorp.com`)
- `EMAIL_ADDRESS` (`[email protected]`)
- `ORG_STUB` (`ORG:evilcorp`)
- `USER_STUB` (`USER:bobsmith`)
- `FILESYSTEM` (`FILESYSTEM:/tmp/asdf`)
- `MOBILE_APP` (`MOBILE_APP:https://play.google.com/store/apps/details?id=com.evilcorp.app`)

For more information, see [Targets](https://www.blacklanternsecurity.com/bbot/Stable/scanning/#targets-t). To learn how BBOT handles scope, see [Scope](https://www.blacklanternsecurity.com/bbot/Stable/scanning/#scope).

Expand Down
23 changes: 22 additions & 1 deletion bbot/core/event/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from typing import Optional
from contextlib import suppress
from radixtarget import RadixTarget
from urllib.parse import urljoin, parse_qs
from pydantic import BaseModel, field_validator
from urllib.parse import urlparse, urljoin, parse_qs


from .helpers import *
Expand Down Expand Up @@ -1584,6 +1584,27 @@ class RAW_DNS_RECORD(DictHostEvent, DnsEvent):
class MOBILE_APP(DictEvent):
_always_emit = True

def _sanitize_data(self, data):
if isinstance(data, str):
data = {"url": data}
if "url" not in data:
raise ValidationError("url is required for MOBILE_APP events")
url = data["url"]
# parse URL
try:
self.parsed_url = urlparse(url)
except Exception as e:
raise ValidationError(f"Error parsing URL {url}: {e}")
if not "id" in data:
# extract "id" getparam
params = parse_qs(self.parsed_url.query)
try:
_id = params["id"][0]
except Exception:
raise ValidationError("id is required for MOBILE_APP events")
data["id"] = _id
return data

def _pretty_string(self):
return self.data["url"]

Expand Down
16 changes: 15 additions & 1 deletion bbot/scanner/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ def add(self, targets):
else:
event = self.make_event(target)
if event:
self.inputs.add(target)
_events = [event]
for event in _events:
self.inputs.add(event.data)
events.add(event)

# sort by host size to ensure consistency
Expand Down Expand Up @@ -140,6 +140,20 @@ def handle_username(self, match):
return [username_event]
return []

@special_target_type(r"^(?:FILESYSTEM|FILE|FOLDER|DIR|PATH):(.*)")
def handle_filesystem(self, match):
filesystem_event = self.make_event({"path": match.group(1)}, event_type="FILESYSTEM")
if filesystem_event:
return [filesystem_event]
return []

@special_target_type(r"^(?:MOBILE_APP|APK|IPA|APP):(.*)")
def handle_mobile_app(self, match):
mobile_app_event = self.make_event({"url": match.group(1)}, event_type="MOBILE_APP")
if mobile_app_event:
return [mobile_app_event]
return []

def get(self, event, single=True, **kwargs):
results = super().get(event, **kwargs)
if results and single:
Expand Down
39 changes: 39 additions & 0 deletions bbot/test/test_step_1/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,45 @@ def test_event_magic():
zip_file.unlink()


@pytest.mark.asyncio
async def test_mobile_app():
scan = Scanner()
with pytest.raises(ValidationError):
scan.make_event("com.evilcorp.app", "MOBILE_APP", parent=scan.root_event)
with pytest.raises(ValidationError):
scan.make_event({"id": "com.evilcorp.app"}, "MOBILE_APP", parent=scan.root_event)
with pytest.raises(ValidationError):
scan.make_event({"url": "https://play.google.com/store/apps/details"}, "MOBILE_APP", parent=scan.root_event)
mobile_app = scan.make_event(
{"url": "https://play.google.com/store/apps/details?id=com.evilcorp.app"}, "MOBILE_APP", parent=scan.root_event
)
assert sorted(mobile_app.data.items()) == [
("id", "com.evilcorp.app"),
("url", "https://play.google.com/store/apps/details?id=com.evilcorp.app"),
]

scan = Scanner("MOBILE_APP:https://play.google.com/store/apps/details?id=com.evilcorp.app")
events = [e async for e in scan.async_start()]
assert len(events) == 3
mobile_app_event = [e for e in events if e.type == "MOBILE_APP"][0]
assert mobile_app_event.type == "MOBILE_APP"
assert sorted(mobile_app_event.data.items()) == [
("id", "com.evilcorp.app"),
("url", "https://play.google.com/store/apps/details?id=com.evilcorp.app"),
]


@pytest.mark.asyncio
async def test_filesystem():
scan = Scanner("FILESYSTEM:/tmp/asdf")
events = [e async for e in scan.async_start()]
assert len(events) == 3
filesystem_events = [e for e in events if e.type == "FILESYSTEM"]
assert len(filesystem_events) == 1
assert filesystem_events[0].type == "FILESYSTEM"
assert filesystem_events[0].data == {"path": "/tmp/asdf"}


def test_event_hashing():
scan = Scanner("example.com")
url_event = scan.make_event("https://api.example.com/", "URL_UNVERIFIED", parent=scan.root_event)
Expand Down
4 changes: 2 additions & 2 deletions bbot/test/test_step_1/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,13 @@ def test_preset_scope():
}
assert preset_whitelist_baked.to_dict(include_target=True) == {
"target": ["evilcorp.org"],
"whitelist": ["1.2.3.0/24", "http://evilcorp.net/"],
"whitelist": ["1.2.3.4/24", "http://evilcorp.net"],
"blacklist": ["[email protected]", "evilcorp.co.uk:443"],
"config": {"modules": {"secretsdb": {"api_key": "deadbeef", "otherthing": "asdf"}}},
}
assert preset_whitelist_baked.to_dict(include_target=True, redact_secrets=True) == {
"target": ["evilcorp.org"],
"whitelist": ["1.2.3.0/24", "http://evilcorp.net/"],
"whitelist": ["1.2.3.4/24", "http://evilcorp.net"],
"blacklist": ["[email protected]", "evilcorp.co.uk:443"],
"config": {"modules": {"secretsdb": {"otherthing": "asdf"}}},
}
Expand Down
5 changes: 5 additions & 0 deletions docs/scanning/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ Targets declare what's in-scope, and seed a scan with initial data. BBOT accepts
- `IP_RANGE` (`1.2.3.0/24`)
- `OPEN_TCP_PORT` (`192.168.0.1:80`)
- `URL` (`https://www.evilcorp.com`)
- `EMAIL_ADDRESS` (`[email protected]`)
- `ORG_STUB` (`ORG:evilcorp`)
- `USER_STUB` (`USER:bobsmith`)
- `FILESYSTEM` (`FILESYSTEM:/tmp/asdf`)
- `MOBILE_APP` (`MOBILE_APP:https://play.google.com/store/apps/details?id=com.evilcorp.app`)

Note that BBOT only discriminates down to the host level. This means, for example, if you specify a URL `https://www.evilcorp.com` as the target, the scan will be *seeded* with that URL, but the scope of the scan will be the entire host, `www.evilcorp.com`. Other ports/URLs on that same host may also be scanned.

Expand Down
Loading