github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/common/automaticscan/automaticscan.go (about)

     1  package automaticscan
     2  
     3  import (
     4  	"io"
     5  	"net/http"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/corpix/uarand"
    11  	"github.com/pkg/errors"
    12  	"github.com/projectdiscovery/gologger"
    13  	"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
    14  	"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
    15  	"github.com/projectdiscovery/nuclei/v2/pkg/core"
    16  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
    17  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
    18  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/httpclientpool"
    19  	httputil "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils/http"
    20  	"github.com/projectdiscovery/nuclei/v2/pkg/templates"
    21  	"github.com/projectdiscovery/nuclei/v2/pkg/templates/types"
    22  	"github.com/projectdiscovery/retryablehttp-go"
    23  	sliceutil "github.com/projectdiscovery/utils/slice"
    24  	wappalyzer "github.com/projectdiscovery/wappalyzergo"
    25  	"gopkg.in/yaml.v2"
    26  )
    27  
    28  // Service is a service for automatic scan execution
    29  type Service struct {
    30  	opts          protocols.ExecutorOptions
    31  	store         *loader.Store
    32  	engine        *core.Engine
    33  	target        core.InputProvider
    34  	wappalyzer    *wappalyzer.Wappalyze
    35  	childExecuter *core.ChildExecuter
    36  	httpclient    *retryablehttp.Client
    37  
    38  	results            bool
    39  	allTemplates       []string
    40  	technologyMappings map[string]string
    41  }
    42  
    43  // Options contains configuration options for automatic scan service
    44  type Options struct {
    45  	ExecuterOpts protocols.ExecutorOptions
    46  	Store        *loader.Store
    47  	Engine       *core.Engine
    48  	Target       core.InputProvider
    49  }
    50  
    51  const mappingFilename = "wappalyzer-mapping.yml"
    52  
    53  // New takes options and returns a new automatic scan service
    54  func New(opts Options) (*Service, error) {
    55  	wappalyzer, err := wappalyzer.New()
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	var mappingData map[string]string
    61  	config := config.DefaultConfig
    62  
    63  	mappingFile := filepath.Join(config.TemplatesDirectory, mappingFilename)
    64  	if file, err := os.Open(mappingFile); err == nil {
    65  		_ = yaml.NewDecoder(file).Decode(&mappingData)
    66  		file.Close()
    67  	}
    68  
    69  	if opts.ExecuterOpts.Options.Verbose {
    70  		gologger.Verbose().Msgf("Normalized mapping (%d): %v\n", len(mappingData), mappingData)
    71  	}
    72  	defaultTemplatesDirectories := []string{config.TemplatesDirectory}
    73  
    74  	// adding custom template path if available
    75  	if len(opts.ExecuterOpts.Options.Templates) > 0 {
    76  		defaultTemplatesDirectories = append(defaultTemplatesDirectories, opts.ExecuterOpts.Options.Templates...)
    77  	}
    78  	// Collect path for default directories we want to look for templates in
    79  	var allTemplates []string
    80  	for _, directory := range defaultTemplatesDirectories {
    81  		templates, err := opts.ExecuterOpts.Catalog.GetTemplatePath(directory)
    82  		if err != nil {
    83  			return nil, errors.Wrap(err, "could not get templates in directory")
    84  		}
    85  		allTemplates = append(allTemplates, templates...)
    86  	}
    87  	childExecuter := opts.Engine.ChildExecuter()
    88  
    89  	httpclient, err := httpclientpool.Get(opts.ExecuterOpts.Options, &httpclientpool.Configuration{
    90  		Connection: &httpclientpool.ConnectionConfiguration{
    91  			DisableKeepAlive: httputil.ShouldDisableKeepAlive(opts.ExecuterOpts.Options),
    92  		},
    93  	})
    94  	if err != nil {
    95  		return nil, errors.Wrap(err, "could not get http client")
    96  	}
    97  
    98  	return &Service{
    99  		opts:               opts.ExecuterOpts,
   100  		store:              opts.Store,
   101  		engine:             opts.Engine,
   102  		target:             opts.Target,
   103  		wappalyzer:         wappalyzer,
   104  		allTemplates:       allTemplates,
   105  		childExecuter:      childExecuter,
   106  		httpclient:         httpclient,
   107  		technologyMappings: mappingData,
   108  	}, nil
   109  }
   110  
   111  // Close closes the service
   112  func (s *Service) Close() bool {
   113  	results := s.childExecuter.Close()
   114  	if results.Load() {
   115  		s.results = true
   116  	}
   117  	return s.results
   118  }
   119  
   120  // Execute performs the execution of automatic scan on provided input
   121  func (s *Service) Execute() {
   122  	if err := s.executeWappalyzerTechDetection(); err != nil {
   123  		gologger.Error().Msgf("Could not execute wappalyzer based detection: %s", err)
   124  	}
   125  }
   126  
   127  const maxDefaultBody = 2 * 1024 * 1024
   128  
   129  // executeWappalyzerTechDetection implements the logic to run the wappalyzer
   130  // technologies detection on inputs which returns tech.
   131  //
   132  // The returned tags are then used for further execution.
   133  func (s *Service) executeWappalyzerTechDetection() error {
   134  	gologger.Info().Msgf("Executing wappalyzer based tech detection on input urls")
   135  
   136  	// Iterate through each target making http request and identifying fingerprints
   137  	inputPool := s.engine.WorkPool().InputPool(types.HTTPProtocol)
   138  
   139  	s.target.Scan(func(value *contextargs.MetaInput) bool {
   140  		inputPool.WaitGroup.Add()
   141  
   142  		go func(input *contextargs.MetaInput) {
   143  			defer inputPool.WaitGroup.Done()
   144  
   145  			s.processWappalyzerInputPair(input)
   146  		}(value)
   147  		return true
   148  	})
   149  	inputPool.WaitGroup.Wait()
   150  	return nil
   151  }
   152  
   153  func (s *Service) processWappalyzerInputPair(input *contextargs.MetaInput) {
   154  	req, err := retryablehttp.NewRequest(http.MethodGet, input.Input, nil)
   155  	if err != nil {
   156  		return
   157  	}
   158  	req.Header.Set("User-Agent", uarand.GetRandom())
   159  
   160  	resp, err := s.httpclient.Do(req)
   161  	if err != nil {
   162  		if resp != nil {
   163  			resp.Body.Close()
   164  		}
   165  		return
   166  	}
   167  	reader := io.LimitReader(resp.Body, maxDefaultBody)
   168  	data, err := io.ReadAll(reader)
   169  	if err != nil {
   170  		resp.Body.Close()
   171  		return
   172  	}
   173  	resp.Body.Close()
   174  
   175  	fingerprints := s.wappalyzer.Fingerprint(resp.Header, data)
   176  	normalized := make(map[string]struct{})
   177  	for k := range fingerprints {
   178  		normalized[normalizeAppName(k)] = struct{}{}
   179  	}
   180  
   181  	if s.opts.Options.Verbose {
   182  		gologger.Verbose().Msgf("Wappalyzer fingerprints %v for %s\n", normalized, input)
   183  	}
   184  
   185  	for k := range normalized {
   186  		// Replace values with mapping data
   187  		if value, ok := s.technologyMappings[k]; ok {
   188  			delete(normalized, k)
   189  			normalized[value] = struct{}{}
   190  		}
   191  	}
   192  
   193  	items := make([]string, 0, len(normalized))
   194  	for k := range normalized {
   195  		if strings.Contains(k, " ") {
   196  			parts := strings.Split(strings.ToLower(k), " ")
   197  			items = append(items, parts...)
   198  		} else {
   199  			items = append(items, strings.ToLower(k))
   200  		}
   201  	}
   202  	if len(items) == 0 {
   203  		return
   204  	}
   205  	uniqueTags := sliceutil.Dedupe(items)
   206  
   207  	templatesList := s.store.LoadTemplatesWithTags(s.allTemplates, uniqueTags)
   208  	gologger.Info().Msgf("Executing tags (%v) for host %s (%d templates)", strings.Join(uniqueTags, ","), input, len(templatesList))
   209  	for _, t := range templatesList {
   210  		s.opts.Progress.AddToTotal(int64(t.Executer.Requests()))
   211  
   212  		if s.opts.Options.VerboseVerbose {
   213  			gologger.Print().Msgf("%s\n", templates.TemplateLogMessage(t.ID,
   214  				t.Info.Name,
   215  				t.Info.Authors.ToSlice(),
   216  				t.Info.SeverityHolder.Severity))
   217  		}
   218  		s.childExecuter.Execute(t, input)
   219  	}
   220  }
   221  
   222  func normalizeAppName(appName string) string {
   223  	if strings.Contains(appName, ":") {
   224  		if parts := strings.Split(appName, ":"); len(parts) == 2 {
   225  			appName = parts[0]
   226  		}
   227  	}
   228  	return strings.ToLower(appName)
   229  }