Second Order: Finding Subdomain Takeovers Hidden in Your Application Logic
Hook
Most subdomain takeover scanners check your DNS records. But what about the forgotten external domains hardcoded in your JavaScript, CSS, and HTML that attackers can claim while bypassing your perimeter defenses entirely?
Context
Traditional subdomain takeover detection focuses on first-order vulnerabilities: scanning DNS records for dangling CNAMEs pointing to unclaimed cloud services. Tools like subjack and SubOver excel at this—they enumerate your subdomains, check DNS, fingerprint services, and tell you what's claimable. But there's a subtler attack vector they miss entirely.
Second-order subdomain takeovers occur when your application references external domains in its code, content, or configuration. A CDN URL in a JavaScript file. An abandoned analytics domain in your CSS. A marketing subdomain hardcoded in an iframe that no one renewed. These aren't in your DNS zone file—they're scattered across thousands of HTML files, JavaScript bundles, and API responses. An attacker who claims these domains can inject malicious code directly into your application's execution context, bypassing Content Security Policy in some cases and gaining the trust of your origin. Second Order was built specifically to find these hidden time bombs by crawling your live application and mapping every external reference it can extract.
Technical Insight
Second Order's architecture is straightforward: it's a concurrent web crawler that extracts HTML elements based on configurable queries, then validates the URLs it finds. Written in Go for performance and portability, it takes a target URL, crawl depth, and thread count as inputs, then systematically maps your application while looking for external domain references.
The real intelligence lies in its configuration system. Rather than hardcoding what to extract, Second Order uses JSON config files that define tag-attribute pairs to search for. Here's an example config for finding script sources and image references:
{
"queries": [
{
"tag": "script",
"attribute": "src"
},
{
"tag": "img",
"attribute": "src"
},
{
"tag": "link",
"attribute": "href"
}
]
}
This declarative approach makes Second Order remarkably versatile. Want to extract all iframe sources? Add {"tag": "iframe", "attribute": "src"}. Looking for form actions that might POST to external domains? Include {"tag": "form", "attribute": "action"}. The tool comes with several pre-built configs: config.json for subdomain takeovers, jsconfig.json for JavaScript file collection, and wordlist.json for parameter extraction—but you can craft custom queries for any reconnaissance goal.
The crawler operates in three phases. First, it spiders the target application to the specified depth, following links and building a URL map. During crawling, it extracts elements matching your config queries and stores them in memory. Finally, it performs HTTP HEAD requests on every discovered URL to check response codes. The output is three JSON files: all.json containing every matched element, non200.json with potentially broken references (your takeover candidates), and inline.json for inline JavaScript and CSS content.
Here's how you'd run a basic scan:
# Scan example.com with depth 2, using 10 threads
second-order -target https://example.com -depth 2 -threads 10 -config config.json
# Output files:
# all.json - Complete mapping of discovered references
# non200.json - Broken/claimable domain candidates
# inline.json - Inline code that might contain hardcoded domains
The non200.json file is where you'll find your takeover candidates. It maps each crawled URL to external resources that returned non-200 status codes—domains that might be expired, misconfigured, or claimable:
{
"https://example.com/dashboard": [
"https://old-cdn.example-corp.com/bundle.js",
"https://analytics.abandoned-startup.io/track.js"
]
}
From here, you'd manually verify which domains are actually claimable by checking registration status, trying to set up the service, or using specialized tools that understand platform-specific takeover conditions.
The concurrent crawling implementation is worth examining. Second Order uses goroutines with a worker pool pattern, allowing you to tune parallelism based on your target's capacity and your bandwidth. The -threads flag controls concurrency—set it too high and you'll overwhelm small servers or trigger rate limiting; too low and scans drag on. In practice, 10-20 threads works well for most targets.
One clever aspect is the inline content extraction. Many applications don't just reference external domains in HTML attributes—they build URLs dynamically in JavaScript. Second Order captures <script> and <style> tag content in inline.json, letting you grep for domain patterns manually. It's not full JavaScript AST parsing, but it's enough to catch hardcoded strings like const API_URL = 'https://old-api.startup.io' that might indicate claimable domains.
Gotcha
Second Order makes two critical assumptions that limit its effectiveness in modern web environments. First, it only processes server-rendered HTML. If your target is a React or Vue SPA that renders content client-side via JavaScript, the crawler sees an empty <div id="app"></div> and extracts nothing. There's no headless browser, no JavaScript execution—just raw HTTP requests parsing static HTML. This works perfectly for traditional server-side rendered applications but fails spectacularly on modern single-page apps.
Second, the tool identifies potential takeovers without verifying them. A non-200 response doesn't mean a domain is claimable—it might be temporarily down, behind authentication, or deliberately returning 404s. You'll get false positives from CDN URLs that return 403 without proper headers, APIs that require authentication, or domains that simply moved. Second Order hands you a list of suspects; you still need to do the detective work of checking WHOIS records, attempting service registration, and understanding platform-specific takeover conditions. It won't tell you that something.s3.amazonaws.com requires creating that specific bucket or that app.github.io needs you to create a GitHub Pages site with that name.
The crawling depth can also be deceptive. Setting -depth 3 sounds reasonable until you realize that complex applications have thousands of pages. The crawler is breadth-first and doesn't respect application structure, so it might spend all its time in one section while missing entire subsystems. There's no smart crawling based on sitemaps, no session handling for authenticated areas, and no form submission to reach POST-only content. Compare this to Burp Suite's spider, which handles authentication, executes JavaScript, and intelligently explores application state.
Verdict
Use Second Order if you're hunting for second-order subdomain takeovers in traditional, server-rendered web applications during bug bounty programs or penetration tests. It's lightweight enough to run on every target, configurable enough to adapt to custom reconnaissance needs, and fast enough to provide results in minutes rather than hours. The JSON output integrates cleanly into automated pipelines, and the config system lets you repurpose it for JS file collection, parameter wordlist generation, or CDN discovery. It excels at mapping external dependencies in monolithic apps where forgotten domains lurk in legacy code. Skip it if your target is a JavaScript-heavy SPA (you need a headless crawler like Playwright or Puppeteer), you want verified subdomain takeover detection with actual claimability checks (use SubOver or can-i-take-over-xyz), or you need deep application crawling with authentication and session handling (Burp Suite or ZAP are better suited). Also skip it for DNS-based first-order takeover hunting—subjack and similar tools are purpose-built for that and much faster. Second Order fills a specific niche: automated discovery of external domain references in server-rendered applications where manual review would be prohibitively time-consuming.