Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/protocols/common/automaticscan/automaticscan.go
2072 views
1
package automaticscan
2
3
import (
4
"context"
5
"io"
6
"net/http"
7
"os"
8
"path/filepath"
9
"strings"
10
"sync"
11
"sync/atomic"
12
13
"github.com/logrusorgru/aurora"
14
"github.com/pkg/errors"
15
"github.com/projectdiscovery/gologger"
16
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
17
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
18
"github.com/projectdiscovery/nuclei/v3/pkg/core"
19
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
20
"github.com/projectdiscovery/nuclei/v3/pkg/output"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
23
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/writer"
24
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
25
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
26
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
27
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
28
"github.com/projectdiscovery/nuclei/v3/pkg/testutils"
29
"github.com/projectdiscovery/retryablehttp-go"
30
"github.com/projectdiscovery/useragent"
31
mapsutil "github.com/projectdiscovery/utils/maps"
32
sliceutil "github.com/projectdiscovery/utils/slice"
33
stringsutil "github.com/projectdiscovery/utils/strings"
34
syncutil "github.com/projectdiscovery/utils/sync"
35
unitutils "github.com/projectdiscovery/utils/unit"
36
wappalyzer "github.com/projectdiscovery/wappalyzergo"
37
"gopkg.in/yaml.v2"
38
)
39
40
const (
41
mappingFilename = "wappalyzer-mapping.yml"
42
maxDefaultBody = 4 * unitutils.Mega
43
)
44
45
// Options contains configuration options for automatic scan service
46
type Options struct {
47
ExecuterOpts *protocols.ExecutorOptions
48
Store *loader.Store
49
Engine *core.Engine
50
Target provider.InputProvider
51
}
52
53
// Service is a service for automatic scan execution
54
type Service struct {
55
opts *protocols.ExecutorOptions
56
store *loader.Store
57
engine *core.Engine
58
target provider.InputProvider
59
wappalyzer *wappalyzer.Wappalyze
60
httpclient *retryablehttp.Client
61
templateDirs []string // root Template Directories
62
technologyMappings map[string]string
63
techTemplates []*templates.Template
64
ServiceOpts Options
65
hasResults *atomic.Bool
66
}
67
68
// New takes options and returns a new automatic scan service
69
func New(opts Options) (*Service, error) {
70
wappalyzer, err := wappalyzer.New()
71
if err != nil {
72
return nil, err
73
}
74
75
// load extra mapping from nuclei-templates for normalization
76
var mappingData map[string]string
77
mappingFile := filepath.Join(config.DefaultConfig.GetTemplateDir(), mappingFilename)
78
if file, err := os.Open(mappingFile); err == nil {
79
_ = yaml.NewDecoder(file).Decode(&mappingData)
80
_ = file.Close()
81
}
82
if opts.ExecuterOpts.Options.Verbose {
83
gologger.Verbose().Msgf("Normalized mapping (%d): %v\n", len(mappingData), mappingData)
84
}
85
86
// get template directories
87
templateDirs, err := getTemplateDirs(opts)
88
if err != nil {
89
return nil, err
90
}
91
92
// load tech detect templates
93
techDetectTemplates, err := LoadTemplatesWithTags(opts, templateDirs, []string{"tech", "detect", "favicon"}, true)
94
if err != nil {
95
return nil, err
96
}
97
98
httpclient, err := httpclientpool.Get(opts.ExecuterOpts.Options, &httpclientpool.Configuration{
99
Connection: &httpclientpool.ConnectionConfiguration{
100
DisableKeepAlive: httputil.ShouldDisableKeepAlive(opts.ExecuterOpts.Options),
101
},
102
})
103
if err != nil {
104
return nil, errors.Wrap(err, "could not get http client")
105
}
106
return &Service{
107
opts: opts.ExecuterOpts,
108
store: opts.Store,
109
engine: opts.Engine,
110
target: opts.Target,
111
wappalyzer: wappalyzer,
112
templateDirs: templateDirs, // fix this
113
httpclient: httpclient,
114
technologyMappings: mappingData,
115
techTemplates: techDetectTemplates,
116
ServiceOpts: opts,
117
hasResults: &atomic.Bool{},
118
}, nil
119
}
120
121
// Close closes the service
122
func (s *Service) Close() bool {
123
return s.hasResults.Load()
124
}
125
126
// Execute automatic scan on each target with -bs host concurrency
127
func (s *Service) Execute() error {
128
gologger.Info().Msgf("Executing Automatic scan on %d target[s]", s.target.Count())
129
// setup host concurrency
130
sg, err := syncutil.New(syncutil.WithSize(s.opts.Options.BulkSize))
131
if err != nil {
132
return err
133
}
134
s.target.Iterate(func(value *contextargs.MetaInput) bool {
135
sg.Add()
136
go func(input *contextargs.MetaInput) {
137
defer sg.Done()
138
s.executeAutomaticScanOnTarget(input)
139
}(value)
140
return true
141
})
142
sg.Wait()
143
return nil
144
}
145
146
// executeAutomaticScanOnTarget executes automatic scan on given target
147
func (s *Service) executeAutomaticScanOnTarget(input *contextargs.MetaInput) {
148
// get tags using wappalyzer
149
tagsFromWappalyzer := s.getTagsUsingWappalyzer(input)
150
// get tags using detection templates
151
tagsFromDetectTemplates, matched := s.getTagsUsingDetectionTemplates(input)
152
if matched > 0 {
153
s.hasResults.Store(true)
154
}
155
156
// create combined final tags
157
finalTags := []string{}
158
for _, tags := range append(tagsFromWappalyzer, tagsFromDetectTemplates...) {
159
if stringsutil.EqualFoldAny(tags, "tech", "waf", "favicon") {
160
continue
161
}
162
finalTags = append(finalTags, tags)
163
}
164
finalTags = sliceutil.Dedupe(finalTags)
165
166
gologger.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))
167
168
// also include any extra tags passed by user
169
finalTags = append(finalTags, s.opts.Options.Tags...)
170
finalTags = sliceutil.Dedupe(finalTags)
171
172
if len(finalTags) == 0 {
173
gologger.Warning().Msgf("Skipping automatic scan since no tags were found on %v\n", input.Input)
174
return
175
}
176
if s.opts.Options.VerboseVerbose {
177
gologger.Print().Msgf("Final tags identified for %v: %+v\n", input.Input, finalTags)
178
}
179
180
finalTemplates, err := LoadTemplatesWithTags(s.ServiceOpts, s.templateDirs, finalTags, false)
181
if err != nil {
182
gologger.Error().Msgf("%v Error loading templates: %s\n", input.Input, err)
183
return
184
}
185
gologger.Info().Msgf("Executing %d templates on %v", len(finalTemplates), input.Input)
186
eng := core.New(s.opts.Options)
187
execOptions := s.opts.Copy()
188
execOptions.Progress = &testutils.MockProgressClient{} // stats are not supported yet due to centralized logic and cannot be reinitialized
189
eng.SetExecuterOptions(execOptions)
190
191
tmp := eng.ExecuteScanWithOpts(context.Background(), finalTemplates, provider.NewSimpleInputProviderWithUrls(s.opts.Options.ExecutionId, input.Input), true)
192
s.hasResults.Store(tmp.Load())
193
}
194
195
// getTagsUsingWappalyzer returns tags using wappalyzer by fingerprinting target
196
// and utilizing the mapping data
197
func (s *Service) getTagsUsingWappalyzer(input *contextargs.MetaInput) []string {
198
req, err := retryablehttp.NewRequest(http.MethodGet, input.Input, nil)
199
if err != nil {
200
return nil
201
}
202
userAgent := useragent.PickRandom()
203
req.Header.Set("User-Agent", userAgent.Raw)
204
205
resp, err := s.httpclient.Do(req)
206
if err != nil {
207
return nil
208
}
209
defer func() {
210
_ = resp.Body.Close()
211
}()
212
data, err := io.ReadAll(io.LimitReader(resp.Body, maxDefaultBody))
213
if err != nil {
214
return nil
215
}
216
217
// fingerprint headers and body
218
fingerprints := s.wappalyzer.Fingerprint(resp.Header, data)
219
normalized := make(map[string]struct{})
220
for k := range fingerprints {
221
normalized[normalizeAppName(k)] = struct{}{}
222
}
223
gologger.Verbose().Msgf("Found %d fingerprints for %s\n", len(normalized), input.Input)
224
225
// normalize fingerprints using mapping data
226
for k := range normalized {
227
// Replace values with mapping data
228
if value, ok := s.technologyMappings[k]; ok {
229
delete(normalized, k)
230
normalized[value] = struct{}{}
231
}
232
}
233
// more post processing
234
items := make([]string, 0, len(normalized))
235
for k := range normalized {
236
if strings.Contains(k, " ") {
237
parts := strings.Split(strings.ToLower(k), " ")
238
items = append(items, parts...)
239
} else {
240
items = append(items, strings.ToLower(k))
241
}
242
}
243
return sliceutil.Dedupe(items)
244
}
245
246
// getTagsUsingDetectionTemplates returns tags using detection templates
247
func (s *Service) getTagsUsingDetectionTemplates(input *contextargs.MetaInput) ([]string, int) {
248
ctx := context.Background()
249
250
ctxArgs := contextargs.NewWithInput(ctx, input.Input)
251
252
// execute tech detection templates on target
253
tags := map[string]struct{}{}
254
m := &sync.Mutex{}
255
sg, _ := syncutil.New(syncutil.WithSize(s.opts.Options.TemplateThreads))
256
counter := atomic.Uint32{}
257
258
for _, t := range s.techTemplates {
259
sg.Add()
260
go func(template *templates.Template) {
261
defer sg.Done()
262
ctx := scan.NewScanContext(ctx, ctxArgs)
263
ctx.OnResult = func(event *output.InternalWrappedEvent) {
264
if event == nil {
265
return
266
}
267
if event.HasOperatorResult() {
268
// match found
269
// find unique tags
270
m.Lock()
271
for _, v := range event.Results {
272
if v.MatcherName != "" {
273
tags[v.MatcherName] = struct{}{}
274
}
275
for _, tag := range v.Info.Tags.ToSlice() {
276
// we shouldn't add all tags since tags also contain protocol type tags
277
// and are not just limited to products or technologies
278
// ex: tags: js,mssql,detect,network
279
280
// A good trick for this is check if tag is present in template-id
281
if !strings.Contains(template.ID, tag) && !strings.Contains(strings.ToLower(template.Info.Name), tag) {
282
// unlikely this is relevant
283
continue
284
}
285
if _, ok := tags[tag]; !ok {
286
tags[tag] = struct{}{}
287
}
288
// matcher names are also relevant in tech detection templates (ex: tech-detect)
289
for k := range event.OperatorsResult.Matches {
290
if _, ok := tags[k]; !ok {
291
tags[k] = struct{}{}
292
}
293
}
294
}
295
}
296
m.Unlock()
297
_ = counter.Add(1)
298
299
// TBD: should we show or hide tech detection results? what about matcher-status flag?
300
_ = writer.WriteResult(event, s.opts.Output, s.opts.Progress, s.opts.IssuesClient)
301
}
302
}
303
304
_, err := template.Executer.ExecuteWithResults(ctx)
305
if err != nil {
306
gologger.Verbose().Msgf("[%s] error executing template: %s\n", aurora.BrightYellow(template.ID), err)
307
return
308
}
309
}(t)
310
}
311
sg.Wait()
312
return mapsutil.GetKeys(tags), int(counter.Load())
313
}
314
315
// normalizeAppName normalizes app name
316
func normalizeAppName(appName string) string {
317
if strings.Contains(appName, ":") {
318
if parts := strings.Split(appName, ":"); len(parts) == 2 {
319
appName = parts[0]
320
}
321
}
322
return strings.ToLower(appName)
323
}
324
325