diff --git a/ReadMe.md b/ReadMe.md index f629dc6..6e579fc 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,21 +1,34 @@ +
+ +
+ 
+███████╗██╗   ██╗██████╗ ██████╗  ██████╗ ███╗   ███╗██╗███╗   ██╗ █████╗ ████████╗ ██████╗ ██████╗
+██╔════╝██║   ██║██╔══██╗██╔══██╗██╔═══██╗████╗ ████║██║████╗  ██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
+███████╗██║   ██║██████╔╝██║  ██║██║   ██║██╔████╔██║██║██╔██╗ ██║███████║   ██║   ██║   ██║██████╔╝
+╚════██║██║   ██║██╔══██╗██║  ██║██║   ██║██║╚██╔╝██║██║██║╚██╗██║██╔══██║   ██║   ██║   ██║██╔══██╗
+███████║╚██████╔╝██████╔╝██████╔╝╚██████╔╝██║ ╚═╝ ██║██║██║ ╚████║██║  ██║   ██║   ╚██████╔╝██║  ██║
+╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝  ╚═════╝ ╚═╝     ╚═╝╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝
+
+ 
+ ![GitHub Actions CI](https://github.com/Stratus-Security/Subdominator/workflows/CI/badge.svg) ![GitHub all releases](https://img.shields.io/github/downloads/Stratus-Security/Subdominator/total) -# Subdominator 🚀 +
## Welcome to the Subdominator Club! -Meet **Subdominator**, your new favourite CLI tool for detecting subdomain takeovers. It's designed to be fast, accurate, and dependable, offering [a significant improvement over other available tools](https://www.stratussecurity.com/post/the-ultimate-subdomain-takeover-tool). +- Meet **Subdominator**, your new favourite CLI tool for detecting subdomain takeovers. It's designed to be fast, accurate, and dependable, offering [a significant improvement over other available tools](https://www.stratussecurity.com/post/the-ultimate-subdomain-takeover-tool). -🔍 Precision and speed are our goal. Subdominator delivers better results without the wait, see the benchmark and feature comparison below for details. +- Precision and speed are our goal. Subdominator delivers better results without the wait, see the benchmark and feature comparison below for details. -## Installing 🛠️ -To quickly, get up and running, you can download the latest release for [windows](https://github.com/Stratus-Security/Subdominator/releases/latest/download/Subdominator.exe) or [linux](https://github.com/Stratus-Security/Subdominator/releases/latest/download/Subdominator). +## Installing +- To quickly, get up and running, you can download the latest release for [windows](https://github.com/Stratus-Security/Subdominator/releases/latest/download/Subdominator.exe) or [linux](https://github.com/Stratus-Security/Subdominator/releases/latest/download/Subdominator). Alternatively, download it via CLI (remove .exe for linux version): ```bash wget https://github.com/Stratus-Security/Subdominator/releases/latest/download/Subdominator.exe ``` -## Quick Start 🚦 +## Quick Start To quickly check a list of domains, simply run: ```bash Subdominator -l subdomains.txt -o takeovers.txt @@ -25,7 +38,7 @@ Or to quickly check a single domain, run: Subdominator -d sub.example.com ``` -## Options 🎛️ +## Options ``` -d, --domain A single domain to check -l, --list A list of domains to check (line delimited) @@ -61,17 +74,17 @@ For example, this shows the same vulnerable domain and another non-vulnerable do [-] www.stratussecurity.com ``` -Finally, if a domain is vulnerable and passes validation with the --validation flag, it will be prepended with a ✅. +Finally, if a domain is vulnerable and passes validation with the --validation flag, it will be prepended with a . These domains have been validated to be vulnerable with the services directly, not just the fingerprint. For example: ``` -✅ [Microsoft Azure] example.stratussecurity.com - CNAME: stratus-cdn-stg.azureedge.net + [Microsoft Azure] example.stratussecurity.com - CNAME: stratus-cdn-stg.azureedge.net ``` ## Demo The tool running across 1000 passively gathered subdomains: ![Demo](https://raw.githubusercontent.com/Stratus-Security/Subdominator/master/Demo.gif) -## Benchmark 📊 +## Benchmark A benchmark was run across ~100,000 subdomains to compare performance with other popular tools | Tool | Threads | Time Taken | |--------------|---------|--------------------| @@ -79,7 +92,7 @@ A benchmark was run across ~100,000 subdomains to compare performance with other | Subjack | 50 | 2 hours, 30 minutes, 2 seconds | | Subdover | 50 | 2 hours, 33 minutes, 27 seconds | -## Key Features 🔥 +## Key Features - **Advanced DNS Matching**: Supports DNS matching for CNAME, A, and AAAA records. - **Recursive DNS Queries**: Performs in-depth queries to enhance accuracy and reduce false positives. - **Intelligent Domain Matching**: Uses a custom `public_suffix_list.dat` for more effective domain matching. @@ -89,7 +102,7 @@ A benchmark was run across ~100,000 subdomains to compare performance with other - **Comprehensive Detection**: Capable of identifying takeovers missed by other tools. - **Validation**: Dynamic takeover validation modules to check beyond fingerprints. -## Feature Comparison 🥊 +## Feature Comparison | Feature | Subdominator | Subjack | Subdover | |----------------------------------|--------------|---------|----------| | Advanced DNS Matching | ✅ | ❌ | ❌ | @@ -104,7 +117,7 @@ A benchmark was run across ~100,000 subdomains to compare performance with other | Fingerprints | 97 | 35 | 80 | ## Contributions -Got a suggestion, fingerprint, or want to chip in? We're all ears! Open a PR or issue – this will keep subdominator on top! 😄 +Got a suggestion, fingerprint, or want to chip in? We're all ears! Open a PR or issue – this will keep subdominator on top! ## Fingerprints The fingerprints and services are dynamically pulled from the [CanITakeOverXYZ repo](https://github.com/EdOverflow/can-i-take-over-xyz) as a source of truth. To fill in the gaps and correct incorrect fingerprints, this tool also has its own [custom fingerprints list](https://github.com/Stratus-Security/Subdominator/blob/master/Subdominator/custom_fingerprints.json) which is used in conjunction. @@ -209,4 +222,4 @@ Below is the current list of services supported, to ignore edge cases use the `- | Wufoo | Vulnerable | | Zendesk | Edge case | | Zoho Forms | Vulnerable | -| Zoho Forms India | Vulnerable | \ No newline at end of file +| Zoho Forms India | Vulnerable | diff --git a/Subdominator.Tests/FingerprintTests.cs b/Subdominator.Tests/FingerprintTests.cs index 136a3aa..256de3e 100644 --- a/Subdominator.Tests/FingerprintTests.cs +++ b/Subdominator.Tests/FingerprintTests.cs @@ -17,6 +17,7 @@ public async Task Setup() } [TestMethod] + public void ShouldNotHaveDuplicateServices() { Assert.IsTrue( @@ -79,4 +80,4 @@ public async Task ShouldExcludeEdgeCaseFingerprints() var filteredFingerprints = await hijack.GetFingerprintsAsync(true); Assert.IsFalse(filteredFingerprints.Any(f => f.Status.ToLower() == "edge case")); } -} \ No newline at end of file +} diff --git a/Subdominator.Tests/MatcherTests.cs b/Subdominator.Tests/MatcherTests.cs new file mode 100644 index 0000000..d99bfe0 --- /dev/null +++ b/Subdominator.Tests/MatcherTests.cs @@ -0,0 +1,20 @@ +namespace Subdominator.Tests; + +[TestClass] +public class MatcherTests +{ + [TestMethod] + public void FingerprintMatchingShouldBeCaseInsensitiveAndNormalizeDnsValues() + { + Assert.IsTrue(Subdominator.SubdomainHijack.MatchesFingerprintValue("Foo.Example.COM.", "example.com")); + Assert.IsTrue(Subdominator.SubdomainHijack.MatchesFingerprintValue("151.101.12.34", "151.101.")); + Assert.IsFalse(Subdominator.SubdomainHijack.MatchesFingerprintValue("foo.example.com", "bar.example.com")); + } + + [TestMethod] + public void FingerprintBodyMatchingShouldIgnoreCase() + { + Assert.IsTrue(Subdominator.SubdomainHijack.ContainsFingerprintText("You Have Found A Missing Link", "missing link")); + Assert.IsFalse(Subdominator.SubdomainHijack.ContainsFingerprintText("No matching string here", "missing link")); + } +} diff --git a/Subdominator/Models/Fingerprint.cs b/Subdominator/Models/Fingerprint.cs index 98e2871..ec1a5ee 100644 --- a/Subdominator/Models/Fingerprint.cs +++ b/Subdominator/Models/Fingerprint.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Subdominator; @@ -15,7 +15,7 @@ public class Fingerprint [JsonPropertyName("fingerprint")] [JsonConverter(typeof(SingleOrArrayConverter))] - public List FingerprintTexts { get; set; } + public List FingerprintTexts { get; set; } = new(); [JsonPropertyName("http_status")] public int? HttpStatus { get; set; } diff --git a/Subdominator/Properties/Assemblyinfo.cs b/Subdominator/Properties/Assemblyinfo.cs new file mode 100644 index 0000000..74a21a3 --- /dev/null +++ b/Subdominator/Properties/Assemblyinfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Subdominator.Tests")] diff --git a/Subdominator/SubdomainHijack.cs b/Subdominator/SubdomainHijack.cs index 5e618cd..1653801 100644 --- a/Subdominator/SubdomainHijack.cs +++ b/Subdominator/SubdomainHijack.cs @@ -1,4 +1,4 @@ -using DnsClient; +using DnsClient; using Microsoft.Extensions.Configuration; using Nager.PublicSuffix; using Nager.PublicSuffix.RuleProviders.CacheProviders; @@ -8,6 +8,7 @@ using System.Collections.Concurrent; using System.Text; using System.Text.Json; +using System.Net; namespace Subdominator; @@ -400,9 +401,9 @@ public async Task IsDomainVulnerable(string domain, bool validat // Check an individual fingerprint against and domain name and dns records public async Task<(bool, MatchedRecord, MatchedLocation)> IsFingerprintVulnerable(Fingerprint fingerprint, CnameResolutionResult dns, string domain) { - var isCnameMatch = dns.Cnames.Any(r => fingerprint.Cnames.Any(r.Contains)); - var isAMatch = dns.A.Any(r => fingerprint.ARecords.Any(r.Contains)); - var isAaaaMatch = dns.AAAA.Any(r => fingerprint.AAAARecords.Any(r.Contains)); + var isCnameMatch = dns.Cnames.Any(r => fingerprint.Cnames.Any(fingerprintValue => MatchesFingerprintValue(r, fingerprintValue))); + var isAMatch = dns.A.Any(r => fingerprint.ARecords.Any(fingerprintValue => MatchesFingerprintValue(r, fingerprintValue))); + var isAaaaMatch = dns.AAAA.Any(r => fingerprint.AAAARecords.Any(fingerprintValue => MatchesFingerprintValue(r, fingerprintValue))); var matchedRecord = MatchedRecord.None; if (isCnameMatch) @@ -462,7 +463,7 @@ public async Task IsDomainVulnerable(string domain, bool validat { foreach (var fingerprintMatch in fingerprint.FingerprintTexts) { - if (!string.IsNullOrWhiteSpace(fingerprintMatch) && responseBody.Contains(fingerprintMatch)) + if (ContainsFingerprintText(responseBody, fingerprintMatch)) { return (true, matchedRecord, MatchedLocation.HttpBody); } @@ -707,4 +708,45 @@ private bool IsDomainRegistered(string whoisResponse) } } } + + internal static bool MatchesFingerprintValue(string actualValue, string fingerprintValue) + { + if (string.IsNullOrWhiteSpace(actualValue) || string.IsNullOrWhiteSpace(fingerprintValue)) + { + return false; + } + + var normalizedActual = NormalizeDnsValue(actualValue); + var normalizedFingerprint = NormalizeDnsValue(fingerprintValue); + + if (LooksLikeIpPrefix(normalizedFingerprint)) + { + return normalizedActual.StartsWith(normalizedFingerprint, StringComparison.OrdinalIgnoreCase); + } + + if (normalizedFingerprint.Contains('.')) + { + return normalizedActual.Equals(normalizedFingerprint, StringComparison.OrdinalIgnoreCase) + || normalizedActual.EndsWith($".{normalizedFingerprint}", StringComparison.OrdinalIgnoreCase); + } + + return normalizedActual.Contains(normalizedFingerprint, StringComparison.OrdinalIgnoreCase); + } + + internal static bool ContainsFingerprintText(string responseBody, string fingerprintMatch) + { + return !string.IsNullOrWhiteSpace(responseBody) + && !string.IsNullOrWhiteSpace(fingerprintMatch) + && responseBody.Contains(fingerprintMatch, StringComparison.OrdinalIgnoreCase); + } + + private static string NormalizeDnsValue(string value) + { + return value.Trim().TrimEnd('.'); + } + + private static bool LooksLikeIpPrefix(string value) + { + return value.All(character => char.IsDigit(character) || character is '.' or ':' or 'x' or 'X'); + } }