mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
the live-response path flagged any host whose body matched a provider fingerprint without checking dns, so a page merely containing a provider 404 string was reported as a takeover. resolve the cname and only confirm when it maps to the same provider; a contradicting cname kills the flag. providers with a body signature but no apex to correlate, and unresolvable lookups, degrade to a medium potential finding via a new confidence field rather than confirming or dropping.
148 lines
7.1 KiB
Go
148 lines
7.1 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package scan
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// serveFingerprint stands up a loopback server returning body at 200 OK and
|
|
// returns its host:port for checkSubdomainTakeover to treat as a live subdomain.
|
|
func serveFingerprint(t *testing.T, body string) string {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return strings.TrimPrefix(srv.URL, "http://")
|
|
}
|
|
|
|
// a body fingerprint for a service that can-i-take-over-xyz marks "Not
|
|
// vulnerable" must not raise a takeover: the provider mitigated the vector, so a
|
|
// live page carrying the string is never claimable.
|
|
func TestCheckSubdomainTakeover_NotVulnerableServiceNotFlagged(t *testing.T) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
mitigated := map[string]string{
|
|
"Fastly": "Fastly error: unknown domain",
|
|
"Zendesk": "Help Center Closed",
|
|
"UserVoice": "This UserVoice subdomain is currently available!",
|
|
"Acquia": "The site you are looking for could not be found.",
|
|
}
|
|
for service, fingerprint := range mitigated {
|
|
subdomain := serveFingerprint(t, "<html><body>"+fingerprint+"</body></html>")
|
|
vulnerable, got, _ := checkSubdomainTakeover(subdomain, client)
|
|
if vulnerable || got != "" {
|
|
t.Errorf("%s fingerprint raised a takeover (vulnerable=%v service=%q); the provider mitigated the vector", service, vulnerable, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// a still-vulnerable provider's fingerprint keeps firing, so pruning the
|
|
// mitigated services does not silently drop real detections.
|
|
func TestCheckSubdomainTakeover_VulnerableServiceStillFlagged(t *testing.T) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
subdomain := serveFingerprint(t, "<html><body>The specified bucket does not exist</body></html>")
|
|
vulnerable, service, _ := checkSubdomainTakeover(subdomain, client)
|
|
if !vulnerable || service != "Amazon S3" {
|
|
t.Errorf("expected Amazon S3 takeover, got vulnerable=%v service=%q", vulnerable, service)
|
|
}
|
|
}
|
|
|
|
// reproduces the fixed false positive: a body fingerprint whose cname resolves
|
|
// elsewhere (or is a plain A record) must not be flagged. the pre-fix live path
|
|
// ignored the cname and fired on the body match alone.
|
|
func TestCheckSubdomainTakeover_BodySignatureWithoutMatchingCNAMENotConfirmed(t *testing.T) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
subdomain := serveFingerprint(t, "<html><body>The specified bucket does not exist</body></html>")
|
|
|
|
orig := lookupCNAME
|
|
defer func() { lookupCNAME = orig }()
|
|
|
|
// cname resolves, but to something that isn't the s3 apex: the signature
|
|
// match is coincidental, not a dangling delegation.
|
|
lookupCNAME = func(_ context.Context, _ string) (string, error) {
|
|
return "unrelated-app.example.net.", nil
|
|
}
|
|
|
|
vulnerable, service, confidence := checkSubdomainTakeover(subdomain, client)
|
|
if vulnerable {
|
|
t.Errorf("expected no takeover when cname does not back the matched provider, got vulnerable=%v service=%q confidence=%q", vulnerable, service, confidence)
|
|
}
|
|
}
|
|
|
|
// when the cname does resolve to the matched provider's apex, the same body
|
|
// signature is a confirmed takeover, not just a potential one.
|
|
func TestCheckSubdomainTakeover_BodySignatureWithMatchingCNAMEConfirmed(t *testing.T) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
subdomain := serveFingerprint(t, "<html><body>The specified bucket does not exist</body></html>")
|
|
|
|
orig := lookupCNAME
|
|
defer func() { lookupCNAME = orig }()
|
|
|
|
lookupCNAME = func(_ context.Context, _ string) (string, error) {
|
|
return "mybucket.s3.amazonaws.com.", nil
|
|
}
|
|
|
|
vulnerable, service, confidence := checkSubdomainTakeover(subdomain, client)
|
|
if !vulnerable || service != "Amazon S3" || confidence != "confirmed" {
|
|
t.Errorf("expected confirmed Amazon S3 takeover, got vulnerable=%v service=%q confidence=%q", vulnerable, service, confidence)
|
|
}
|
|
}
|
|
|
|
// a provider with no cname apex to correlate against (e.g. tumblr) must degrade
|
|
// to a low-confidence potential, not be dropped: dropping it would be a false
|
|
// negative on a real takeover. see serviceCorrelatable.
|
|
func TestCheckSubdomainTakeover_UncorrelatableProviderDegradesToPotential(t *testing.T) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
subdomain := serveFingerprint(t, "<html><body>There's nothing here.</body></html>")
|
|
|
|
orig := lookupCNAME
|
|
defer func() { lookupCNAME = orig }()
|
|
|
|
// host resolves (lookup succeeds), but tumblr has no apex to check against.
|
|
lookupCNAME = func(_ context.Context, _ string) (string, error) {
|
|
return "domains.tumblr.com.", nil
|
|
}
|
|
|
|
vulnerable, service, confidence := checkSubdomainTakeover(subdomain, client)
|
|
if !vulnerable || service != "Tumblr" || confidence != "potential" {
|
|
t.Errorf("expected potential Tumblr takeover, got vulnerable=%v service=%q confidence=%q", vulnerable, service, confidence)
|
|
}
|
|
}
|
|
|
|
// removed fingerprints matched generic content, not a provider's unclaimed-domain
|
|
// page (none in can-i-take-over-xyz), so must not raise a takeover: activecampaign
|
|
// was lighttpd's default page, kajabi/thinkific/tave/teamwork generic "not found".
|
|
func TestCheckSubdomainTakeover_GenericFingerprintNotFlagged(t *testing.T) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
bogus := map[string]string{
|
|
"lighttpd default page": `<html><body><img alt="LIGHTTPD - fly light." src="light.png"></body></html>`,
|
|
"generic 404 (kajabi)": "<html><body>The page you were looking for doesn't exist.</body></html>",
|
|
"generic 404 (thinkific)": "<html><body>You may have mistyped the address or the page may have moved.</body></html>",
|
|
"generic 404 (tave)": "<html><body>Sorry, this page is no longer available.</body></html>",
|
|
"generic 404 (teamwork)": "<html><body>Oops - We didn't find your site.</body></html>",
|
|
}
|
|
for name, body := range bogus {
|
|
subdomain := serveFingerprint(t, body)
|
|
vulnerable, service, _ := checkSubdomainTakeover(subdomain, client)
|
|
if vulnerable || service != "" {
|
|
t.Errorf("%s raised a takeover (vulnerable=%v service=%q); it matches generic content, not an unclaimed-domain page", name, vulnerable, service)
|
|
}
|
|
}
|
|
}
|