package runner import ( "bufio" "bytes" "context" "encoding/csv" "encoding/json" "fmt" "io" "net" "net/http" "net/http/httputil" "net/url" "os" "path" "path/filepath" "reflect" "regexp" "sort" "strconv" "strings" "time" "golang.org/x/exp/maps" asnmap "github.com/projectdiscovery/asnmap/libs" dsl "github.com/projectdiscovery/dsl" "github.com/projectdiscovery/fastdialer/fastdialer" "github.com/projectdiscovery/httpx/common/customextract" "github.com/projectdiscovery/httpx/common/hashes/jarm" "github.com/projectdiscovery/mapcidr/asn" errorutil "github.com/projectdiscovery/utils/errors" "github.com/Mzack9999/gcache" "github.com/logrusorgru/aurora" "github.com/pkg/errors" "github.com/projectdiscovery/clistats" "github.com/projectdiscovery/goconfig" "github.com/projectdiscovery/httpx/common/hashes" "github.com/projectdiscovery/retryablehttp-go" sliceutil "github.com/projectdiscovery/utils/slice" stringsutil "github.com/projectdiscovery/utils/strings" urlutil "github.com/projectdiscovery/utils/url" "github.com/projectdiscovery/ratelimit" "github.com/remeh/sizedwaitgroup" // automatic fd max increase if running as root _ "github.com/projectdiscovery/fdmax/autofdmax" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/hmap/store/hybrid" customport "github.com/projectdiscovery/httpx/common/customports" fileutilz "github.com/projectdiscovery/httpx/common/fileutil" "github.com/projectdiscovery/httpx/common/httputilz" "github.com/projectdiscovery/httpx/common/httpx" "github.com/projectdiscovery/httpx/common/slice" "github.com/projectdiscovery/httpx/common/stringz" "github.com/projectdiscovery/mapcidr" "github.com/projectdiscovery/rawhttp" fileutil "github.com/projectdiscovery/utils/file" pdhttputil "github.com/projectdiscovery/utils/http" iputil "github.com/projectdiscovery/utils/ip" wappalyzer "github.com/projectdiscovery/wappalyzergo" ) // Runner is a client for running the enumeration process. type Runner struct { options *Options hp *httpx.HTTPX wappalyzer *wappalyzer.Wappalyze fastdialer *fastdialer.Dialer scanopts ScanOptions hm *hybrid.HybridMap stats clistats.StatisticsClient ratelimiter ratelimit.Limiter HostErrorsCache gcache.Cache[string, int] browser *Browser NextCheckUrl []string CallBack func(resp Result) } // New creates a new client for running enumeration process. func New(options *Options) (*Runner, error) { runner := &Runner{ options: options, } var err error if options.TechDetect { runner.wappalyzer, err = wappalyzer.New() } if err != nil { return nil, errors.Wrap(err, "could not create wappalyzer client") } if options.StoreResponseDir != "" { os.RemoveAll(filepath.Join(options.StoreResponseDir, "response", "index.txt")) os.RemoveAll(filepath.Join(options.StoreResponseDir, "screenshot", "index_screenshot.txt")) } dialerOpts := fastdialer.DefaultOptions dialerOpts.WithDialerHistory = true dialerOpts.MaxRetries = 3 dialerOpts.DialerTimeout = time.Duration(options.Timeout) * time.Second if len(options.Resolvers) > 0 { dialerOpts.BaseResolvers = options.Resolvers } fastDialer, err := fastdialer.NewDialer(dialerOpts) if err != nil { return nil, errors.Wrap(err, "could not create dialer") } runner.fastdialer = fastDialer httpxOptions := httpx.DefaultOptions // Enables automatically tlsgrab if tlsprobe is requested httpxOptions.TLSGrab = options.TLSGrab || options.TLSProbe httpxOptions.Timeout = time.Duration(options.Timeout) * time.Second httpxOptions.RetryMax = options.Retries httpxOptions.FollowRedirects = options.FollowRedirects httpxOptions.FollowHostRedirects = options.FollowHostRedirects httpxOptions.MaxRedirects = options.MaxRedirects httpxOptions.HTTPProxy = options.HTTPProxy httpxOptions.Unsafe = options.Unsafe httpxOptions.UnsafeURI = options.RequestURI httpxOptions.CdnCheck = options.OutputCDN httpxOptions.ExcludeCdn = options.ExcludeCDN if options.CustomHeaders.Has("User-Agent:") { httpxOptions.RandomAgent = false } else { httpxOptions.RandomAgent = options.RandomAgent } httpxOptions.Deny = options.Deny httpxOptions.Allow = options.Allow httpxOptions.ZTLS = options.ZTLS httpxOptions.MaxResponseBodySizeToSave = int64(options.MaxResponseBodySizeToSave) httpxOptions.MaxResponseBodySizeToRead = int64(options.MaxResponseBodySizeToRead) // adjust response size saved according to the max one read by the server if httpxOptions.MaxResponseBodySizeToSave > httpxOptions.MaxResponseBodySizeToRead { httpxOptions.MaxResponseBodySizeToSave = httpxOptions.MaxResponseBodySizeToRead } httpxOptions.Resolvers = options.Resolvers var key, value string httpxOptions.CustomHeaders = make(map[string]string) for _, customHeader := range options.CustomHeaders { tokens := strings.SplitN(customHeader, ":", two) // rawhttp skips all checks if options.Unsafe { httpxOptions.CustomHeaders[customHeader] = "" continue } // Continue normally if len(tokens) < two { continue } key = strings.TrimSpace(tokens[0]) value = strings.TrimSpace(tokens[1]) httpxOptions.CustomHeaders[key] = value } httpxOptions.SniName = options.SniName runner.hp, err = httpx.New(&httpxOptions) if err != nil { gologger.Fatal().Msgf("Could not create httpx instance: %s\n", err) } var scanopts ScanOptions if options.InputRawRequest != "" { var rawRequest []byte rawRequest, err = os.ReadFile(options.InputRawRequest) if err != nil { gologger.Fatal().Msgf("Could not read raw request from path '%s': %s\n", options.InputRawRequest, err) } rrMethod, rrPath, rrHeaders, rrBody, errParse := httputilz.ParseRequest(string(rawRequest), options.Unsafe) if errParse != nil { gologger.Fatal().Msgf("Could not parse raw request: %s\n", err) } scanopts.Methods = append(scanopts.Methods, rrMethod) scanopts.RequestURI = rrPath for name, value := range rrHeaders { httpxOptions.CustomHeaders[name] = value } scanopts.RequestBody = rrBody options.rawRequest = string(rawRequest) options.RequestBody = rrBody } // disable automatic host header for rawhttp if manually specified // as it can be malformed the best approach is to remove spaces and check for lowercase "host" word if options.Unsafe { for name := range runner.hp.CustomHeaders { nameLower := strings.TrimSpace(strings.ToLower(name)) if strings.HasPrefix(nameLower, "host") { rawhttp.AutomaticHostHeader(false) } } } if strings.EqualFold(options.Methods, "all") { scanopts.Methods = pdhttputil.AllHTTPMethods() } else if options.Methods != "" { // if unsafe is specified then converts the methods to uppercase if !options.Unsafe { options.Methods = strings.ToUpper(options.Methods) } scanopts.Methods = append(scanopts.Methods, stringz.SplitByCharAndTrimSpace(options.Methods, ",")...) } if len(scanopts.Methods) == 0 { scanopts.Methods = append(scanopts.Methods, http.MethodGet) } runner.options.protocol = httpx.HTTPorHTTPS scanopts.VHost = options.VHost scanopts.OutputTitle = options.ExtractTitle scanopts.OutputStatusCode = options.StatusCode scanopts.OutputLocation = options.Location scanopts.OutputContentLength = options.ContentLength scanopts.StoreResponse = options.StoreResponse scanopts.StoreResponseDirectory = options.StoreResponseDir scanopts.OutputServerHeader = options.OutputServerHeader scanopts.OutputWithNoColor = options.NoColor scanopts.ResponseInStdout = options.responseInStdout scanopts.Base64ResponseInStdout = options.base64responseInStdout scanopts.ChainInStdout = options.chainInStdout scanopts.OutputWebSocket = options.OutputWebSocket scanopts.TLSProbe = options.TLSProbe scanopts.CSPProbe = options.CSPProbe if options.RequestURI != "" { scanopts.RequestURI = options.RequestURI } scanopts.VHostInput = options.VHostInput scanopts.OutputContentType = options.OutputContentType scanopts.RequestBody = options.RequestBody scanopts.Unsafe = options.Unsafe scanopts.Pipeline = options.Pipeline scanopts.HTTP2Probe = options.HTTP2Probe scanopts.OutputMethod = options.OutputMethod scanopts.OutputIP = options.OutputIP scanopts.OutputCName = options.OutputCName scanopts.OutputCDN = options.OutputCDN scanopts.OutputResponseTime = options.OutputResponseTime scanopts.NoFallback = options.NoFallback scanopts.NoFallbackScheme = options.NoFallbackScheme scanopts.TechDetect = options.TechDetect scanopts.StoreChain = options.StoreChain scanopts.MaxResponseBodySizeToSave = options.MaxResponseBodySizeToSave scanopts.MaxResponseBodySizeToRead = options.MaxResponseBodySizeToRead scanopts.extractRegexps = make(map[string]*regexp.Regexp) if options.Screenshot { browser, err := NewBrowser(options.HTTPProxy, options.UseInstalledChrome) if err != nil { return nil, err } runner.browser = browser } scanopts.Screenshot = options.Screenshot scanopts.UseInstalledChrome = options.UseInstalledChrome if options.OutputExtractRegexs != nil { for _, regex := range options.OutputExtractRegexs { if compiledRegex, err := regexp.Compile(regex); err != nil { return nil, err } else { scanopts.extractRegexps[regex] = compiledRegex } } } if options.OutputExtractPresets != nil { for _, regexName := range options.OutputExtractPresets { if regex, ok := customextract.ExtractPresets[regexName]; ok { scanopts.extractRegexps[regexName] = regex } else { availablePresets := strings.Join(maps.Keys(customextract.ExtractPresets), ",") gologger.Warning().Msgf("Could not find preset: '%s'. Available presets are: %s\n", regexName, availablePresets) } } } // output verb if more than one is specified if len(scanopts.Methods) > 1 && !options.Silent { scanopts.OutputMethod = true } scanopts.ExcludeCDN = options.ExcludeCDN scanopts.HostMaxErrors = options.HostMaxErrors scanopts.ProbeAllIPS = options.ProbeAllIPS scanopts.Favicon = options.Favicon scanopts.LeaveDefaultPorts = options.LeaveDefaultPorts scanopts.OutputLinesCount = options.OutputLinesCount scanopts.OutputWordsCount = options.OutputWordsCount scanopts.Hashes = options.Hashes runner.scanopts = scanopts if options.ShowStatistics { runner.stats, err = clistats.New() if err != nil { return nil, err } if options.StatsInterval == 0 { options.StatsInterval = 5 } } hmapOptions := hybrid.DefaultDiskOptions hmapOptions.DBType = hybrid.PogrebDB hm, err := hybrid.New(hybrid.DefaultDiskOptions) if err != nil { return nil, err } runner.hm = hm if options.RateLimitMinute > 0 { runner.ratelimiter = *ratelimit.New(context.Background(), uint(options.RateLimitMinute), time.Minute) } else if options.RateLimit > 0 { runner.ratelimiter = *ratelimit.New(context.Background(), uint(options.RateLimit), time.Second) } else { runner.ratelimiter = *ratelimit.NewUnlimited(context.Background()) } if options.HostMaxErrors >= 0 { gc := gcache.New[string, int](1000). ARC(). Build() runner.HostErrorsCache = gc } return runner, nil } func (r *Runner) prepareInputPaths() { // most likely, the user would provide the most simplified path to an existing file isAbsoluteOrRelativePath := filepath.Clean(r.options.RequestURIs) == r.options.RequestURIs // Check if the user requested multiple paths if isAbsoluteOrRelativePath && fileutil.FileExists(r.options.RequestURIs) { r.options.requestURIs = fileutilz.LoadFile(r.options.RequestURIs) } else if r.options.RequestURIs != "" { r.options.requestURIs = strings.Split(r.options.RequestURIs, ",") } } func (r *Runner) prepareInput() { var numHosts int // check if input target host(s) have been provided if len(r.options.InputTargetHost) > 0 { for _, target := range r.options.InputTargetHost { expandedTarget := r.countTargetFromRawTarget(target) if expandedTarget > 0 { numHosts += expandedTarget r.hm.Set(target, nil) //nolint } } } // check if file has been provided if fileutil.FileExists(r.options.InputFile) { finput, err := os.Open(r.options.InputFile) if err != nil { gologger.Fatal().Msgf("Could not read input file '%s': %s\n", r.options.InputFile, err) } numHosts, err = r.loadAndCloseFile(finput) if err != nil { gologger.Fatal().Msgf("Could not read input file '%s': %s\n", r.options.InputFile, err) } } else if r.options.InputFile != "" { files, err := fileutilz.ListFilesWithPattern(r.options.InputFile) if err != nil { gologger.Fatal().Msgf("No input provided: %s", err) } for _, file := range files { finput, err := os.Open(file) if err != nil { gologger.Fatal().Msgf("Could not read input file '%s': %s\n", r.options.InputFile, err) } numTargetsFile, err := r.loadAndCloseFile(finput) if err != nil { gologger.Fatal().Msgf("Could not read input file '%s': %s\n", r.options.InputFile, err) } numHosts += numTargetsFile } } if fileutil.HasStdin() { numTargetsStdin, err := r.loadAndCloseFile(os.Stdin) if err != nil { gologger.Fatal().Msgf("Could not read input from stdin: %s\n", err) } numHosts += numTargetsStdin } } func (r *Runner) setSeen(k string) { _ = r.hm.Set(k, nil) } func (r *Runner) seen(k string) bool { _, ok := r.hm.Get(k) return ok } func (r *Runner) testAndSet(k string) bool { // skip empty lines k = strings.TrimSpace(k) if k == "" { return false } if r.seen(k) { return false } r.setSeen(k) return true } func (r *Runner) streamInput() (chan string, error) { out := make(chan string) go func() { defer close(out) if fileutil.FileExists(r.options.InputFile) { fchan, err := fileutil.ReadFile(r.options.InputFile) if err != nil { return } for item := range fchan { if r.options.SkipDedupe || r.testAndSet(item) { out <- item } } } else if r.options.InputFile != "" { files, err := fileutilz.ListFilesWithPattern(r.options.InputFile) if err != nil { gologger.Fatal().Msgf("No input provided: %s", err) } for _, file := range files { fchan, err := fileutil.ReadFile(file) if err != nil { return } for item := range fchan { if r.options.SkipDedupe || r.testAndSet(item) { out <- item } } } } if fileutil.HasStdin() { fchan, err := fileutil.ReadFileWithReader(os.Stdin) if err != nil { return } for item := range fchan { if r.options.SkipDedupe || r.testAndSet(item) { out <- item } } } }() return out, nil } func (r *Runner) loadAndCloseFile(finput *os.File) (numTargets int, err error) { scanner := bufio.NewScanner(finput) for scanner.Scan() { target := strings.TrimSpace(scanner.Text()) // Used just to get the exact number of targets expandedTarget := r.countTargetFromRawTarget(target) if expandedTarget > 0 { numTargets += expandedTarget r.hm.Set(target, nil) //nolint } } err = finput.Close() return numTargets, err } func (r *Runner) countTargetFromRawTarget(rawTarget string) (numTargets int) { if rawTarget == "" { return 0 } if _, ok := r.hm.Get(rawTarget); ok { return 0 } expandedTarget := 0 switch { case iputil.IsCIDR(rawTarget): if ipsCount, err := mapcidr.AddressCount(rawTarget); err == nil && ipsCount > 0 { expandedTarget = int(ipsCount) } case asn.IsASN(rawTarget): cidrs, _ := asn.GetCIDRsForASNNum(rawTarget) for _, cidr := range cidrs { expandedTarget += int(mapcidr.AddressCountIpnet(cidr)) } default: expandedTarget = 1 } return expandedTarget } var ( lastRequestsCount float64 ) func makePrintCallback() func(stats clistats.StatisticsClient) { builder := &strings.Builder{} return func(stats clistats.StatisticsClient) { startedAt, _ := stats.GetStatic("startedAt") duration := time.Since(startedAt.(time.Time)) builder.WriteRune('[') builder.WriteString(clistats.FmtDuration(duration)) builder.WriteRune(']') var currentRequests float64 if reqs, _ := stats.GetCounter("requests"); reqs > 0 { currentRequests = float64(reqs) } builder.WriteString(" | RPS: ") incrementRequests := currentRequests - lastRequestsCount builder.WriteString(clistats.String(uint64(incrementRequests / duration.Seconds()))) builder.WriteString(" | Requests: ") builder.WriteString(fmt.Sprintf("%.0f", currentRequests)) hosts, _ := stats.GetCounter("hosts") totalHosts, _ := stats.GetStatic("totalHosts") builder.WriteString(" | Hosts: ") builder.WriteString(clistats.String(hosts)) builder.WriteRune('/') builder.WriteString(clistats.String(totalHosts)) builder.WriteRune(' ') builder.WriteRune('(') //nolint:gomnd // this is not a magic number builder.WriteString(clistats.String(uint64(float64(hosts) / float64(totalHosts.(int)) * 100.0))) builder.WriteRune('%') builder.WriteRune(')') builder.WriteRune('\n') fmt.Fprintf(os.Stderr, "%s", builder.String()) builder.Reset() lastRequestsCount = currentRequests } } // Close closes the httpx scan instance func (r *Runner) Close() { // nolint:errcheck // ignore r.hm.Close() r.hp.Dialer.Close() if r.options.HostMaxErrors >= 0 { r.HostErrorsCache.Purge() } if r.options.Screenshot { r.browser.Close() } } var ( reg1 = regexp.MustCompile(`(?i)`) reg2 = regexp.MustCompile(`(?i)[window\.]?location[\.href]?.*?=.*?["'](.*?)["']`) reg3 = regexp.MustCompile(`(?i)window\.location\.replace\(['"](.*?)['"]\)`) ) func getJumpPath(Raw string) string { matches := reg1.FindAllStringSubmatch(Raw, -1) if len(matches) > 0 { // 去除注释的情况 if !strings.Contains(Raw, "