mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-08-17 19:07:12 +02:00
🚀 God's Eye v0.1 - Initial Release
God's Eye is an ultra-fast subdomain enumeration and reconnaissance tool with AI-powered security analysis. ## ✨ Key Features ### 🔍 Comprehensive Enumeration - 20+ passive sources (crt.sh, Censys, URLScan, etc.) - DNS brute-force with smart wordlists - Wildcard detection and filtering - 1000 concurrent workers for maximum speed ### 🌐 Deep Reconnaissance - HTTP probing with 13+ security checks - Port scanning (configurable) - TLS/SSL fingerprinting - Technology detection (Wappalyzer-style) - WAF detection (Cloudflare, Akamai, etc.) - Security header analysis - JavaScript secrets extraction - Admin panel & API discovery - Backup file detection - robots.txt & sitemap.xml checks ### 🎯 Subdomain Takeover Detection - 110+ fingerprints (AWS, Azure, GitHub Pages, Heroku, etc.) - CNAME validation - Dead DNS detection ### 🤖 AI-Powered Analysis (NEW!) - Local AI using Ollama - No API costs, complete privacy - Real-time CVE detection via function calling (queries NVD database) - Cascade architecture: phi3.5 (fast triage) + qwen2.5-coder (deep analysis) - JavaScript security analysis - HTTP response anomaly detection - Executive summary reports ### 📊 Output Formats - Pretty terminal output with colors - JSON export - CSV export - TXT (simple subdomain list) - Silent mode for piping ## 🚀 Installation bash go install github.com/Vyntral/god-eye@latest ## 📖 Quick Start bash # Basic scan god-eye -d example.com # With AI analysis god-eye -d example.com --enable-ai # Only active hosts god-eye -d example.com --active # Export to JSON god-eye -d example.com -o results.json -f json ## 🎯 Use Cases - Bug bounty reconnaissance - Penetration testing - Security audits - Attack surface mapping - Red team operations ## ⚠️ Legal Notice This tool is for authorized security testing only. Users must obtain explicit permission before scanning any targets. Unauthorized access is illegal. ## 📄 License MIT License with additional security tool terms - see LICENSE file ## 🙏 Credits Built with ❤️ by Vyntral for Orizon Powered by Go, Ollama, and the security community --- 🤖 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckOpenRedirect tests for open redirect vulnerabilities
|
||||
func CheckOpenRedirect(subdomain string, timeout int) bool {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// Common open redirect parameters
|
||||
testPayloads := []string{
|
||||
"?url=https://evil.com",
|
||||
"?redirect=https://evil.com",
|
||||
"?next=https://evil.com",
|
||||
"?return=https://evil.com",
|
||||
"?dest=https://evil.com",
|
||||
"?destination=https://evil.com",
|
||||
"?rurl=https://evil.com",
|
||||
"?target=https://evil.com",
|
||||
}
|
||||
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
for _, payload := range testPayloads {
|
||||
testURL := baseURL + payload
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Check if redirects to evil.com
|
||||
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||
location := resp.Header.Get("Location")
|
||||
if strings.Contains(location, "evil.com") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckCORS tests for CORS misconfiguration
|
||||
func CheckCORS(subdomain string, timeout int) string {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Test with evil origin
|
||||
req.Header.Set("Origin", "https://evil.com")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
acao := resp.Header.Get("Access-Control-Allow-Origin")
|
||||
acac := resp.Header.Get("Access-Control-Allow-Credentials")
|
||||
|
||||
// Check for dangerous CORS configs
|
||||
if acao == "*" {
|
||||
if acac == "true" {
|
||||
return "Wildcard + Credentials"
|
||||
}
|
||||
return "Wildcard Origin"
|
||||
}
|
||||
|
||||
if acao == "https://evil.com" {
|
||||
if acac == "true" {
|
||||
return "Origin Reflection + Credentials"
|
||||
}
|
||||
return "Origin Reflection"
|
||||
}
|
||||
|
||||
if strings.Contains(acao, "null") {
|
||||
return "Null Origin Allowed"
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CheckHTTPMethods tests which HTTP methods are allowed
|
||||
func CheckHTTPMethods(subdomain string, timeout int) (allowed []string, dangerous []string) {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "TRACE"}
|
||||
dangerousMethods := map[string]bool{
|
||||
"PUT": true,
|
||||
"DELETE": true,
|
||||
"TRACE": true,
|
||||
"PATCH": true,
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
// First try OPTIONS to get Allow header
|
||||
req, err := http.NewRequest("OPTIONS", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Check Allow header
|
||||
allowHeader := resp.Header.Get("Allow")
|
||||
if allowHeader != "" {
|
||||
for _, method := range strings.Split(allowHeader, ",") {
|
||||
method = strings.TrimSpace(method)
|
||||
allowed = append(allowed, method)
|
||||
if dangerousMethods[method] {
|
||||
dangerous = append(dangerous, method)
|
||||
}
|
||||
}
|
||||
return allowed, dangerous
|
||||
}
|
||||
|
||||
// If no Allow header, test each method
|
||||
for _, method := range methods {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Method is allowed if not 405 Method Not Allowed
|
||||
if resp.StatusCode != 405 {
|
||||
allowed = append(allowed, method)
|
||||
if dangerousMethods[method] {
|
||||
dangerous = append(dangerous, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowed) > 0 {
|
||||
return allowed, dangerous
|
||||
}
|
||||
}
|
||||
|
||||
return allowed, dangerous
|
||||
}
|
||||
|
||||
// WithClient versions for parallel execution with shared client
|
||||
|
||||
func CheckOpenRedirectWithClient(subdomain string, client *http.Client) bool {
|
||||
testPayloads := []string{
|
||||
"?url=https://evil.com",
|
||||
"?redirect=https://evil.com",
|
||||
"?next=https://evil.com",
|
||||
"?return=https://evil.com",
|
||||
}
|
||||
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
for _, payload := range testPayloads {
|
||||
testURL := baseURL + payload
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||
location := resp.Header.Get("Location")
|
||||
// Check if redirect actually goes to evil.com, not just contains it as parameter
|
||||
if strings.HasPrefix(location, "https://evil.com") ||
|
||||
strings.HasPrefix(location, "http://evil.com") ||
|
||||
strings.HasPrefix(location, "//evil.com") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckCORSWithClient(subdomain string, client *http.Client) string {
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Origin", "https://evil.com")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
acao := resp.Header.Get("Access-Control-Allow-Origin")
|
||||
acac := resp.Header.Get("Access-Control-Allow-Credentials")
|
||||
|
||||
if acao == "*" {
|
||||
if acac == "true" {
|
||||
return "Wildcard + Credentials"
|
||||
}
|
||||
return "Wildcard Origin"
|
||||
}
|
||||
|
||||
if acao == "https://evil.com" {
|
||||
if acac == "true" {
|
||||
return "Origin Reflection + Credentials"
|
||||
}
|
||||
return "Origin Reflection"
|
||||
}
|
||||
|
||||
if strings.Contains(acao, "null") {
|
||||
return "Null Origin Allowed"
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func CheckHTTPMethodsWithClient(subdomain string, client *http.Client) (allowed []string, dangerous []string) {
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
dangerousMethods := map[string]bool{
|
||||
"PUT": true,
|
||||
"DELETE": true,
|
||||
"TRACE": true,
|
||||
"PATCH": true,
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
req, err := http.NewRequest("OPTIONS", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Only trust the Allow header from OPTIONS response
|
||||
// Don't probe individual methods as this causes too many false positives
|
||||
allowHeader := resp.Header.Get("Allow")
|
||||
if allowHeader != "" {
|
||||
for _, method := range strings.Split(allowHeader, ",") {
|
||||
method = strings.TrimSpace(method)
|
||||
allowed = append(allowed, method)
|
||||
if dangerousMethods[method] {
|
||||
dangerous = append(dangerous, method)
|
||||
}
|
||||
}
|
||||
return allowed, dangerous
|
||||
}
|
||||
}
|
||||
|
||||
// If no Allow header found, don't report anything to avoid false positives
|
||||
return allowed, dangerous
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CheckAdminPanels(subdomain string, timeout int) []string {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// Common admin panel paths
|
||||
paths := []string{
|
||||
"/admin", "/administrator", "/admin.php", "/admin.html",
|
||||
"/login", "/login.php", "/signin", "/auth",
|
||||
"/wp-admin", "/wp-login.php",
|
||||
"/phpmyadmin", "/pma", "/mysql",
|
||||
"/cpanel", "/webmail",
|
||||
"/manager", "/console", "/dashboard",
|
||||
"/admin/login", "/user/login",
|
||||
}
|
||||
|
||||
var found []string
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
for _, path := range paths {
|
||||
testURL := baseURL + path
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Found if 200, 301, 302, 401, 403 (not 404)
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 0 {
|
||||
found = append(found, path)
|
||||
}
|
||||
}
|
||||
if len(found) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// CheckGitSvnExposure checks for exposed .git or .svn directories
|
||||
func CheckGitSvnExposure(subdomain string, timeout int) (gitExposed bool, svnExposed bool) {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
// Check .git
|
||||
resp, err := client.Get(baseURL + "/.git/config")
|
||||
if err == nil {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1000))
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 && strings.Contains(string(body), "[core]") {
|
||||
gitExposed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check .svn
|
||||
resp, err = client.Get(baseURL + "/.svn/entries")
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
svnExposed = true
|
||||
}
|
||||
}
|
||||
|
||||
if gitExposed || svnExposed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return gitExposed, svnExposed
|
||||
}
|
||||
|
||||
// CheckBackupFiles checks for common backup files
|
||||
func CheckBackupFiles(subdomain string, timeout int) []string {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
// Common backup file patterns
|
||||
paths := []string{
|
||||
"/backup.zip", "/backup.tar.gz", "/backup.sql",
|
||||
"/db.sql", "/database.sql", "/dump.sql",
|
||||
"/site.zip", "/www.zip", "/public.zip",
|
||||
"/config.bak", "/config.old", "/.env.bak",
|
||||
"/index.php.bak", "/index.php.old", "/index.html.bak",
|
||||
"/web.config.bak", "/.htaccess.bak",
|
||||
}
|
||||
|
||||
var found []string
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
for _, path := range paths {
|
||||
resp, err := client.Head(baseURL + path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
found = append(found, path)
|
||||
}
|
||||
}
|
||||
if len(found) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// CheckAPIEndpoints checks for common API endpoints
|
||||
func CheckAPIEndpoints(subdomain string, timeout int) []string {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// Common API endpoint patterns
|
||||
paths := []string{
|
||||
"/api", "/api/v1", "/api/v2", "/api/v3",
|
||||
"/graphql", "/graphiql",
|
||||
"/swagger", "/swagger-ui", "/swagger.json", "/swagger.yaml",
|
||||
"/openapi.json", "/openapi.yaml",
|
||||
"/docs", "/api-docs", "/redoc",
|
||||
"/health", "/healthz", "/status",
|
||||
"/metrics", "/actuator", "/actuator/health",
|
||||
"/v1", "/v2", "/rest",
|
||||
}
|
||||
|
||||
var found []string
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
for _, path := range paths {
|
||||
resp, err := client.Get(baseURL + path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Found if not 404
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 0 {
|
||||
found = append(found, path)
|
||||
}
|
||||
}
|
||||
if len(found) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// WithClient versions for parallel execution
|
||||
|
||||
func CheckAdminPanelsWithClient(subdomain string, client *http.Client) []string {
|
||||
paths := []string{
|
||||
"/admin", "/administrator", "/admin.php", "/admin.html",
|
||||
"/login", "/login.php", "/signin", "/auth",
|
||||
"/wp-admin", "/wp-login.php",
|
||||
"/phpmyadmin", "/pma", "/mysql",
|
||||
"/cpanel", "/webmail",
|
||||
"/manager", "/console", "/dashboard",
|
||||
"/admin/login", "/user/login",
|
||||
}
|
||||
|
||||
var found []string
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
// First, get the root page to detect SPA catch-all behavior
|
||||
rootResp, err := client.Get(baseURL + "/")
|
||||
var rootContentLength string
|
||||
var rootContentType string
|
||||
if err == nil {
|
||||
rootContentLength = rootResp.Header.Get("Content-Length")
|
||||
rootContentType = rootResp.Header.Get("Content-Type")
|
||||
rootResp.Body.Close()
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
testURL := baseURL + path
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Report 200 OK (found), 401/403 (protected but exists)
|
||||
if resp.StatusCode == 200 {
|
||||
// Check for SPA catch-all: same content-length and content-type as root
|
||||
contentLength := resp.Header.Get("Content-Length")
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
|
||||
// If response matches root page exactly, it's likely SPA catch-all
|
||||
isSPACatchAll := rootContentLength != "" && contentLength == rootContentLength &&
|
||||
strings.Contains(contentType, "text/html") && strings.Contains(rootContentType, "text/html")
|
||||
|
||||
if !isSPACatchAll {
|
||||
found = append(found, path)
|
||||
}
|
||||
} else if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
// Protected endpoint exists
|
||||
found = append(found, path+" (protected)")
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) > 0 {
|
||||
break // Found results, no need to try HTTP
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func CheckGitSvnExposureWithClient(subdomain string, client *http.Client) (gitExposed bool, svnExposed bool) {
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
resp, err := client.Get(baseURL + "/.git/config")
|
||||
if err == nil {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1000))
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 && strings.Contains(string(body), "[core]") {
|
||||
gitExposed = true
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = client.Get(baseURL + "/.svn/entries")
|
||||
if err == nil {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1000))
|
||||
resp.Body.Close()
|
||||
// SVN entries file starts with version number or contains specific format
|
||||
// Must not be HTML (SPA catch-all returns HTML for all routes)
|
||||
bodyStr := string(body)
|
||||
if resp.StatusCode == 200 && !strings.Contains(bodyStr, "<html") && !strings.Contains(bodyStr, "<!DOCTYPE") {
|
||||
// Old SVN format starts with version number, new format is XML
|
||||
if len(bodyStr) > 0 && (bodyStr[0] >= '0' && bodyStr[0] <= '9' || strings.Contains(bodyStr, "<?xml")) {
|
||||
svnExposed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gitExposed || svnExposed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return gitExposed, svnExposed
|
||||
}
|
||||
|
||||
func CheckBackupFilesWithClient(subdomain string, client *http.Client) []string {
|
||||
paths := []string{
|
||||
"/backup.zip", "/backup.tar.gz", "/backup.sql",
|
||||
"/db.sql", "/database.sql", "/dump.sql",
|
||||
"/site.zip", "/www.zip", "/public.zip",
|
||||
"/config.bak", "/config.old", "/.env.bak",
|
||||
"/index.php.bak", "/index.php.old", "/index.html.bak",
|
||||
"/web.config.bak", "/.htaccess.bak",
|
||||
}
|
||||
|
||||
var found []string
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
for _, path := range paths {
|
||||
resp, err := client.Head(baseURL + path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
// Check content-type to avoid SPA catch-all false positives
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
// Backup files should NOT be text/html - that indicates SPA catch-all
|
||||
if !strings.Contains(contentType, "text/html") {
|
||||
found = append(found, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) > 0 {
|
||||
break // Found results, no need to try HTTP
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func CheckAPIEndpointsWithClient(subdomain string, client *http.Client) []string {
|
||||
paths := []string{
|
||||
"/api", "/api/v1", "/api/v2", "/api/v3",
|
||||
"/graphql", "/graphiql",
|
||||
"/swagger", "/swagger-ui", "/swagger.json", "/swagger.yaml",
|
||||
"/openapi.json", "/openapi.yaml",
|
||||
"/docs", "/api-docs", "/redoc",
|
||||
"/health", "/healthz", "/status",
|
||||
"/metrics", "/actuator", "/actuator/health",
|
||||
"/v1", "/v2", "/rest",
|
||||
}
|
||||
|
||||
var found []string
|
||||
baseURLs := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, baseURL := range baseURLs {
|
||||
// First, get the root page to detect SPA catch-all behavior
|
||||
rootResp, err := client.Get(baseURL + "/")
|
||||
var rootContentLength string
|
||||
var rootContentType string
|
||||
if err == nil {
|
||||
rootContentLength = rootResp.Header.Get("Content-Length")
|
||||
rootContentType = rootResp.Header.Get("Content-Type")
|
||||
rootResp.Body.Close()
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
resp, err := client.Get(baseURL + path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Report 200 OK (found), 401/403 (protected but exists)
|
||||
if resp.StatusCode == 200 {
|
||||
// Check for SPA catch-all: same content-length and content-type as root
|
||||
contentLength := resp.Header.Get("Content-Length")
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
|
||||
// If response matches root page exactly, it's likely SPA catch-all
|
||||
isSPACatchAll := rootContentLength != "" && contentLength == rootContentLength &&
|
||||
strings.Contains(contentType, "text/html") && strings.Contains(rootContentType, "text/html")
|
||||
|
||||
if !isSPACatchAll {
|
||||
found = append(found, path)
|
||||
}
|
||||
} else if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
// Protected endpoint exists
|
||||
found = append(found, path+" (protected)")
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) > 0 {
|
||||
break // Found results, no need to try HTTP
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
Reference in New Issue
Block a user