Path: blob/dev/pkg/protocols/common/automaticscan/automaticscan.go
2072 views
package automaticscan12import (3"context"4"io"5"net/http"6"os"7"path/filepath"8"strings"9"sync"10"sync/atomic"1112"github.com/logrusorgru/aurora"13"github.com/pkg/errors"14"github.com/projectdiscovery/gologger"15"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"16"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"17"github.com/projectdiscovery/nuclei/v3/pkg/core"18"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"19"github.com/projectdiscovery/nuclei/v3/pkg/output"20"github.com/projectdiscovery/nuclei/v3/pkg/protocols"21"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"22"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/writer"23"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"24httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"25"github.com/projectdiscovery/nuclei/v3/pkg/scan"26"github.com/projectdiscovery/nuclei/v3/pkg/templates"27"github.com/projectdiscovery/nuclei/v3/pkg/testutils"28"github.com/projectdiscovery/retryablehttp-go"29"github.com/projectdiscovery/useragent"30mapsutil "github.com/projectdiscovery/utils/maps"31sliceutil "github.com/projectdiscovery/utils/slice"32stringsutil "github.com/projectdiscovery/utils/strings"33syncutil "github.com/projectdiscovery/utils/sync"34unitutils "github.com/projectdiscovery/utils/unit"35wappalyzer "github.com/projectdiscovery/wappalyzergo"36"gopkg.in/yaml.v2"37)3839const (40mappingFilename = "wappalyzer-mapping.yml"41maxDefaultBody = 4 * unitutils.Mega42)4344// Options contains configuration options for automatic scan service45type Options struct {46ExecuterOpts *protocols.ExecutorOptions47Store *loader.Store48Engine *core.Engine49Target provider.InputProvider50}5152// Service is a service for automatic scan execution53type Service struct {54opts *protocols.ExecutorOptions55store *loader.Store56engine *core.Engine57target provider.InputProvider58wappalyzer *wappalyzer.Wappalyze59httpclient *retryablehttp.Client60templateDirs []string // root Template Directories61technologyMappings map[string]string62techTemplates []*templates.Template63ServiceOpts Options64hasResults *atomic.Bool65}6667// New takes options and returns a new automatic scan service68func New(opts Options) (*Service, error) {69wappalyzer, err := wappalyzer.New()70if err != nil {71return nil, err72}7374// load extra mapping from nuclei-templates for normalization75var mappingData map[string]string76mappingFile := filepath.Join(config.DefaultConfig.GetTemplateDir(), mappingFilename)77if file, err := os.Open(mappingFile); err == nil {78_ = yaml.NewDecoder(file).Decode(&mappingData)79_ = file.Close()80}81if opts.ExecuterOpts.Options.Verbose {82gologger.Verbose().Msgf("Normalized mapping (%d): %v\n", len(mappingData), mappingData)83}8485// get template directories86templateDirs, err := getTemplateDirs(opts)87if err != nil {88return nil, err89}9091// load tech detect templates92techDetectTemplates, err := LoadTemplatesWithTags(opts, templateDirs, []string{"tech", "detect", "favicon"}, true)93if err != nil {94return nil, err95}9697httpclient, err := httpclientpool.Get(opts.ExecuterOpts.Options, &httpclientpool.Configuration{98Connection: &httpclientpool.ConnectionConfiguration{99DisableKeepAlive: httputil.ShouldDisableKeepAlive(opts.ExecuterOpts.Options),100},101})102if err != nil {103return nil, errors.Wrap(err, "could not get http client")104}105return &Service{106opts: opts.ExecuterOpts,107store: opts.Store,108engine: opts.Engine,109target: opts.Target,110wappalyzer: wappalyzer,111templateDirs: templateDirs, // fix this112httpclient: httpclient,113technologyMappings: mappingData,114techTemplates: techDetectTemplates,115ServiceOpts: opts,116hasResults: &atomic.Bool{},117}, nil118}119120// Close closes the service121func (s *Service) Close() bool {122return s.hasResults.Load()123}124125// Execute automatic scan on each target with -bs host concurrency126func (s *Service) Execute() error {127gologger.Info().Msgf("Executing Automatic scan on %d target[s]", s.target.Count())128// setup host concurrency129sg, err := syncutil.New(syncutil.WithSize(s.opts.Options.BulkSize))130if err != nil {131return err132}133s.target.Iterate(func(value *contextargs.MetaInput) bool {134sg.Add()135go func(input *contextargs.MetaInput) {136defer sg.Done()137s.executeAutomaticScanOnTarget(input)138}(value)139return true140})141sg.Wait()142return nil143}144145// executeAutomaticScanOnTarget executes automatic scan on given target146func (s *Service) executeAutomaticScanOnTarget(input *contextargs.MetaInput) {147// get tags using wappalyzer148tagsFromWappalyzer := s.getTagsUsingWappalyzer(input)149// get tags using detection templates150tagsFromDetectTemplates, matched := s.getTagsUsingDetectionTemplates(input)151if matched > 0 {152s.hasResults.Store(true)153}154155// create combined final tags156finalTags := []string{}157for _, tags := range append(tagsFromWappalyzer, tagsFromDetectTemplates...) {158if stringsutil.EqualFoldAny(tags, "tech", "waf", "favicon") {159continue160}161finalTags = append(finalTags, tags)162}163finalTags = sliceutil.Dedupe(finalTags)164165gologger.Info().Msgf("Found %d tags and %d matches on detection templates on %v [wappalyzer: %d, detection: %d]\n", len(finalTags), matched, input.Input, len(tagsFromWappalyzer), len(tagsFromDetectTemplates))166167// also include any extra tags passed by user168finalTags = append(finalTags, s.opts.Options.Tags...)169finalTags = sliceutil.Dedupe(finalTags)170171if len(finalTags) == 0 {172gologger.Warning().Msgf("Skipping automatic scan since no tags were found on %v\n", input.Input)173return174}175if s.opts.Options.VerboseVerbose {176gologger.Print().Msgf("Final tags identified for %v: %+v\n", input.Input, finalTags)177}178179finalTemplates, err := LoadTemplatesWithTags(s.ServiceOpts, s.templateDirs, finalTags, false)180if err != nil {181gologger.Error().Msgf("%v Error loading templates: %s\n", input.Input, err)182return183}184gologger.Info().Msgf("Executing %d templates on %v", len(finalTemplates), input.Input)185eng := core.New(s.opts.Options)186execOptions := s.opts.Copy()187execOptions.Progress = &testutils.MockProgressClient{} // stats are not supported yet due to centralized logic and cannot be reinitialized188eng.SetExecuterOptions(execOptions)189190tmp := eng.ExecuteScanWithOpts(context.Background(), finalTemplates, provider.NewSimpleInputProviderWithUrls(s.opts.Options.ExecutionId, input.Input), true)191s.hasResults.Store(tmp.Load())192}193194// getTagsUsingWappalyzer returns tags using wappalyzer by fingerprinting target195// and utilizing the mapping data196func (s *Service) getTagsUsingWappalyzer(input *contextargs.MetaInput) []string {197req, err := retryablehttp.NewRequest(http.MethodGet, input.Input, nil)198if err != nil {199return nil200}201userAgent := useragent.PickRandom()202req.Header.Set("User-Agent", userAgent.Raw)203204resp, err := s.httpclient.Do(req)205if err != nil {206return nil207}208defer func() {209_ = resp.Body.Close()210}()211data, err := io.ReadAll(io.LimitReader(resp.Body, maxDefaultBody))212if err != nil {213return nil214}215216// fingerprint headers and body217fingerprints := s.wappalyzer.Fingerprint(resp.Header, data)218normalized := make(map[string]struct{})219for k := range fingerprints {220normalized[normalizeAppName(k)] = struct{}{}221}222gologger.Verbose().Msgf("Found %d fingerprints for %s\n", len(normalized), input.Input)223224// normalize fingerprints using mapping data225for k := range normalized {226// Replace values with mapping data227if value, ok := s.technologyMappings[k]; ok {228delete(normalized, k)229normalized[value] = struct{}{}230}231}232// more post processing233items := make([]string, 0, len(normalized))234for k := range normalized {235if strings.Contains(k, " ") {236parts := strings.Split(strings.ToLower(k), " ")237items = append(items, parts...)238} else {239items = append(items, strings.ToLower(k))240}241}242return sliceutil.Dedupe(items)243}244245// getTagsUsingDetectionTemplates returns tags using detection templates246func (s *Service) getTagsUsingDetectionTemplates(input *contextargs.MetaInput) ([]string, int) {247ctx := context.Background()248249ctxArgs := contextargs.NewWithInput(ctx, input.Input)250251// execute tech detection templates on target252tags := map[string]struct{}{}253m := &sync.Mutex{}254sg, _ := syncutil.New(syncutil.WithSize(s.opts.Options.TemplateThreads))255counter := atomic.Uint32{}256257for _, t := range s.techTemplates {258sg.Add()259go func(template *templates.Template) {260defer sg.Done()261ctx := scan.NewScanContext(ctx, ctxArgs)262ctx.OnResult = func(event *output.InternalWrappedEvent) {263if event == nil {264return265}266if event.HasOperatorResult() {267// match found268// find unique tags269m.Lock()270for _, v := range event.Results {271if v.MatcherName != "" {272tags[v.MatcherName] = struct{}{}273}274for _, tag := range v.Info.Tags.ToSlice() {275// we shouldn't add all tags since tags also contain protocol type tags276// and are not just limited to products or technologies277// ex: tags: js,mssql,detect,network278279// A good trick for this is check if tag is present in template-id280if !strings.Contains(template.ID, tag) && !strings.Contains(strings.ToLower(template.Info.Name), tag) {281// unlikely this is relevant282continue283}284if _, ok := tags[tag]; !ok {285tags[tag] = struct{}{}286}287// matcher names are also relevant in tech detection templates (ex: tech-detect)288for k := range event.OperatorsResult.Matches {289if _, ok := tags[k]; !ok {290tags[k] = struct{}{}291}292}293}294}295m.Unlock()296_ = counter.Add(1)297298// TBD: should we show or hide tech detection results? what about matcher-status flag?299_ = writer.WriteResult(event, s.opts.Output, s.opts.Progress, s.opts.IssuesClient)300}301}302303_, err := template.Executer.ExecuteWithResults(ctx)304if err != nil {305gologger.Verbose().Msgf("[%s] error executing template: %s\n", aurora.BrightYellow(template.ID), err)306return307}308}(t)309}310sg.Wait()311return mapsutil.GetKeys(tags), int(counter.Load())312}313314// normalizeAppName normalizes app name315func normalizeAppName(appName string) string {316if strings.Contains(appName, ":") {317if parts := strings.Split(appName, ":"); len(parts) == 2 {318appName = parts[0]319}320}321return strings.ToLower(appName)322}323324325