mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-08-17 15:17:14 +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,254 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CVEInfo represents CVE vulnerability information
|
||||
type CVEInfo struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
Severity string `json:"severity"`
|
||||
Score float64 `json:"score"`
|
||||
Published string `json:"published"`
|
||||
References []string `json:"references"`
|
||||
}
|
||||
|
||||
// NVDResponse represents the response from NVD API
|
||||
type NVDResponse struct {
|
||||
ResultsPerPage int `json:"resultsPerPage"`
|
||||
StartIndex int `json:"startIndex"`
|
||||
TotalResults int `json:"totalResults"`
|
||||
Vulnerabilities []struct {
|
||||
CVE struct {
|
||||
ID string `json:"id"`
|
||||
Published string `json:"published"`
|
||||
Descriptions []struct {
|
||||
Lang string `json:"lang"`
|
||||
Value string `json:"value"`
|
||||
} `json:"descriptions"`
|
||||
Metrics struct {
|
||||
CVSSMetricV31 []struct {
|
||||
CVSSData struct {
|
||||
BaseScore float64 `json:"baseScore"`
|
||||
BaseSeverity string `json:"baseSeverity"`
|
||||
} `json:"cvssData"`
|
||||
} `json:"cvssMetricV31,omitempty"`
|
||||
CVSSMetricV2 []struct {
|
||||
CVSSData struct {
|
||||
BaseScore float64 `json:"baseScore"`
|
||||
} `json:"cvssData"`
|
||||
BaseSeverity string `json:"baseSeverity"`
|
||||
} `json:"cvssMetricV2,omitempty"`
|
||||
} `json:"metrics,omitempty"`
|
||||
References []struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"references"`
|
||||
} `json:"cve"`
|
||||
} `json:"vulnerabilities"`
|
||||
}
|
||||
|
||||
var (
|
||||
nvdClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
nvdBaseURL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
||||
)
|
||||
|
||||
// SearchCVE searches for CVE vulnerabilities using NVD API
|
||||
func SearchCVE(technology string, version string) (string, error) {
|
||||
// Normalize technology name
|
||||
tech := normalizeTechnology(technology)
|
||||
|
||||
// Build search query
|
||||
query := tech
|
||||
if version != "" && version != "unknown" {
|
||||
query = fmt.Sprintf("%s %s", tech, version)
|
||||
}
|
||||
|
||||
// Query NVD API
|
||||
cves, err := queryNVD(query)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Unable to search CVE database for %s: %v", technology, err), nil
|
||||
}
|
||||
|
||||
if len(cves) == 0 {
|
||||
return fmt.Sprintf("No known CVE vulnerabilities found for %s %s in the NVD database. This doesn't guarantee the software is secure - always keep software updated.", technology, version), nil
|
||||
}
|
||||
|
||||
// Format results
|
||||
result := fmt.Sprintf("CVE Vulnerabilities for %s %s:\n\n", technology, version)
|
||||
result += fmt.Sprintf("Found %d CVE(s):\n\n", len(cves))
|
||||
|
||||
// Show top 5 most recent/critical CVEs
|
||||
maxShow := 5
|
||||
if len(cves) < maxShow {
|
||||
maxShow = len(cves)
|
||||
}
|
||||
|
||||
for i := 0; i < maxShow; i++ {
|
||||
cve := cves[i]
|
||||
result += fmt.Sprintf("🔴 %s (%s - Score: %.1f)\n", cve.ID, cve.Severity, cve.Score)
|
||||
result += fmt.Sprintf(" Published: %s\n", cve.Published)
|
||||
|
||||
// Truncate description if too long
|
||||
desc := cve.Description
|
||||
if len(desc) > 200 {
|
||||
desc = desc[:200] + "..."
|
||||
}
|
||||
result += fmt.Sprintf(" %s\n", desc)
|
||||
|
||||
if len(cve.References) > 0 {
|
||||
result += fmt.Sprintf(" Reference: %s\n", cve.References[0])
|
||||
}
|
||||
result += "\n"
|
||||
}
|
||||
|
||||
if len(cves) > maxShow {
|
||||
result += fmt.Sprintf("... and %d more CVEs. Check https://nvd.nist.gov for complete details.\n", len(cves)-maxShow)
|
||||
}
|
||||
|
||||
result += "\n⚠️ Recommendation: Update to the latest version to mitigate known vulnerabilities."
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// queryNVD queries the NVD API for CVE information
|
||||
func queryNVD(keyword string) ([]CVEInfo, error) {
|
||||
// Build URL with query parameters
|
||||
params := url.Values{}
|
||||
params.Add("keywordSearch", keyword)
|
||||
params.Add("resultsPerPage", "10") // Limit results
|
||||
|
||||
reqURL := fmt.Sprintf("%s?%s", nvdBaseURL, params.Encode())
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// NVD recommends including a user agent
|
||||
req.Header.Set("User-Agent", "GodEye-Security-Scanner/0.1")
|
||||
|
||||
// Execute request
|
||||
resp, err := nvdClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query NVD: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("NVD API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var nvdResp NVDResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&nvdResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse NVD response: %w", err)
|
||||
}
|
||||
|
||||
// Convert to CVEInfo
|
||||
var cves []CVEInfo
|
||||
for _, vuln := range nvdResp.Vulnerabilities {
|
||||
cve := CVEInfo{
|
||||
ID: vuln.CVE.ID,
|
||||
Published: formatDate(vuln.CVE.Published),
|
||||
}
|
||||
|
||||
// Get description
|
||||
for _, desc := range vuln.CVE.Descriptions {
|
||||
if desc.Lang == "en" {
|
||||
cve.Description = desc.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get severity and score (prefer CVSS v3.1)
|
||||
if len(vuln.CVE.Metrics.CVSSMetricV31) > 0 {
|
||||
metric := vuln.CVE.Metrics.CVSSMetricV31[0]
|
||||
cve.Score = metric.CVSSData.BaseScore
|
||||
cve.Severity = metric.CVSSData.BaseSeverity
|
||||
} else if len(vuln.CVE.Metrics.CVSSMetricV2) > 0 {
|
||||
metric := vuln.CVE.Metrics.CVSSMetricV2[0]
|
||||
cve.Score = metric.CVSSData.BaseScore
|
||||
cve.Severity = metric.BaseSeverity
|
||||
}
|
||||
|
||||
// Get references
|
||||
for _, ref := range vuln.CVE.References {
|
||||
cve.References = append(cve.References, ref.URL)
|
||||
}
|
||||
|
||||
cves = append(cves, cve)
|
||||
}
|
||||
|
||||
return cves, nil
|
||||
}
|
||||
|
||||
// normalizeTechnology normalizes technology names for better CVE search results
|
||||
func normalizeTechnology(tech string) string {
|
||||
tech = strings.ToLower(tech)
|
||||
|
||||
// Common normalizations
|
||||
replacements := map[string]string{
|
||||
"microsoft-iis": "iis",
|
||||
"apache httpd": "apache",
|
||||
"apache http server": "apache",
|
||||
"nginx/": "nginx",
|
||||
"wordpress": "wordpress",
|
||||
"asp.net": "asp.net",
|
||||
"next.js": "nextjs",
|
||||
"react": "react",
|
||||
"angular": "angular",
|
||||
"vue": "vue",
|
||||
"express": "express",
|
||||
"django": "django",
|
||||
"flask": "flask",
|
||||
"spring": "spring",
|
||||
"tomcat": "tomcat",
|
||||
"jetty": "jetty",
|
||||
"php": "php",
|
||||
"mysql": "mysql",
|
||||
"postgresql": "postgresql",
|
||||
"mongodb": "mongodb",
|
||||
"redis": "redis",
|
||||
"elasticsearch": "elasticsearch",
|
||||
"docker": "docker",
|
||||
"kubernetes": "kubernetes",
|
||||
"jenkins": "jenkins",
|
||||
"gitlab": "gitlab",
|
||||
"grafana": "grafana",
|
||||
}
|
||||
|
||||
for old, new := range replacements {
|
||||
if strings.Contains(tech, old) {
|
||||
return new
|
||||
}
|
||||
}
|
||||
|
||||
// Remove version numbers and extra info
|
||||
parts := strings.Fields(tech)
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
return tech
|
||||
}
|
||||
|
||||
// formatDate formats ISO 8601 date to a more readable format
|
||||
func formatDate(isoDate string) string {
|
||||
t, err := time.Parse(time.RFC3339, isoDate)
|
||||
if err != nil {
|
||||
return isoDate
|
||||
}
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OllamaClient handles communication with local Ollama instance
|
||||
type OllamaClient struct {
|
||||
BaseURL string
|
||||
FastModel string // phi3.5:3.8b for quick triage
|
||||
DeepModel string // qwen2.5-coder:7b for deep analysis
|
||||
Timeout time.Duration
|
||||
EnableCascade bool
|
||||
}
|
||||
|
||||
// OllamaRequest represents the request payload for Ollama API
|
||||
type OllamaRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// OllamaResponse represents the response from Ollama API
|
||||
type OllamaResponse struct {
|
||||
Model string `json:"model"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Response string `json:"response"`
|
||||
Done bool `json:"done"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
// AnalysisResult contains AI analysis findings
|
||||
type AnalysisResult struct {
|
||||
Type string // "javascript", "http", "anomaly", "report"
|
||||
Severity string // "critical", "high", "medium", "low", "info"
|
||||
Findings []string
|
||||
Model string
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// NewOllamaClient creates a new Ollama client
|
||||
func NewOllamaClient(baseURL, fastModel, deepModel string, enableCascade bool) *OllamaClient {
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:11434"
|
||||
}
|
||||
if fastModel == "" {
|
||||
fastModel = "phi3.5:3.8b"
|
||||
}
|
||||
if deepModel == "" {
|
||||
deepModel = "qwen2.5-coder:7b"
|
||||
}
|
||||
|
||||
return &OllamaClient{
|
||||
BaseURL: baseURL,
|
||||
FastModel: fastModel,
|
||||
DeepModel: deepModel,
|
||||
Timeout: 60 * time.Second,
|
||||
EnableCascade: enableCascade,
|
||||
}
|
||||
}
|
||||
|
||||
// IsAvailable checks if Ollama is running and models are available
|
||||
func (c *OllamaClient) IsAvailable() bool {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get(c.BaseURL + "/api/tags")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == 200
|
||||
}
|
||||
|
||||
// QuickTriage performs fast classification using lightweight model
|
||||
func (c *OllamaClient) QuickTriage(content, contextType string) (bool, string, error) {
|
||||
prompt := fmt.Sprintf(`You are a security triage expert. Quickly classify if this %s contains security-relevant information.
|
||||
|
||||
Content:
|
||||
%s
|
||||
|
||||
Respond with ONLY:
|
||||
- "RELEVANT: <brief reason>" if it contains security issues, secrets, vulnerabilities, or suspicious patterns
|
||||
- "SKIP: <brief reason>" if it's normal/benign
|
||||
|
||||
Be concise. One line response only.`, contextType, truncate(content, 2000))
|
||||
|
||||
start := time.Now()
|
||||
response, err := c.query(c.FastModel, prompt, 10*time.Second)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
// Parse response
|
||||
isRelevant := strings.HasPrefix(strings.ToUpper(response), "RELEVANT:")
|
||||
reason := strings.TrimPrefix(response, "RELEVANT:")
|
||||
reason = strings.TrimPrefix(reason, "SKIP:")
|
||||
reason = strings.TrimSpace(reason)
|
||||
|
||||
if duration > 5*time.Second {
|
||||
// If fast model is too slow, disable it
|
||||
c.EnableCascade = false
|
||||
}
|
||||
|
||||
return isRelevant, reason, nil
|
||||
}
|
||||
|
||||
// AnalyzeJavaScript performs deep analysis of JavaScript code
|
||||
func (c *OllamaClient) AnalyzeJavaScript(code string) (*AnalysisResult, error) {
|
||||
// Fast triage first if cascade enabled
|
||||
if c.EnableCascade {
|
||||
relevant, reason, err := c.QuickTriage(code, "JavaScript code")
|
||||
if err == nil && !relevant {
|
||||
return &AnalysisResult{
|
||||
Type: "javascript",
|
||||
Severity: "info",
|
||||
Findings: []string{fmt.Sprintf("Skipped (triage: %s)", reason)},
|
||||
Model: c.FastModel,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`You are a security expert analyzing JavaScript code. Identify:
|
||||
|
||||
1. **Hardcoded Secrets**: API keys, tokens, passwords, private keys
|
||||
2. **Vulnerabilities**: XSS, injection points, insecure functions
|
||||
3. **Suspicious Patterns**: Obfuscation, backdoors, malicious logic
|
||||
4. **Hidden Endpoints**: Undocumented APIs, internal URLs
|
||||
|
||||
JavaScript Code:
|
||||
%s
|
||||
|
||||
Format your response as:
|
||||
CRITICAL: <finding>
|
||||
HIGH: <finding>
|
||||
MEDIUM: <finding>
|
||||
LOW: <finding>
|
||||
INFO: <finding>
|
||||
|
||||
Only list actual findings. Be concise and specific.`, truncate(code, 3000))
|
||||
|
||||
start := time.Now()
|
||||
response, err := c.query(c.DeepModel, prompt, 30*time.Second)
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseFindings(response, "javascript", c.DeepModel, duration), nil
|
||||
}
|
||||
|
||||
// AnalyzeHTTPResponse analyzes HTTP response for security issues
|
||||
func (c *OllamaClient) AnalyzeHTTPResponse(subdomain string, statusCode int, headers []string, body string) (*AnalysisResult, error) {
|
||||
// Fast triage
|
||||
if c.EnableCascade {
|
||||
content := fmt.Sprintf("Status: %d\nHeaders: %s\nBody: %s", statusCode, strings.Join(headers, ", "), truncate(body, 500))
|
||||
relevant, reason, err := c.QuickTriage(content, "HTTP response")
|
||||
if err == nil && !relevant {
|
||||
return &AnalysisResult{
|
||||
Type: "http",
|
||||
Severity: "info",
|
||||
Findings: []string{fmt.Sprintf("Normal response (triage: %s)", reason)},
|
||||
Model: c.FastModel,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Analyze this HTTP response for security issues:
|
||||
|
||||
URL: %s
|
||||
Status: %d
|
||||
Headers: %s
|
||||
Body (first 1000 chars): %s
|
||||
|
||||
Identify:
|
||||
- Information disclosure
|
||||
- Misconfigurations
|
||||
- Debug/error information exposure
|
||||
- Unusual behavior patterns
|
||||
|
||||
Format as: SEVERITY: finding`, subdomain, statusCode, strings.Join(headers, "\n"), truncate(body, 1000))
|
||||
|
||||
start := time.Now()
|
||||
response, err := c.query(c.DeepModel, prompt, 20*time.Second)
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseFindings(response, "http", c.DeepModel, duration), nil
|
||||
}
|
||||
|
||||
// DetectAnomalies identifies unusual patterns across scan results
|
||||
func (c *OllamaClient) DetectAnomalies(summary string) (*AnalysisResult, error) {
|
||||
prompt := fmt.Sprintf(`You are analyzing subdomain enumeration results. Find anomalies and prioritize findings:
|
||||
|
||||
%s
|
||||
|
||||
Identify:
|
||||
- Subdomains with unusual behavior vs others
|
||||
- Potential high-value targets (admin, api, internal)
|
||||
- Misconfigurations or exposed services
|
||||
- Patterns suggesting vulnerabilities
|
||||
|
||||
Format: SEVERITY: finding`, truncate(summary, 4000))
|
||||
|
||||
start := time.Now()
|
||||
response, err := c.query(c.DeepModel, prompt, 30*time.Second)
|
||||
duration := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseFindings(response, "anomaly", c.DeepModel, duration), nil
|
||||
}
|
||||
|
||||
// GenerateReport creates executive summary and recommendations
|
||||
func (c *OllamaClient) GenerateReport(findings string, stats map[string]int) (string, error) {
|
||||
prompt := fmt.Sprintf(`Create a concise security assessment report:
|
||||
|
||||
SCAN STATISTICS:
|
||||
- Total subdomains: %d
|
||||
- Active: %d
|
||||
- Vulnerabilities: %d
|
||||
- Takeovers: %d
|
||||
|
||||
KEY FINDINGS:
|
||||
%s
|
||||
|
||||
Generate report with:
|
||||
## Executive Summary (2-3 sentences)
|
||||
## Critical Findings (prioritized list)
|
||||
## Recommendations (actionable items)
|
||||
|
||||
Be concise and professional.`,
|
||||
stats["total"], stats["active"], stats["vulns"], stats["takeovers"], truncate(findings, 3000))
|
||||
|
||||
response, err := c.query(c.DeepModel, prompt, 45*time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CVEMatch checks for known vulnerabilities in detected technologies using function calling
|
||||
func (c *OllamaClient) CVEMatch(technology, version string) (string, error) {
|
||||
prompt := fmt.Sprintf(`Check if %s version %s has known CVE vulnerabilities. Use the search_cve tool to look up real CVE data from the NVD database.
|
||||
|
||||
After getting CVE results, analyze them and provide:
|
||||
1. Summary of findings
|
||||
2. Severity assessment
|
||||
3. Specific recommendations
|
||||
|
||||
If version is unknown, still search using just the technology name.`, technology, version)
|
||||
|
||||
// Use function calling with tools
|
||||
response, err := c.queryWithTools(c.DeepModel, prompt, 30*time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(response), "no known cve") {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// query sends a request to Ollama API
|
||||
func (c *OllamaClient) query(model, prompt string, timeout time.Duration) (string, error) {
|
||||
reqBody := OllamaRequest{
|
||||
Model: model,
|
||||
Prompt: prompt,
|
||||
Stream: false,
|
||||
Options: map[string]interface{}{
|
||||
"temperature": 0.3, // Low temperature for more focused responses
|
||||
"top_p": 0.9,
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Post(
|
||||
c.BaseURL+"/api/generate",
|
||||
"application/json",
|
||||
bytes.NewBuffer(jsonData),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ollama request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("ollama returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var ollamaResp OllamaResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
|
||||
return "", fmt.Errorf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(ollamaResp.Response), nil
|
||||
}
|
||||
|
||||
// parseFindings extracts findings by severity from AI response
|
||||
func parseFindings(response, findingType, model string, duration time.Duration) *AnalysisResult {
|
||||
result := &AnalysisResult{
|
||||
Type: findingType,
|
||||
Severity: "info",
|
||||
Findings: []string{},
|
||||
Model: model,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
lines := strings.Split(response, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse severity-prefixed findings
|
||||
upper := strings.ToUpper(line)
|
||||
if strings.HasPrefix(upper, "CRITICAL:") {
|
||||
result.Severity = "critical"
|
||||
result.Findings = append(result.Findings, strings.TrimPrefix(line, "CRITICAL:"))
|
||||
} else if strings.HasPrefix(upper, "HIGH:") {
|
||||
if result.Severity != "critical" {
|
||||
result.Severity = "high"
|
||||
}
|
||||
result.Findings = append(result.Findings, strings.TrimPrefix(line, "HIGH:"))
|
||||
} else if strings.HasPrefix(upper, "MEDIUM:") {
|
||||
if result.Severity != "critical" && result.Severity != "high" {
|
||||
result.Severity = "medium"
|
||||
}
|
||||
result.Findings = append(result.Findings, strings.TrimPrefix(line, "MEDIUM:"))
|
||||
} else if strings.HasPrefix(upper, "LOW:") {
|
||||
if result.Severity == "info" {
|
||||
result.Severity = "low"
|
||||
}
|
||||
result.Findings = append(result.Findings, strings.TrimPrefix(line, "LOW:"))
|
||||
} else if strings.HasPrefix(upper, "INFO:") {
|
||||
result.Findings = append(result.Findings, strings.TrimPrefix(line, "INFO:"))
|
||||
} else if len(line) > 0 && !strings.HasPrefix(line, "#") {
|
||||
// Non-prefixed findings
|
||||
result.Findings = append(result.Findings, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up findings
|
||||
for i := range result.Findings {
|
||||
result.Findings[i] = strings.TrimSpace(result.Findings[i])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// queryWithTools sends a request to Ollama API with function calling support
|
||||
func (c *OllamaClient) queryWithTools(model, prompt string, timeout time.Duration) (string, error) {
|
||||
tools := GetAvailableTools()
|
||||
|
||||
reqBody := OllamaRequest{
|
||||
Model: model,
|
||||
Prompt: prompt,
|
||||
Stream: false,
|
||||
Tools: tools,
|
||||
Options: map[string]interface{}{
|
||||
"temperature": 0.3,
|
||||
"top_p": 0.9,
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Post(
|
||||
c.BaseURL+"/api/generate",
|
||||
"application/json",
|
||||
bytes.NewBuffer(jsonData),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ollama request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("ollama returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var ollamaResp OllamaResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
|
||||
return "", fmt.Errorf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
// Check if AI requested tool calls
|
||||
if len(ollamaResp.ToolCalls) > 0 {
|
||||
// Execute tool calls and get results
|
||||
toolResults := make(map[string]string)
|
||||
for _, toolCall := range ollamaResp.ToolCalls {
|
||||
result, err := ExecuteTool(toolCall)
|
||||
if err != nil {
|
||||
toolResults[toolCall.Function.Name] = fmt.Sprintf("Error: %v", err)
|
||||
} else {
|
||||
toolResults[toolCall.Function.Name] = result
|
||||
}
|
||||
}
|
||||
|
||||
// Send tool results back to AI for final analysis
|
||||
followUpPrompt := fmt.Sprintf(`%s
|
||||
|
||||
Tool Results:
|
||||
%s
|
||||
|
||||
Based on these results, provide your analysis.`, prompt, formatToolResults(toolResults))
|
||||
|
||||
return c.query(model, followUpPrompt, timeout)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(ollamaResp.Response), nil
|
||||
}
|
||||
|
||||
// formatToolResults formats tool execution results for the AI
|
||||
func formatToolResults(results map[string]string) string {
|
||||
var formatted strings.Builder
|
||||
for tool, result := range results {
|
||||
formatted.WriteString(fmt.Sprintf("\n=== %s ===\n%s\n", tool, result))
|
||||
}
|
||||
return formatted.String()
|
||||
}
|
||||
|
||||
// truncate limits string length for prompts
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "\n...(truncated)"
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Tool represents a function that can be called by the AI
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
Function ToolFunction `json:"function"`
|
||||
}
|
||||
|
||||
// ToolFunction describes a callable function
|
||||
type ToolFunction struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
|
||||
// ToolCall represents an AI request to call a function
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function ToolCallFunction `json:"function"`
|
||||
}
|
||||
|
||||
// ToolCallFunction contains the function name and arguments
|
||||
type ToolCallFunction struct {
|
||||
Name string `json:"name"`
|
||||
Arguments json.RawMessage `json:"arguments"`
|
||||
}
|
||||
|
||||
// GetAvailableTools returns the list of tools available for AI function calling
|
||||
func GetAvailableTools() []Tool {
|
||||
return []Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: ToolFunction{
|
||||
Name: "search_cve",
|
||||
Description: "Search for CVE vulnerabilities for a specific software/technology and version. Returns a list of known CVEs with descriptions and severity.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"technology": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "The software or technology name (e.g., 'nginx', 'Apache', 'WordPress', 'IIS')",
|
||||
},
|
||||
"version": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "The version number if known (e.g., '2.4.49', '10.0'). Use 'unknown' if version is not specified.",
|
||||
},
|
||||
},
|
||||
"required": []string{"technology"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "function",
|
||||
Function: ToolFunction{
|
||||
Name: "check_security_headers",
|
||||
Description: "Analyzes HTTP security headers and returns recommendations for missing or misconfigured headers.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"headers": map[string]interface{}{
|
||||
"type": "object",
|
||||
"description": "HTTP response headers as key-value pairs",
|
||||
},
|
||||
},
|
||||
"required": []string{"headers"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "function",
|
||||
Function: ToolFunction{
|
||||
Name: "analyze_javascript",
|
||||
Description: "Analyzes JavaScript code for potential security issues like hardcoded secrets, eval usage, or suspicious patterns.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"code": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "JavaScript code snippet to analyze",
|
||||
},
|
||||
"url": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "The URL where the JavaScript was found",
|
||||
},
|
||||
},
|
||||
"required": []string{"code"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteTool executes a tool call and returns the result
|
||||
func ExecuteTool(toolCall ToolCall) (string, error) {
|
||||
switch toolCall.Function.Name {
|
||||
case "search_cve":
|
||||
var args struct {
|
||||
Technology string `json:"technology"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal(toolCall.Function.Arguments, &args); err != nil {
|
||||
return "", fmt.Errorf("failed to parse arguments: %w", err)
|
||||
}
|
||||
return SearchCVE(args.Technology, args.Version)
|
||||
|
||||
case "check_security_headers":
|
||||
var args struct {
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
if err := json.Unmarshal(toolCall.Function.Arguments, &args); err != nil {
|
||||
return "", fmt.Errorf("failed to parse arguments: %w", err)
|
||||
}
|
||||
return CheckSecurityHeaders(args.Headers)
|
||||
|
||||
case "analyze_javascript":
|
||||
var args struct {
|
||||
Code string `json:"code"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
if err := json.Unmarshal(toolCall.Function.Arguments, &args); err != nil {
|
||||
return "", fmt.Errorf("failed to parse arguments: %w", err)
|
||||
}
|
||||
return AnalyzeJavaScript(args.Code, args.URL)
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unknown tool: %s", toolCall.Function.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckSecurityHeaders analyzes HTTP headers for security issues
|
||||
func CheckSecurityHeaders(headers map[string]string) (string, error) {
|
||||
var issues []string
|
||||
var recommendations []string
|
||||
|
||||
// Check for important security headers
|
||||
if _, ok := headers["Strict-Transport-Security"]; !ok {
|
||||
issues = append(issues, "Missing HSTS header")
|
||||
recommendations = append(recommendations, "Add 'Strict-Transport-Security: max-age=31536000; includeSubDomains'")
|
||||
}
|
||||
|
||||
if _, ok := headers["X-Content-Type-Options"]; !ok {
|
||||
issues = append(issues, "Missing X-Content-Type-Options header")
|
||||
recommendations = append(recommendations, "Add 'X-Content-Type-Options: nosniff'")
|
||||
}
|
||||
|
||||
if _, ok := headers["X-Frame-Options"]; !ok {
|
||||
issues = append(issues, "Missing X-Frame-Options header")
|
||||
recommendations = append(recommendations, "Add 'X-Frame-Options: DENY' or 'SAMEORIGIN'")
|
||||
}
|
||||
|
||||
if csp, ok := headers["Content-Security-Policy"]; !ok {
|
||||
issues = append(issues, "Missing Content-Security-Policy header")
|
||||
recommendations = append(recommendations, "Add CSP header to prevent XSS attacks")
|
||||
} else if csp == "" {
|
||||
issues = append(issues, "Empty Content-Security-Policy header")
|
||||
}
|
||||
|
||||
if xss, ok := headers["X-XSS-Protection"]; ok && xss == "0" {
|
||||
issues = append(issues, "X-XSS-Protection is disabled")
|
||||
recommendations = append(recommendations, "Enable XSS protection: '1; mode=block'")
|
||||
}
|
||||
|
||||
// Check for information disclosure
|
||||
if server, ok := headers["Server"]; ok {
|
||||
issues = append(issues, fmt.Sprintf("Server header exposes technology: %s", server))
|
||||
recommendations = append(recommendations, "Remove or obfuscate Server header")
|
||||
}
|
||||
|
||||
if xPowered, ok := headers["X-Powered-By"]; ok {
|
||||
issues = append(issues, fmt.Sprintf("X-Powered-By header exposes technology: %s", xPowered))
|
||||
recommendations = append(recommendations, "Remove X-Powered-By header")
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Security Headers Analysis:\n\nIssues Found (%d):\n", len(issues))
|
||||
for i, issue := range issues {
|
||||
result += fmt.Sprintf("%d. %s\n", i+1, issue)
|
||||
}
|
||||
|
||||
if len(recommendations) > 0 {
|
||||
result += fmt.Sprintf("\nRecommendations (%d):\n", len(recommendations))
|
||||
for i, rec := range recommendations {
|
||||
result += fmt.Sprintf("%d. %s\n", i+1, rec)
|
||||
}
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
result = "Security headers look good! No major issues found."
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AnalyzeJavaScript performs basic security analysis on JavaScript code
|
||||
func AnalyzeJavaScript(code string, url string) (string, error) {
|
||||
var findings []string
|
||||
|
||||
// Simple pattern matching for security issues
|
||||
patterns := map[string]string{
|
||||
"eval(": "Usage of eval() - can lead to code injection",
|
||||
"innerHTML": "Usage of innerHTML - potential XSS vulnerability",
|
||||
"document.write": "Usage of document.write - can be dangerous",
|
||||
"api_key": "Potential hardcoded API key",
|
||||
"apikey": "Potential hardcoded API key",
|
||||
"password": "Potential hardcoded password",
|
||||
"secret": "Potential hardcoded secret",
|
||||
"token": "Potential hardcoded token",
|
||||
"access_token": "Potential hardcoded access token",
|
||||
"AKIA": "Potential AWS access key",
|
||||
"Bearer ": "Potential hardcoded bearer token",
|
||||
"crypto.createCipheriv": "Cryptographic operations - review implementation",
|
||||
"Math.random()": "Math.random() is not cryptographically secure",
|
||||
"localStorage.setItem": "Data stored in localStorage - ensure no sensitive data",
|
||||
"sessionStorage.setItem": "Data stored in sessionStorage - ensure no sensitive data",
|
||||
"XMLHttpRequest": "Legacy XMLHttpRequest - consider using fetch API",
|
||||
"dangerouslySetInnerHTML": "React dangerouslySetInnerHTML - XSS risk",
|
||||
}
|
||||
|
||||
for pattern, description := range patterns {
|
||||
if contains(code, pattern) {
|
||||
findings = append(findings, fmt.Sprintf("⚠️ %s", description))
|
||||
}
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("JavaScript Security Analysis for %s:\n\n", url)
|
||||
|
||||
if len(findings) == 0 {
|
||||
result += "No obvious security issues detected in this code snippet."
|
||||
} else {
|
||||
result += fmt.Sprintf("Found %d potential security issues:\n", len(findings))
|
||||
for i, finding := range findings {
|
||||
result += fmt.Sprintf("%d. %s\n", i+1, finding)
|
||||
}
|
||||
result += "\nNote: These are automated findings. Manual review is recommended."
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// contains checks if a string contains a substring (case-insensitive for simplicity)
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsAt(s, substr, 0))
|
||||
}
|
||||
|
||||
func containsAt(s, substr string, start int) bool {
|
||||
for i := start; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config holds the scan configuration
|
||||
type Config struct {
|
||||
Domain string
|
||||
Wordlist string
|
||||
Concurrency int
|
||||
Timeout int
|
||||
Output string
|
||||
Format string
|
||||
Silent bool
|
||||
Verbose bool
|
||||
NoBrute bool
|
||||
NoProbe bool
|
||||
NoPorts bool
|
||||
NoTakeover bool
|
||||
Resolvers string
|
||||
Ports string
|
||||
OnlyActive bool
|
||||
JsonOutput bool
|
||||
// AI Configuration
|
||||
EnableAI bool
|
||||
AIUrl string
|
||||
AIFastModel string
|
||||
AIDeepModel string
|
||||
AICascade bool
|
||||
AIDeepAnalysis bool
|
||||
}
|
||||
|
||||
// Stats holds scan statistics
|
||||
type Stats struct {
|
||||
TotalFound int32
|
||||
TotalResolved int32
|
||||
TotalActive int32
|
||||
TakeoverFound int32
|
||||
StartTime time.Time
|
||||
}
|
||||
|
||||
// SubdomainResult holds all information about a subdomain
|
||||
type SubdomainResult struct {
|
||||
Subdomain string `json:"subdomain"`
|
||||
IPs []string `json:"ips,omitempty"`
|
||||
CNAME string `json:"cname,omitempty"`
|
||||
PTR string `json:"ptr,omitempty"`
|
||||
ASN string `json:"asn,omitempty"`
|
||||
Org string `json:"org,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
StatusCode int `json:"status_code,omitempty"`
|
||||
ContentLength int64 `json:"content_length,omitempty"`
|
||||
RedirectURL string `json:"redirect_url,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Server string `json:"server,omitempty"`
|
||||
Tech []string `json:"technologies,omitempty"`
|
||||
Headers []string `json:"headers,omitempty"`
|
||||
WAF string `json:"waf,omitempty"`
|
||||
TLSVersion string `json:"tls_version,omitempty"`
|
||||
TLSIssuer string `json:"tls_issuer,omitempty"`
|
||||
TLSExpiry string `json:"tls_expiry,omitempty"`
|
||||
Ports []int `json:"ports,omitempty"`
|
||||
Takeover string `json:"takeover,omitempty"`
|
||||
ResponseMs int64 `json:"response_ms,omitempty"`
|
||||
FaviconHash string `json:"favicon_hash,omitempty"`
|
||||
RobotsTxt bool `json:"robots_txt,omitempty"`
|
||||
SitemapXml bool `json:"sitemap_xml,omitempty"`
|
||||
MXRecords []string `json:"mx_records,omitempty"`
|
||||
TXTRecords []string `json:"txt_records,omitempty"`
|
||||
NSRecords []string `json:"ns_records,omitempty"`
|
||||
// Security checks
|
||||
SecurityHeaders []string `json:"security_headers,omitempty"`
|
||||
MissingHeaders []string `json:"missing_headers,omitempty"`
|
||||
OpenRedirect bool `json:"open_redirect,omitempty"`
|
||||
CORSMisconfig string `json:"cors_misconfig,omitempty"`
|
||||
AllowedMethods []string `json:"allowed_methods,omitempty"`
|
||||
DangerousMethods []string `json:"dangerous_methods,omitempty"`
|
||||
// Discovery checks
|
||||
AdminPanels []string `json:"admin_panels,omitempty"`
|
||||
GitExposed bool `json:"git_exposed,omitempty"`
|
||||
SvnExposed bool `json:"svn_exposed,omitempty"`
|
||||
BackupFiles []string `json:"backup_files,omitempty"`
|
||||
APIEndpoints []string `json:"api_endpoints,omitempty"`
|
||||
// Cloud and Email Security
|
||||
CloudProvider string `json:"cloud_provider,omitempty"`
|
||||
S3Buckets []string `json:"s3_buckets,omitempty"`
|
||||
SPFRecord string `json:"spf_record,omitempty"`
|
||||
DMARCRecord string `json:"dmarc_record,omitempty"`
|
||||
EmailSecurity string `json:"email_security,omitempty"`
|
||||
TLSAltNames []string `json:"tls_alt_names,omitempty"`
|
||||
// JavaScript Analysis
|
||||
JSFiles []string `json:"js_files,omitempty"`
|
||||
JSSecrets []string `json:"js_secrets,omitempty"`
|
||||
// AI Analysis
|
||||
AIFindings []string `json:"ai_findings,omitempty"`
|
||||
AISeverity string `json:"ai_severity,omitempty"`
|
||||
AIModel string `json:"ai_model,omitempty"`
|
||||
CVEFindings []string `json:"cve_findings,omitempty"`
|
||||
}
|
||||
|
||||
// IPInfo holds IP geolocation data
|
||||
type IPInfo struct {
|
||||
ASN string `json:"as"`
|
||||
Org string `json:"org"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
// SourceResult holds passive source results
|
||||
type SourceResult struct {
|
||||
Name string
|
||||
Subs []string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Default values
|
||||
var DefaultResolvers = []string{
|
||||
"8.8.8.8:53",
|
||||
"8.8.4.4:53",
|
||||
"1.1.1.1:53",
|
||||
"1.0.0.1:53",
|
||||
"9.9.9.9:53",
|
||||
}
|
||||
|
||||
var DefaultWordlist = []string{
|
||||
"www", "mail", "ftp", "localhost", "webmail", "smtp", "pop", "ns1", "ns2",
|
||||
"ns3", "ns4", "dns", "dns1", "dns2", "api", "dev", "staging", "prod",
|
||||
"admin", "administrator", "app", "apps", "auth", "beta", "blog", "cdn",
|
||||
"chat", "cloud", "cms", "cpanel", "dashboard", "db", "demo", "docs",
|
||||
"email", "forum", "git", "gitlab", "help", "home", "host", "img",
|
||||
"images", "imap", "internal", "intranet", "jenkins", "jira", "lab",
|
||||
"legacy", "login", "m", "mobile", "monitor", "mx", "mysql", "new",
|
||||
"news", "old", "panel", "portal", "preview", "private", "proxy", "remote",
|
||||
"server", "shop", "smtp", "sql", "ssh", "ssl", "stage", "staging",
|
||||
"static", "status", "store", "support", "test", "testing", "tools",
|
||||
"vpn", "web", "webmail", "wiki", "www1", "www2", "www3",
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"god-eye/internal/config"
|
||||
)
|
||||
|
||||
func ResolveSubdomain(subdomain string, resolvers []string, timeout int) []string {
|
||||
c := dns.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(subdomain), dns.TypeA)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var ips []string
|
||||
for _, ans := range r.Answer {
|
||||
if a, ok := ans.(*dns.A); ok {
|
||||
ips = append(ips, a.A.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
return ips
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckWildcard(domain string, resolvers []string) []string {
|
||||
// Test multiple random patterns for better wildcard detection
|
||||
patterns := []string{
|
||||
fmt.Sprintf("random%d.%s", time.Now().UnixNano(), domain),
|
||||
fmt.Sprintf("xyz%d.%s", time.Now().UnixNano()%1000000, domain),
|
||||
fmt.Sprintf("nonexistent-%s.%s", "abc123xyz", domain),
|
||||
}
|
||||
|
||||
allIPs := make(map[string]int)
|
||||
for _, pattern := range patterns {
|
||||
ips := ResolveSubdomain(pattern, resolvers, 3)
|
||||
for _, ip := range ips {
|
||||
allIPs[ip]++
|
||||
}
|
||||
}
|
||||
|
||||
// If same IP(s) appear in multiple patterns, it's a wildcard
|
||||
var wildcardIPs []string
|
||||
for ip, count := range allIPs {
|
||||
if count >= 2 {
|
||||
wildcardIPs = append(wildcardIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return wildcardIPs
|
||||
}
|
||||
|
||||
func ResolveCNAME(subdomain string, resolvers []string, timeout int) string {
|
||||
c := dns.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(subdomain), dns.TypeCNAME)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ans := range r.Answer {
|
||||
if cname, ok := ans.(*dns.CNAME); ok {
|
||||
return strings.TrimSuffix(cname.Target, ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func ResolvePTR(ip string, resolvers []string, timeout int) string {
|
||||
c := dns.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
// Convert IP to reverse DNS format
|
||||
parts := strings.Split(ip, ".")
|
||||
if len(parts) != 4 {
|
||||
return ""
|
||||
}
|
||||
reverseIP := fmt.Sprintf("%s.%s.%s.%s.in-addr.arpa.", parts[3], parts[2], parts[1], parts[0])
|
||||
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(reverseIP, dns.TypePTR)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ans := range r.Answer {
|
||||
if ptr, ok := ans.(*dns.PTR); ok {
|
||||
return strings.TrimSuffix(ptr.Ptr, ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func ResolveMX(domain string, resolvers []string, timeout int) []string {
|
||||
c := dns.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeMX)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var records []string
|
||||
for _, ans := range r.Answer {
|
||||
if mx, ok := ans.(*dns.MX); ok {
|
||||
records = append(records, strings.TrimSuffix(mx.Mx, "."))
|
||||
}
|
||||
}
|
||||
if len(records) > 0 {
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResolveTXT(domain string, resolvers []string, timeout int) []string {
|
||||
c := dns.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var records []string
|
||||
for _, ans := range r.Answer {
|
||||
if txt, ok := ans.(*dns.TXT); ok {
|
||||
for _, t := range txt.Txt {
|
||||
// Limit length for display
|
||||
if len(t) > 100 {
|
||||
t = t[:97] + "..."
|
||||
}
|
||||
records = append(records, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(records) > 0 {
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResolveNS(domain string, resolvers []string, timeout int) []string {
|
||||
c := dns.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeNS)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var records []string
|
||||
for _, ans := range r.Answer {
|
||||
if ns, ok := ans.(*dns.NS); ok {
|
||||
records = append(records, strings.TrimSuffix(ns.Ns, "."))
|
||||
}
|
||||
}
|
||||
if len(records) > 0 {
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetIPInfo(ip string) (*config.IPInfo, error) {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=as,org,country,city", ip)
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var info config.IPInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SharedTransport is a global shared HTTP transport for connection pooling
|
||||
var SharedTransport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
DisableCompression: true, // Keep Content-Length header for SPA detection
|
||||
}
|
||||
|
||||
// GetSharedClient returns an HTTP client with connection pooling
|
||||
func GetSharedClient(timeout int) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: SharedTransport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"god-eye/internal/config"
|
||||
)
|
||||
|
||||
func ProbeHTTP(subdomain string, timeout int) *config.SubdomainResult {
|
||||
result := &config.SubdomainResult{}
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
start := time.Now()
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result.StatusCode = resp.StatusCode
|
||||
result.ResponseMs = time.Since(start).Milliseconds()
|
||||
|
||||
// Content-Length
|
||||
if cl := resp.ContentLength; cl > 0 {
|
||||
result.ContentLength = cl
|
||||
}
|
||||
|
||||
// Redirect location
|
||||
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||
if loc := resp.Header.Get("Location"); loc != "" {
|
||||
result.RedirectURL = loc
|
||||
}
|
||||
}
|
||||
|
||||
// Server header
|
||||
if server := resp.Header.Get("Server"); server != "" {
|
||||
result.Server = server
|
||||
result.Tech = append(result.Tech, server)
|
||||
}
|
||||
|
||||
// TLS/SSL info
|
||||
if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
|
||||
cert := resp.TLS.PeerCertificates[0]
|
||||
result.TLSIssuer = cert.Issuer.CommonName
|
||||
result.TLSExpiry = cert.NotAfter.Format("2006-01-02")
|
||||
|
||||
// TLS version
|
||||
switch resp.TLS.Version {
|
||||
case tls.VersionTLS13:
|
||||
result.TLSVersion = "TLS 1.3"
|
||||
case tls.VersionTLS12:
|
||||
result.TLSVersion = "TLS 1.2"
|
||||
case tls.VersionTLS11:
|
||||
result.TLSVersion = "TLS 1.1"
|
||||
case tls.VersionTLS10:
|
||||
result.TLSVersion = "TLS 1.0"
|
||||
}
|
||||
}
|
||||
|
||||
// Interesting headers
|
||||
interestingHeaders := []string{
|
||||
"X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version",
|
||||
"X-Generator", "X-Drupal-Cache", "X-Varnish",
|
||||
"X-Cache", "X-Backend-Server", "X-Server",
|
||||
}
|
||||
for _, h := range interestingHeaders {
|
||||
if val := resp.Header.Get(h); val != "" {
|
||||
result.Headers = append(result.Headers, fmt.Sprintf("%s: %s", h, val))
|
||||
if h == "X-Powered-By" {
|
||||
result.Tech = append(result.Tech, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WAF detection
|
||||
result.WAF = DetectWAF(resp)
|
||||
|
||||
// Security headers check
|
||||
result.SecurityHeaders, result.MissingHeaders = CheckSecurityHeaders(resp)
|
||||
|
||||
// Read body for title and tech detection
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 100000))
|
||||
if err == nil {
|
||||
// Content-Length from body if not set
|
||||
if result.ContentLength == 0 {
|
||||
result.ContentLength = int64(len(body))
|
||||
}
|
||||
|
||||
// Extract title
|
||||
titleRe := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
|
||||
if matches := titleRe.FindSubmatch(body); len(matches) > 1 {
|
||||
result.Title = strings.TrimSpace(string(matches[1]))
|
||||
}
|
||||
|
||||
// Detect technologies
|
||||
bodyStr := string(body)
|
||||
if strings.Contains(bodyStr, "wp-content") || strings.Contains(bodyStr, "wordpress") {
|
||||
result.Tech = append(result.Tech, "WordPress")
|
||||
}
|
||||
if strings.Contains(bodyStr, "_next") || strings.Contains(bodyStr, "Next.js") {
|
||||
result.Tech = append(result.Tech, "Next.js")
|
||||
}
|
||||
if strings.Contains(bodyStr, "react") || strings.Contains(bodyStr, "React") {
|
||||
result.Tech = append(result.Tech, "React")
|
||||
}
|
||||
if strings.Contains(bodyStr, "laravel") || strings.Contains(bodyStr, "Laravel") {
|
||||
result.Tech = append(result.Tech, "Laravel")
|
||||
}
|
||||
if strings.Contains(bodyStr, "django") || strings.Contains(bodyStr, "Django") {
|
||||
result.Tech = append(result.Tech, "Django")
|
||||
}
|
||||
if strings.Contains(bodyStr, "angular") || strings.Contains(bodyStr, "ng-") {
|
||||
result.Tech = append(result.Tech, "Angular")
|
||||
}
|
||||
if strings.Contains(bodyStr, "vue") || strings.Contains(bodyStr, "Vue.js") {
|
||||
result.Tech = append(result.Tech, "Vue.js")
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func DetectWAF(resp *http.Response) string {
|
||||
// Check headers for WAF signatures
|
||||
serverHeader := strings.ToLower(resp.Header.Get("Server"))
|
||||
|
||||
// Cloudflare
|
||||
if resp.Header.Get("CF-RAY") != "" || strings.Contains(serverHeader, "cloudflare") {
|
||||
return "Cloudflare"
|
||||
}
|
||||
|
||||
// AWS WAF/CloudFront
|
||||
if resp.Header.Get("X-Amz-Cf-Id") != "" || resp.Header.Get("X-Amz-Cf-Pop") != "" {
|
||||
return "AWS CloudFront"
|
||||
}
|
||||
|
||||
// Akamai
|
||||
if resp.Header.Get("X-Akamai-Transformed") != "" || strings.Contains(serverHeader, "akamai") {
|
||||
return "Akamai"
|
||||
}
|
||||
|
||||
// Sucuri
|
||||
if resp.Header.Get("X-Sucuri-ID") != "" || strings.Contains(serverHeader, "sucuri") {
|
||||
return "Sucuri"
|
||||
}
|
||||
|
||||
// Imperva/Incapsula
|
||||
if resp.Header.Get("X-Iinfo") != "" || resp.Header.Get("X-CDN") == "Incapsula" {
|
||||
return "Imperva"
|
||||
}
|
||||
|
||||
// F5 BIG-IP
|
||||
if strings.Contains(serverHeader, "big-ip") || resp.Header.Get("X-WA-Info") != "" {
|
||||
return "F5 BIG-IP"
|
||||
}
|
||||
|
||||
// Barracuda
|
||||
if strings.Contains(serverHeader, "barracuda") {
|
||||
return "Barracuda"
|
||||
}
|
||||
|
||||
// Fastly
|
||||
if resp.Header.Get("X-Fastly-Request-ID") != "" || resp.Header.Get("Fastly-Debug-Digest") != "" {
|
||||
return "Fastly"
|
||||
}
|
||||
|
||||
// Varnish
|
||||
if resp.Header.Get("X-Varnish") != "" {
|
||||
return "Varnish"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func CheckSecurityHeaders(resp *http.Response) (present []string, missing []string) {
|
||||
securityHeaders := map[string]string{
|
||||
"Content-Security-Policy": "CSP",
|
||||
"X-Frame-Options": "X-Frame",
|
||||
"X-Content-Type-Options": "X-Content-Type",
|
||||
"Strict-Transport-Security": "HSTS",
|
||||
"X-XSS-Protection": "X-XSS",
|
||||
"Referrer-Policy": "Referrer",
|
||||
"Permissions-Policy": "Permissions",
|
||||
}
|
||||
|
||||
for header, shortName := range securityHeaders {
|
||||
if val := resp.Header.Get(header); val != "" {
|
||||
present = append(present, shortName)
|
||||
} else {
|
||||
missing = append(missing, shortName)
|
||||
}
|
||||
}
|
||||
|
||||
return present, missing
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"god-eye/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
// Basic colors
|
||||
Green = color.New(color.FgGreen).SprintFunc()
|
||||
Red = color.New(color.FgRed).SprintFunc()
|
||||
Blue = color.New(color.FgBlue).SprintFunc()
|
||||
Yellow = color.New(color.FgYellow).SprintFunc()
|
||||
Cyan = color.New(color.FgCyan).SprintFunc()
|
||||
Magenta = color.New(color.FgMagenta).SprintFunc()
|
||||
White = color.New(color.FgWhite).SprintFunc()
|
||||
|
||||
// Bold variants
|
||||
BoldGreen = color.New(color.FgGreen, color.Bold).SprintFunc()
|
||||
BoldRed = color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
BoldCyan = color.New(color.FgCyan, color.Bold).SprintFunc()
|
||||
BoldYellow = color.New(color.FgYellow, color.Bold).SprintFunc()
|
||||
BoldMagenta = color.New(color.FgMagenta, color.Bold).SprintFunc()
|
||||
BoldWhite = color.New(color.FgWhite, color.Bold).SprintFunc()
|
||||
|
||||
// Dim/faint
|
||||
Dim = color.New(color.Faint).SprintFunc()
|
||||
|
||||
// Background highlights
|
||||
BgRed = color.New(color.BgRed, color.FgWhite, color.Bold).SprintFunc()
|
||||
BgGreen = color.New(color.BgGreen, color.FgBlack, color.Bold).SprintFunc()
|
||||
BgYellow = color.New(color.BgYellow, color.FgBlack, color.Bold).SprintFunc()
|
||||
)
|
||||
|
||||
func PrintBanner() {
|
||||
fmt.Println()
|
||||
fmt.Println(BoldCyan(" ██████╗ ██████╗ ██████╗ ") + BoldWhite("███████╗") + BoldCyan(" ███████╗██╗ ██╗███████╗"))
|
||||
fmt.Println(BoldCyan(" ██╔════╝ ██╔═══██╗██╔══██╗") + BoldWhite("██╔════╝") + BoldCyan(" ██╔════╝╚██╗ ██╔╝██╔════╝"))
|
||||
fmt.Println(BoldCyan(" ██║ ███╗██║ ██║██║ ██║") + BoldWhite("███████╗") + BoldCyan(" █████╗ ╚████╔╝ █████╗ "))
|
||||
fmt.Println(BoldCyan(" ██║ ██║██║ ██║██║ ██║") + BoldWhite("╚════██║") + BoldCyan(" ██╔══╝ ╚██╔╝ ██╔══╝ "))
|
||||
fmt.Println(BoldCyan(" ╚██████╔╝╚██████╔╝██████╔╝") + BoldWhite("███████║") + BoldCyan(" ███████╗ ██║ ███████╗"))
|
||||
fmt.Println(BoldCyan(" ╚═════╝ ╚═════╝ ╚═════╝ ") + BoldWhite("╚══════╝") + BoldCyan(" ╚══════╝ ╚═╝ ╚══════╝"))
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s %s\n", BoldWhite("⚡"), Dim("Ultra-fast subdomain enumeration & reconnaissance"))
|
||||
fmt.Printf(" %s %s %s %s %s %s\n",
|
||||
Dim("Version:"), BoldGreen("0.1"),
|
||||
Dim("By:"), Cyan("github.com/Vyntral"),
|
||||
Dim("For:"), Yellow("github.com/Orizon-eu"))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func PrintSection(icon, title string) {
|
||||
fmt.Printf("\n%s %s %s\n", BoldCyan("┌──"), BoldWhite(icon+" "+title), BoldCyan(strings.Repeat("─", 50)))
|
||||
}
|
||||
|
||||
func PrintSubSection(text string) {
|
||||
fmt.Printf("%s %s\n", Cyan("│"), text)
|
||||
}
|
||||
|
||||
func PrintEndSection() {
|
||||
fmt.Printf("%s\n", BoldCyan("└"+strings.Repeat("─", 60)))
|
||||
}
|
||||
|
||||
func PrintProgress(current, total int, label string) {
|
||||
width := 30
|
||||
filled := int(float64(current) / float64(total) * float64(width))
|
||||
if filled > width {
|
||||
filled = width
|
||||
}
|
||||
bar := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
|
||||
percent := float64(current) / float64(total) * 100
|
||||
fmt.Printf("\r%s %s %s %s %.0f%% ", Cyan("│"), label, BoldGreen(bar), Dim(fmt.Sprintf("(%d/%d)", current, total)), percent)
|
||||
}
|
||||
|
||||
func ClearLine() {
|
||||
fmt.Print("\r\033[K")
|
||||
}
|
||||
|
||||
func SaveOutput(path string, format string, results map[string]*config.SubdomainResult) {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
fmt.Printf("%s Failed to create output file: %v\n", Red("[-]"), err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Sort subdomains for consistent output
|
||||
var sortedSubs []string
|
||||
for sub := range results {
|
||||
sortedSubs = append(sortedSubs, sub)
|
||||
}
|
||||
sort.Strings(sortedSubs)
|
||||
|
||||
switch format {
|
||||
case "json":
|
||||
var resultList []*config.SubdomainResult
|
||||
for _, sub := range sortedSubs {
|
||||
resultList = append(resultList, results[sub])
|
||||
}
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(resultList)
|
||||
|
||||
case "csv":
|
||||
writer := csv.NewWriter(file)
|
||||
// Header
|
||||
writer.Write([]string{"subdomain", "ips", "status_code", "title", "server", "technologies", "ports", "takeover", "response_ms"})
|
||||
|
||||
for _, sub := range sortedSubs {
|
||||
r := results[sub]
|
||||
var portStrs []string
|
||||
for _, p := range r.Ports {
|
||||
portStrs = append(portStrs, strconv.Itoa(p))
|
||||
}
|
||||
writer.Write([]string{
|
||||
r.Subdomain,
|
||||
strings.Join(r.IPs, ";"),
|
||||
strconv.Itoa(r.StatusCode),
|
||||
r.Title,
|
||||
r.Server,
|
||||
strings.Join(r.Tech, ";"),
|
||||
strings.Join(portStrs, ";"),
|
||||
r.Takeover,
|
||||
strconv.FormatInt(r.ResponseMs, 10),
|
||||
})
|
||||
}
|
||||
writer.Flush()
|
||||
|
||||
default: // txt
|
||||
for _, sub := range sortedSubs {
|
||||
file.WriteString(sub + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%s Results saved to %s\n", Green("[+]"), path)
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DetectCloudProvider detects cloud provider based on IP/CNAME
|
||||
func DetectCloudProvider(ips []string, cname string, asn string) string {
|
||||
// Check CNAME patterns
|
||||
cnamePatterns := map[string]string{
|
||||
"amazonaws.com": "AWS",
|
||||
"aws.com": "AWS",
|
||||
"cloudfront.net": "AWS CloudFront",
|
||||
"elasticbeanstalk.com": "AWS Elastic Beanstalk",
|
||||
"elb.amazonaws.com": "AWS ELB",
|
||||
"s3.amazonaws.com": "AWS S3",
|
||||
"azure.com": "Azure",
|
||||
"azurewebsites.net": "Azure App Service",
|
||||
"cloudapp.net": "Azure",
|
||||
"azurefd.net": "Azure Front Door",
|
||||
"blob.core.windows.net": "Azure Blob",
|
||||
"googleapis.com": "Google Cloud",
|
||||
"appspot.com": "Google App Engine",
|
||||
"storage.googleapis.com": "Google Cloud Storage",
|
||||
"digitaloceanspaces.com": "DigitalOcean Spaces",
|
||||
"ondigitalocean.app": "DigitalOcean App Platform",
|
||||
"cloudflare.com": "Cloudflare",
|
||||
"fastly.net": "Fastly",
|
||||
"akamai.net": "Akamai",
|
||||
"netlify.app": "Netlify",
|
||||
"vercel.app": "Vercel",
|
||||
"herokuapp.com": "Heroku",
|
||||
}
|
||||
|
||||
for pattern, provider := range cnamePatterns {
|
||||
if strings.Contains(cname, pattern) {
|
||||
return provider
|
||||
}
|
||||
}
|
||||
|
||||
// Check ASN patterns
|
||||
asnPatterns := map[string]string{
|
||||
"AS14618": "AWS",
|
||||
"AS16509": "AWS",
|
||||
"AS8075": "Azure",
|
||||
"AS15169": "Google Cloud",
|
||||
"AS14061": "DigitalOcean",
|
||||
"AS13335": "Cloudflare",
|
||||
"AS54113": "Fastly",
|
||||
"AS20940": "Akamai",
|
||||
}
|
||||
|
||||
for pattern, provider := range asnPatterns {
|
||||
if strings.Contains(asn, pattern) {
|
||||
return provider
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CheckS3Buckets checks for exposed S3 buckets
|
||||
func CheckS3Buckets(subdomain string, timeout int) []string {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
// Common S3 bucket URL patterns
|
||||
parts := strings.Split(subdomain, ".")
|
||||
bucketName := parts[0]
|
||||
|
||||
patterns := []string{
|
||||
fmt.Sprintf("https://%s.s3.amazonaws.com", bucketName),
|
||||
fmt.Sprintf("https://s3.amazonaws.com/%s", bucketName),
|
||||
fmt.Sprintf("https://%s.s3.us-east-1.amazonaws.com", bucketName),
|
||||
fmt.Sprintf("https://%s.s3.us-west-2.amazonaws.com", bucketName),
|
||||
fmt.Sprintf("https://%s.s3.eu-west-1.amazonaws.com", bucketName),
|
||||
}
|
||||
|
||||
var found []string
|
||||
for _, url := range patterns {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Public bucket if 200 or 403 (exists but forbidden)
|
||||
if resp.StatusCode == 200 {
|
||||
found = append(found, url+" (PUBLIC)")
|
||||
} else if resp.StatusCode == 403 {
|
||||
found = append(found, url+" (exists)")
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// CheckEmailSecurity checks SPF/DKIM/DMARC records
|
||||
func CheckEmailSecurity(domain string, resolvers []string, timeout int) (spf string, dmarc string, security string) {
|
||||
c := dns.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
|
||||
// Check SPF record
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ans := range r.Answer {
|
||||
if txt, ok := ans.(*dns.TXT); ok {
|
||||
for _, t := range txt.Txt {
|
||||
if strings.HasPrefix(t, "v=spf1") {
|
||||
spf = t
|
||||
if len(spf) > 80 {
|
||||
spf = spf[:77] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if spf != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check DMARC record
|
||||
m2 := dns.Msg{}
|
||||
m2.SetQuestion(dns.Fqdn("_dmarc."+domain), dns.TypeTXT)
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
r, _, err := c.Exchange(&m2, resolver)
|
||||
if err != nil || r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ans := range r.Answer {
|
||||
if txt, ok := ans.(*dns.TXT); ok {
|
||||
for _, t := range txt.Txt {
|
||||
if strings.HasPrefix(t, "v=DMARC1") {
|
||||
dmarc = t
|
||||
if len(dmarc) > 80 {
|
||||
dmarc = dmarc[:77] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if dmarc != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Determine email security level
|
||||
if spf != "" && dmarc != "" {
|
||||
if strings.Contains(dmarc, "p=reject") || strings.Contains(dmarc, "p=quarantine") {
|
||||
security = "Strong"
|
||||
} else {
|
||||
security = "Moderate"
|
||||
}
|
||||
} else if spf != "" || dmarc != "" {
|
||||
security = "Weak"
|
||||
} else {
|
||||
security = "None"
|
||||
}
|
||||
|
||||
return spf, dmarc, security
|
||||
}
|
||||
|
||||
// GetTLSAltNames extracts Subject Alternative Names from TLS certificate
|
||||
func GetTLSAltNames(subdomain string, timeout int) []string {
|
||||
conn, err := tls.DialWithDialer(
|
||||
&net.Dialer{Timeout: time.Duration(timeout) * time.Second},
|
||||
"tcp",
|
||||
subdomain+":443",
|
||||
&tls.Config{InsecureSkipVerify: true},
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
certs := conn.ConnectionState().PeerCertificates
|
||||
if len(certs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var altNames []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, cert := range certs {
|
||||
for _, name := range cert.DNSNames {
|
||||
if !seen[name] && name != subdomain {
|
||||
seen[name] = true
|
||||
altNames = append(altNames, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit to first 10
|
||||
if len(altNames) > 10 {
|
||||
altNames = altNames[:10]
|
||||
}
|
||||
|
||||
return altNames
|
||||
}
|
||||
|
||||
// CheckS3BucketsWithClient checks for exposed S3 buckets with shared client
|
||||
func CheckS3BucketsWithClient(subdomain string, client *http.Client) []string {
|
||||
parts := strings.Split(subdomain, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
subPrefix := parts[0]
|
||||
// Get domain name (e.g., "finnat" from "ftp.finnat.it")
|
||||
var domainName string
|
||||
if len(parts) >= 2 {
|
||||
domainName = parts[len(parts)-2]
|
||||
}
|
||||
|
||||
// Skip generic subdomain names that cause false positives
|
||||
genericNames := map[string]bool{
|
||||
"www": true, "ftp": true, "mail": true, "smtp": true, "imap": true,
|
||||
"pop": true, "webmail": true, "autodiscover": true, "test": true,
|
||||
"dev": true, "staging": true, "api": true, "admin": true, "pop3": true,
|
||||
}
|
||||
|
||||
var patterns []string
|
||||
if genericNames[subPrefix] {
|
||||
// For generic subdomains, use domain-specific bucket names
|
||||
patterns = []string{
|
||||
fmt.Sprintf("https://%s-%s.s3.amazonaws.com", domainName, subPrefix),
|
||||
fmt.Sprintf("https://%s.s3.amazonaws.com", domainName),
|
||||
}
|
||||
} else {
|
||||
// For specific subdomains, use combination
|
||||
patterns = []string{
|
||||
fmt.Sprintf("https://%s-%s.s3.amazonaws.com", domainName, subPrefix),
|
||||
fmt.Sprintf("https://%s.s3.amazonaws.com", domainName),
|
||||
}
|
||||
}
|
||||
|
||||
var found []string
|
||||
for _, url := range patterns {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Only report PUBLIC buckets (200), not just existing (403)
|
||||
if resp.StatusCode == 200 {
|
||||
found = append(found, url+" (PUBLIC)")
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AnalyzeJSFiles finds JavaScript files and extracts potential secrets
|
||||
func AnalyzeJSFiles(subdomain string, client *http.Client) ([]string, []string) {
|
||||
var jsFiles []string
|
||||
var secrets []string
|
||||
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s", subdomain),
|
||||
fmt.Sprintf("http://%s", subdomain),
|
||||
}
|
||||
|
||||
// Common JS file paths
|
||||
jsPaths := []string{
|
||||
"/main.js", "/app.js", "/bundle.js", "/vendor.js",
|
||||
"/static/js/main.js", "/static/js/app.js",
|
||||
"/assets/js/app.js", "/js/main.js", "/js/app.js",
|
||||
"/dist/main.js", "/dist/bundle.js",
|
||||
"/_next/static/chunks/main.js",
|
||||
"/build/static/js/main.js",
|
||||
}
|
||||
|
||||
// Secret patterns to search for
|
||||
secretPatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)['"]?api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?aws[_-]?access[_-]?key[_-]?id['"]?\s*[:=]\s*['"]([A-Z0-9]{20})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?aws[_-]?secret[_-]?access[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9/+=]{40})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?google[_-]?api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{39})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?firebase[_-]?api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{39})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?stripe[_-]?(publishable|secret)[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?github[_-]?token['"]?\s*[:=]\s*['"]([a-zA-Z0-9_]{36,})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?slack[_-]?token['"]?\s*[:=]\s*['"]([a-zA-Z0-9\-]{30,})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?private[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9/+=]{50,})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?secret['"]?\s*[:=]\s*['"]([a-zA-Z0-9_\-]{20,})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?password['"]?\s*[:=]\s*['"]([^'"]{8,})['"]`),
|
||||
regexp.MustCompile(`(?i)['"]?authorization['"]?\s*[:=]\s*['"]Bearer\s+([a-zA-Z0-9_\-\.]+)['"]`),
|
||||
}
|
||||
|
||||
// Also search for API endpoints in JS
|
||||
endpointPatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)['"]https?://[a-zA-Z0-9\-\.]+/api/[a-zA-Z0-9/\-_]+['"]`),
|
||||
regexp.MustCompile(`(?i)['"]https?://api\.[a-zA-Z0-9\-\.]+[a-zA-Z0-9/\-_]*['"]`),
|
||||
}
|
||||
|
||||
for _, baseURL := range urls {
|
||||
// First, get the main page and extract JS file references
|
||||
resp, err := client.Get(baseURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 500000))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find JS files referenced in HTML
|
||||
jsRe := regexp.MustCompile(`src=["']([^"']*\.js[^"']*)["']`)
|
||||
matches := jsRe.FindAllStringSubmatch(string(body), -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
jsURL := match[1]
|
||||
if !strings.HasPrefix(jsURL, "http") {
|
||||
if strings.HasPrefix(jsURL, "/") {
|
||||
jsURL = baseURL + jsURL
|
||||
} else {
|
||||
jsURL = baseURL + "/" + jsURL
|
||||
}
|
||||
}
|
||||
jsFiles = append(jsFiles, jsURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Also check common JS paths
|
||||
for _, path := range jsPaths {
|
||||
testURL := baseURL + path
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
jsFiles = append(jsFiles, path)
|
||||
|
||||
// Read JS content and search for secrets
|
||||
jsBody, err := io.ReadAll(io.LimitReader(resp.Body, 500000))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
jsContent := string(jsBody)
|
||||
|
||||
// Search for secrets
|
||||
for _, pattern := range secretPatterns {
|
||||
if matches := pattern.FindAllStringSubmatch(jsContent, 3); len(matches) > 0 {
|
||||
for _, m := range matches {
|
||||
if len(m) > 1 {
|
||||
secret := m[0]
|
||||
if len(secret) > 60 {
|
||||
secret = secret[:57] + "..."
|
||||
}
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for API endpoints
|
||||
for _, pattern := range endpointPatterns {
|
||||
if matches := pattern.FindAllString(jsContent, 5); len(matches) > 0 {
|
||||
for _, m := range matches {
|
||||
if len(m) > 60 {
|
||||
m = m[:57] + "..."
|
||||
}
|
||||
secrets = append(secrets, "endpoint: "+m)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsFiles) > 0 || len(secrets) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and limit
|
||||
jsFiles = UniqueStrings(jsFiles)
|
||||
secrets = UniqueStrings(secrets)
|
||||
|
||||
if len(jsFiles) > 10 {
|
||||
jsFiles = jsFiles[:10]
|
||||
}
|
||||
if len(secrets) > 10 {
|
||||
secrets = secrets[:10]
|
||||
}
|
||||
|
||||
return jsFiles, secrets
|
||||
}
|
||||
|
||||
// UniqueStrings returns unique strings from a slice
|
||||
func UniqueStrings(input []string) []string {
|
||||
seen := make(map[string]bool)
|
||||
var result []string
|
||||
for _, s := range input {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,254 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var TakeoverFingerprints = map[string]string{
|
||||
// GitHub
|
||||
"github.io": "There isn't a GitHub Pages site here",
|
||||
"githubusercontent.com": "There isn't a GitHub Pages site here",
|
||||
// Heroku
|
||||
"herokuapp.com": "no-such-app.herokuapp.com",
|
||||
"herokussl.com": "no-such-app.herokuapp.com",
|
||||
// AWS
|
||||
"s3.amazonaws.com": "NoSuchBucket",
|
||||
"s3-website": "NoSuchBucket",
|
||||
"elasticbeanstalk.com": "NoSuchBucket",
|
||||
"cloudfront.net": "Bad Request",
|
||||
"elb.amazonaws.com": "NXDOMAIN",
|
||||
// Azure
|
||||
"azurewebsites.net": "404 Web Site not found",
|
||||
"cloudapp.net": "404 Web Site not found",
|
||||
"cloudapp.azure.com": "404 Web Site not found",
|
||||
"azurefd.net": "404 Web Site not found",
|
||||
"blob.core.windows.net": "BlobNotFound",
|
||||
"azure-api.net": "404 Resource not found",
|
||||
"azurehdinsight.net": "404",
|
||||
"azureedge.net": "404 Web Site not found",
|
||||
"trafficmanager.net": "404 Web Site not found",
|
||||
// Google Cloud
|
||||
"appspot.com": "Error: Not Found",
|
||||
"storage.googleapis.com": "NoSuchBucket",
|
||||
"googleplex.com": "404. That's an error",
|
||||
// Shopify
|
||||
"myshopify.com": "Sorry, this shop is currently unavailable",
|
||||
// Pantheon
|
||||
"pantheonsite.io": "404 error unknown site",
|
||||
// Zendesk
|
||||
"zendesk.com": "Help Center Closed",
|
||||
// Various services
|
||||
"teamwork.com": "Oops - We didn't find your site",
|
||||
"helpjuice.com": "We could not find what you're looking for",
|
||||
"helpscoutdocs.com": "No settings were found for this company",
|
||||
"ghost.io": "The thing you were looking for is no longer here",
|
||||
"surge.sh": "project not found",
|
||||
"bitbucket.io": "Repository not found",
|
||||
"wordpress.com": "Do you want to register",
|
||||
"smartling.com": "Domain is not configured",
|
||||
"acquia.com": "Web Site Not Found",
|
||||
"fastly.net": "Fastly error: unknown domain",
|
||||
"uservoice.com": "This UserVoice subdomain is currently available",
|
||||
"unbounce.com": "The requested URL was not found on this server",
|
||||
"thinkific.com": "You may have mistyped the address",
|
||||
"tilda.cc": "Please renew your subscription",
|
||||
"mashery.com": "Unrecognized domain",
|
||||
"intercom.help": "This page is reserved for",
|
||||
"webflow.io": "The page you are looking for doesn't exist",
|
||||
"wishpond.com": "https://www.wishpond.com/404",
|
||||
"aftership.com": "Oops.</h2><p>The page you're looking for doesn't exist",
|
||||
"aha.io": "There is no portal here",
|
||||
"tictail.com": "to target URL: <a href=\"https://tictail.com",
|
||||
"campaignmonitor.com": "Trying to access your account?",
|
||||
"cargocollective.com": "404 Not Found",
|
||||
"statuspage.io": "You are being <a href=\"https://www.statuspage.io\">",
|
||||
"tumblr.com": "There's nothing here.",
|
||||
"worksites.net": "Hello! Sorry, but the website you’re looking for doesn’t exist.",
|
||||
"smugmug.com": "class=\"message-text\">Page Not Found<",
|
||||
// Additional services
|
||||
"netlify.app": "Not Found",
|
||||
"netlify.com": "Not Found",
|
||||
"vercel.app": "NOT_FOUND",
|
||||
"now.sh": "NOT_FOUND",
|
||||
"fly.dev": "404 Not Found",
|
||||
"render.com": "NOT_FOUND",
|
||||
"gitbook.io": "Domain not found",
|
||||
"readme.io": "Project doesnt exist",
|
||||
"desk.com": "Sorry, We Couldn't Find That Page",
|
||||
"freshdesk.com": "There is no helpdesk here",
|
||||
"tave.com": "Sorry, this profile doesn't exist",
|
||||
"feedpress.me": "The feed has not been found",
|
||||
"launchrock.com": "It looks like you may have taken a wrong turn",
|
||||
"pingdom.com": "This public status page",
|
||||
"surveygizmo.com": "data-html-name",
|
||||
"tribepad.com": "Sorry, we could not find that page",
|
||||
"uptimerobot.com": "This public status page",
|
||||
"wufoo.com": "Profile not found",
|
||||
"brightcove.com": "Error - Loss of soul",
|
||||
"bigcartel.com": "Oops! We couldn't find that page",
|
||||
"activehosted.com": "alt=\"LIGHTTPD - fly light.\"",
|
||||
"createsend.com": "Double check the URL",
|
||||
"flexbe.com": "Domain doesn't exist",
|
||||
"agilecrm.com": "Sorry, this page is no longer available",
|
||||
"anima.io": "not found",
|
||||
"proposify.com": "If you need immediate assistance",
|
||||
"simplebooklet.com": "We can't find this FlipBook",
|
||||
"getresponse.com": "With GetResponse Landing Pages",
|
||||
"vend.com": "Looks like you've traveled too far",
|
||||
"strikingly.com": "But if you're looking to build your own website",
|
||||
"airee.ru": "Ошибка 402. Сервис",
|
||||
"anweb.ru": "Эта страница не существует",
|
||||
"domain.ru": "К сожалению, не удалось",
|
||||
"instapage.com": "Looks Like You're Lost",
|
||||
"landingi.com": "Nie znaleziono strony",
|
||||
"leadpages.net": "Oops - We Couldn't Find Your Page",
|
||||
"pagewiz.com": "PAGE NOT FOUND",
|
||||
"short.io": "Link does not exist",
|
||||
"smartjobboard.com": "Company Not Found",
|
||||
"uberflip.com": "Non-hub polygon detected",
|
||||
"vingle.net": "해당 페이지가 존재하지 않습니다",
|
||||
"ngrok.io": "Tunnel",
|
||||
"kinsta.cloud": "No Site For Domain",
|
||||
"canny.io": "There is no such company",
|
||||
"hatena.ne.jp": "404 Blog is not found",
|
||||
"medium.com": "This page doesn't exist",
|
||||
"hatenablog.com": "404 Blog is not found",
|
||||
"jetbrains.com": "is not a registered InCloud YouTrack",
|
||||
}
|
||||
|
||||
func CheckTakeover(subdomain string, timeout int) string {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
// Check CNAME
|
||||
c := dns.Client{Timeout: 3 * time.Second}
|
||||
m := dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(subdomain), dns.TypeCNAME)
|
||||
|
||||
r, _, err := c.Exchange(&m, "8.8.8.8:53")
|
||||
if err != nil || r == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var cname string
|
||||
for _, ans := range r.Answer {
|
||||
if cn, ok := ans.(*dns.CNAME); ok {
|
||||
cname = cn.Target
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cname == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if CNAME matches any vulnerable service
|
||||
for service, fingerprint := range TakeoverFingerprints {
|
||||
if strings.Contains(cname, service) {
|
||||
// Verify by checking response
|
||||
resp, err := client.Get(fmt.Sprintf("http://%s", subdomain))
|
||||
if err != nil {
|
||||
resp, err = client.Get(fmt.Sprintf("https://%s", subdomain))
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 100000))
|
||||
if strings.Contains(string(body), fingerprint) {
|
||||
return service
|
||||
}
|
||||
}
|
||||
|
||||
// If can't reach, might still be vulnerable
|
||||
if err != nil {
|
||||
return service + " (unverified)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Helper functions for connection pooling
|
||||
|
||||
func CheckRobotsTxtWithClient(subdomain string, client *http.Client) bool {
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s/robots.txt", subdomain),
|
||||
fmt.Sprintf("http://%s/robots.txt", subdomain),
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckSitemapXmlWithClient(subdomain string, client *http.Client) bool {
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s/sitemap.xml", subdomain),
|
||||
fmt.Sprintf("http://%s/sitemap.xml", subdomain),
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func GetFaviconHashWithClient(subdomain string, client *http.Client) string {
|
||||
urls := []string{
|
||||
fmt.Sprintf("https://%s/favicon.ico", subdomain),
|
||||
fmt.Sprintf("http://%s/favicon.ico", subdomain),
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 100000))
|
||||
if err != nil || len(body) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hash := md5.Sum(body)
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user