Skip to content

Commit

Permalink
Merge pull request #483 from blacklanternsecurity/dev
Browse files Browse the repository at this point in the history
Dev - > Main
  • Loading branch information
liquidsec authored Aug 28, 2024
2 parents ea3d56f + d866c19 commit 29cd03c
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 31 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ Check out the [introductory blog](https://blog.blacklanternsecurity.com/p/introd

We have a [pypi](https://pypi.org/project/baddns/) package, so you can just do `pip install baddns` to make use of the library.

Or use pipx: `pipx install git+https://github.com/blacklanternsecurity/baddns`

## Usage

After installing with pip, you can just run `baddns` from the command line.

```
baddns [-h] [-n CUSTOM_NAMESERVERS] [-c CUSTOM_SIGNATURES] [-l] [-m MODULES] [-d] [target]
usage: baddns [-h] [-n CUSTOM_NAMESERVERS] [-c CUSTOM_SIGNATURES] [-l] [-s] [-m MODULES] [-d] [target]
Check subdomains for subdomain takeovers and other DNS tomfoolery
positional arguments:
target subdomain to analyze
Expand All @@ -34,6 +38,7 @@ options:
-c CUSTOM_SIGNATURES, --custom-signatures CUSTOM_SIGNATURES
Use an alternate directory for loading signatures
-l, --list-modules List available modules and their descriptions.
-s, --silent Show only vulnerable targets
-m MODULES, --modules MODULES
Comma separated list of module names to use. Ex: module1,module2,module3
-d, --debug Enable debug logging
Expand Down Expand Up @@ -75,4 +80,4 @@ Please visit our full [documentation](https://www.blacklanternsecurity.com/baddn

### Acknowledgements

BadDNS Signatures are sourced primarily from [Nuclei Templates](https://github.com/projectdiscovery/nuclei-templates) and from [dnsReaper](https://github.com/punk-security/dnsReaper) by [Punk Security](https://punksecurity.co.uk/), although many have been modified or updated in BadDNS. Much of the research contained in the signatures was originally discussed on the issues page of [can-i-take-over-xyz](https://github.com/EdOverflow/can-i-take-over-xyz).
BadDNS Signatures are sourced primarily from [Nuclei Templates](https://github.com/projectdiscovery/nuclei-templates) and from [dnsReaper](https://github.com/punk-security/dnsReaper) by [Punk Security](https://punksecurity.co.uk/), although many have been modified or updated in BadDNS. Much of the research contained in the signatures was originally discussed on the issues page of [can-i-take-over-xyz](https://github.com/EdOverflow/can-i-take-over-xyz).
26 changes: 18 additions & 8 deletions baddns/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def validate_modules(arg_value, pattern=re.compile(r"^[a-zA-Z0-9_]+(,[a-zA-Z0-9_
return arg_value


async def execute_module(ModuleClass, target, custom_nameservers, signatures):
async def execute_module(ModuleClass, target, custom_nameservers, signatures, silent=False):
findings = None
try:
module_instance = ModuleClass(target, custom_nameservers=custom_nameservers, signatures=signatures, cli=True)
Expand All @@ -84,9 +84,11 @@ async def execute_module(ModuleClass, target, custom_nameservers, signatures):
if await module_instance.dispatch():
findings = module_instance.analyze()
if findings:
print(f"{Fore.GREEN}{'Vulnerable!'}{Style.RESET_ALL}")
if not silent:
print(f"{Fore.GREEN}{'Vulnerable!'}{Style.RESET_ALL}")
for finding in findings:
print(finding.to_dict())

return findings


Expand All @@ -96,8 +98,6 @@ async def _main():
log = logging.getLogger("baddns")

parser = CustomArgumentParser(description="Check subdomains for subdomain takeovers and other DNS tomfoolery")
print(f"{Fore.GREEN}{ascii_art_banner}{Style.RESET_ALL}")
print_version()

parser.add_argument(
"-n",
Expand All @@ -116,6 +116,8 @@ async def _main():
"-l", "--list-modules", action="store_true", help="List available modules and their descriptions."
)

parser.add_argument("-s", "--silent", action="store_true", help="Only show results, no other output (JSON format)")

parser.add_argument(
"-m",
"--modules",
Expand All @@ -128,6 +130,14 @@ async def _main():
parser.add_argument("target", nargs="?", type=validate_target, help="subdomain to analyze")
args = parser.parse_args()

silent = bool(args.silent)

if silent:
log.setLevel(logging.ERROR)
else:
print(f"{Fore.GREEN}{ascii_art_banner}{Style.RESET_ALL}")
print_version()

if not args.target and not args.list_modules:
parser.error("the following arguments are required: target")

Expand Down Expand Up @@ -167,7 +177,7 @@ async def _main():
signatures = load_signatures(signatures_dir=custom_signatures)

for ModuleClass in modules_to_execute:
await execute_module(ModuleClass, args.target, custom_nameservers, signatures)
await execute_module(ModuleClass, args.target, custom_nameservers, signatures, silent=silent)


def main():
Expand All @@ -185,9 +195,9 @@ def main():

ascii_art_banner = """
__ ) | |
__ \ _` | _` | _` | __ \ __|
| | ( | ( | ( | | | \__ \
____/ \__,_| \__,_| \__,_| _| _| ____/
__ \\ _` | _` | _` | __ \\ __|
| | ( | ( | ( | | | \\__ \\
____/ \\__,_| \\__,_| \\__,_| _| _| ____/
"""

Expand Down
14 changes: 10 additions & 4 deletions baddns/lib/dnswalk.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ class DnsWalk:
]

max_depth = 10
raw_query_max_retries = 6

def __init__(self, dns_manager):
def __init__(self, dns_manager, raw_query_max_retries=6, raw_query_timeout=6.0, raw_query_retry_wait=3):
self.dns_manager = dns_manager
self.raw_query_max_retries = raw_query_max_retries
self.raw_query_timeout = raw_query_timeout
self.raw_query_retry_wait = raw_query_retry_wait

async def a_resolve(self, nameserver):
nameserver_ips = set()
Expand All @@ -56,7 +58,9 @@ async def raw_query_with_retry(self, target, nameserver_ip):
retries = 0
while retries < self.raw_query_max_retries:
try:
response_msg, used_tcp = await dns.asyncquery.udp_with_fallback(target, nameserver_ip, timeout=6.0)
response_msg, used_tcp = await dns.asyncquery.udp_with_fallback(
target, nameserver_ip, timeout=self.raw_query_timeout
)
log.debug("raw_query_with_retry: Had to fall back to TCP")
return response_msg, used_tcp
except (dns.exception.Timeout, dns.message.Truncated) as e:
Expand All @@ -65,7 +69,9 @@ async def raw_query_with_retry(self, target, nameserver_ip):
except Exception as e:
log.debug(f"raw_query_with_retry: An unexpected error occurred: {e}. Aborting.")
retries += 1
await asyncio.sleep(12) # Wait before retrying. We don't want to piss off the root DNS servers.
await asyncio.sleep(
self.raw_query_retry_wait
) # Wait before retrying. We don't want to piss off the root DNS servers.
log.debug("raw_query_with_retry: Max retries reached. Query failed.")
return None, None

Expand Down
10 changes: 9 additions & 1 deletion baddns/modules/ns.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class BadDNS_ns(BadDNS_base):

def __init__(self, target, **kwargs):
super().__init__(target, **kwargs)

self._dnswalk_kwargs = kwargs

self.target_dnsmanager = DNSManager(
target, dns_client=self.dns_client, custom_nameservers=self.custom_nameservers
)
Expand All @@ -33,7 +36,12 @@ async def dispatch(self):

await self.target_dnsmanager.dispatchDNS(omit_types=["CNAME", "NS"])

dnswalk = DnsWalk(self.target_dnsmanager)
dnswalk = DnsWalk(
self.target_dnsmanager,
raw_query_max_retries=self._dnswalk_kwargs.get("raw_query_max_retries", 6),
raw_query_timeout=self._dnswalk_kwargs.get("raw_query_timeout", 6.0),
raw_query_retry_wait=self._dnswalk_kwargs.get("raw_query_retry_wait", 3),
)
self.target_dnsmanager.answers["NS"] = await dnswalk.ns_trace(self.target)
return True

Expand Down
2 changes: 1 addition & 1 deletion baddns/modules/zonetransfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def dispatch(self):
r = await self.zone_transfer(ns, self.target_dnsmanager.target)
if r:
zone_transfer_detected = True
log.info(
self.infomsg(
f"Successful Zone Transfer against NS [{ns}] for target [{self.target_dnsmanager.target}]"
)
return zone_transfer_detected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ identifiers:
not_cnames: []
matcher_rule:
matchers:
- dsl:
- Host != ip
type: dsl
- condition: and
part: body
type: word
words:
- <strong>Trying to access your account?</strong>
- or <a href="mailto:help@createsend.com
- Email Newsletter Software
- css.createsend1.com
matchers-condition: and
mode: http
service_name: CampaignMonitor Takeover Detection
Expand Down
3 changes: 2 additions & 1 deletion baddns/signatures/nucleitemplates_ghost-takeover.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ matcher_rule:
- dsl:
- Host != ip
type: dsl
- condition: and
- condition: or
part: header
type: word
words:
- error.ghost.org
- offline.ghost.org
- status: 302
type: status
Expand Down
2 changes: 2 additions & 0 deletions baddns/signatures/signature_history.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,5 @@ e09c908bd6994a66863281b5fd4d75029e249ba7032cd7073b69ddab769f8eb1 #nucleitemplate
22b21533c10e5859223004c0d65bc722b393e1d752c9fa7b27dc04ba315999df #nucleitemplates_uptime-takeover.yml
e3a452987e0aab4f6c387dd89dc1a042590a14555274a564d5d6e5f9dfc691c9 #nucleitemplates_squadcast-takeover.yml
2fef72d2f015bade20486e291d1ccf003cc8ce827ff2bbd14eb2e0e73e005116 #nucleitemplates_flexbe-takeover.yml
a2a6a8fd35e65d7c92882ba0e751b0496b5780937cebf9a9610fa279181060a2 #nucleitemplates_ghost-takeover.yml
e155aed36a19a0437650f5d1033a64a47f39a8981de9f1b5f39e2dfe7e14996d #nucleitemplates_campaignmonitor-takeover.yml
8 changes: 6 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ def dnswalk_harness(request, monkeypatch):
mock_data = getattr(request, "param", {})

def init_wrapper(mock_data):
def mock_init(self):
pass
def mock_init(self, dns_manager, *args, **kwargs): # dns_manager as positional, others as kwargs
# Manually set the attributes that are normally initialized
self.dns_manager = dns_manager
self.raw_query_max_retries = kwargs.get("raw_query_max_retries", 6)
self.raw_query_timeout = kwargs.get("raw_query_timeout", 6.0)
self.raw_query_retry_wait = kwargs.get("raw_query_retry_wait", 3)

return mock_init

Expand Down
18 changes: 11 additions & 7 deletions tests/dnswalk_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import pytest

from unittest.mock import Mock
from baddns.lib.dnswalk import DnsWalk

# Test for normal typical normal behavior

# Mock DNS Manager
mock_dns_manager = Mock()

mock_data = {
"ns_records": [
{"127.0.0.1": {"answer": [], "authority": ["tld.dns.nameserver"]}},
Expand All @@ -18,7 +22,7 @@
@pytest.mark.asyncio
@pytest.mark.parametrize("dnswalk_harness", [mock_data], indirect=True)
async def test_dnswalk_normal(dnswalk_harness):
dnswalk = DnsWalk()
dnswalk = DnsWalk(mock_dns_manager, raw_query_max_retries=3, raw_query_timeout=5.0, raw_query_retry_wait=1)
ns_trace_results = await dnswalk.ns_trace("bad.dns")
assert sorted(ns_trace_results) == ["ns1.bad.dns", "ns2.bad.dns"]

Expand All @@ -33,7 +37,7 @@ async def test_dnswalk_normal(dnswalk_harness):
@pytest.mark.asyncio
@pytest.mark.parametrize("dnswalk_harness", [mock_data], indirect=True)
async def test_dnswalk_nameserversdontresolve(dnswalk_harness):
dnswalk = DnsWalk()
dnswalk = DnsWalk(mock_dns_manager, raw_query_max_retries=3, raw_query_timeout=5.0, raw_query_retry_wait=1)
ns_trace_results = await dnswalk.ns_trace("bad.dns")
print(ns_trace_results)
assert sorted(ns_trace_results) == ["ns1.noresolve.dns", "ns2.noresolve.dns"]
Expand All @@ -53,7 +57,7 @@ async def test_dnswalk_nameserversdontresolve(dnswalk_harness):
@pytest.mark.asyncio
@pytest.mark.parametrize("dnswalk_harness", [mock_data], indirect=True)
async def test_dnswalk_resolvewithnoanswers(dnswalk_harness):
dnswalk = DnsWalk()
dnswalk = DnsWalk(mock_dns_manager, raw_query_max_retries=3, raw_query_timeout=5.0, raw_query_retry_wait=1)
ns_trace_results = await dnswalk.ns_trace("bad.dns")
print(ns_trace_results)
assert sorted(ns_trace_results) == ["ns1.noresolve.dns", "ns2.noresolve.dns"]
Expand All @@ -79,7 +83,7 @@ async def test_dnswalk_resolvewithnoanswers(dnswalk_harness):
@pytest.mark.asyncio
@pytest.mark.parametrize("dnswalk_harness", [mock_data], indirect=True)
async def test_dnswalk_answeratendofnschain(dnswalk_harness):
dnswalk = DnsWalk()
dnswalk = DnsWalk(mock_dns_manager, raw_query_max_retries=3, raw_query_timeout=5.0, raw_query_retry_wait=1)
ns_trace_results = await dnswalk.ns_trace("bad.dns")
print(ns_trace_results)
assert sorted(ns_trace_results) == ["ns1.bad.dns", "ns2.bad.dns"]
Expand All @@ -99,7 +103,7 @@ async def test_dnswalk_answeratendofnschain(dnswalk_harness):
@pytest.mark.asyncio
@pytest.mark.parametrize("dnswalk_harness", [mock_data], indirect=True)
async def test_dnswalk_badauthority(dnswalk_harness):
dnswalk = DnsWalk()
dnswalk = DnsWalk(mock_dns_manager, raw_query_max_retries=3, raw_query_timeout=5.0, raw_query_retry_wait=1)
ns_trace_results = await dnswalk.ns_trace("sub.bad.dns")
print(ns_trace_results)
assert ns_trace_results
Expand Down Expand Up @@ -133,7 +137,7 @@ async def test_dnswalk_badauthority(dnswalk_harness):
@pytest.mark.asyncio
@pytest.mark.parametrize("dnswalk_harness", [mock_data], indirect=True)
async def test_dnswalk_subdomainnons(dnswalk_harness):
dnswalk = DnsWalk()
dnswalk = DnsWalk(mock_dns_manager, raw_query_max_retries=3, raw_query_timeout=5.0, raw_query_retry_wait=1)
ns_trace_results = await dnswalk.ns_trace("sub.bad.dns")
print(ns_trace_results)
assert sorted(ns_trace_results) == []
Expand Down Expand Up @@ -165,7 +169,7 @@ async def test_dnswalk_subdomainnons(dnswalk_harness):
@pytest.mark.asyncio
@pytest.mark.parametrize("dnswalk_harness", [mock_data], indirect=True)
async def test_dnswalk_subdomainfindns(dnswalk_harness):
dnswalk = DnsWalk()
dnswalk = DnsWalk(mock_dns_manager, raw_query_max_retries=3, raw_query_timeout=5.0, raw_query_retry_wait=1)
ns_trace_results = await dnswalk.ns_trace("sub.bad.dns")
print(ns_trace_results)
assert ns_trace_results
Expand Down

0 comments on commit 29cd03c

Please sign in to comment.