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 }