Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/templates/compile.go
2070 views
1
package templates
2
3
import (
4
"bytes"
5
"fmt"
6
"io"
7
"reflect"
8
"sync"
9
"sync/atomic"
10
11
"github.com/logrusorgru/aurora"
12
"github.com/pkg/errors"
13
"gopkg.in/yaml.v2"
14
15
"github.com/projectdiscovery/gologger"
16
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
17
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
18
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
19
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
20
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
23
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/offlinehttp"
24
"github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
25
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec"
26
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
27
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
28
"github.com/projectdiscovery/utils/errkit"
29
stringsutil "github.com/projectdiscovery/utils/strings"
30
)
31
32
var (
33
ErrCreateTemplateExecutor = errors.New("cannot create template executer")
34
ErrIncompatibleWithOfflineMatching = errors.New("template can't be used for offline matching")
35
// track how many templates are verfied and by which signer
36
SignatureStats = map[string]*atomic.Uint64{}
37
)
38
39
const (
40
Unsigned = "unsigned"
41
)
42
43
func init() {
44
for _, verifier := range signer.DefaultTemplateVerifiers {
45
SignatureStats[verifier.Identifier()] = &atomic.Uint64{}
46
}
47
SignatureStats[Unsigned] = &atomic.Uint64{}
48
}
49
50
// Parse parses a yaml request template file
51
// TODO make sure reading from the disk the template parsing happens once: see parsers.ParseTemplate vs templates.Parse
52
func Parse(filePath string, preprocessor Preprocessor, options *protocols.ExecutorOptions) (*Template, error) {
53
parser, ok := options.Parser.(*Parser)
54
if !ok {
55
panic("not a parser")
56
}
57
if !options.DoNotCache {
58
if value, _, _ := parser.compiledTemplatesCache.Has(filePath); value != nil {
59
// Copy the template, apply new options, and recompile requests
60
tplCopy := *value
61
newBase := options.Copy()
62
newBase.TemplateID = tplCopy.Options.TemplateID
63
newBase.TemplatePath = tplCopy.Options.TemplatePath
64
newBase.TemplateInfo = tplCopy.Options.TemplateInfo
65
newBase.TemplateVerifier = tplCopy.Options.TemplateVerifier
66
newBase.RawTemplate = tplCopy.Options.RawTemplate
67
68
if tplCopy.Options.Variables.Len() > 0 {
69
newBase.Variables = tplCopy.Options.Variables
70
}
71
if len(tplCopy.Options.Constants) > 0 {
72
newBase.Constants = tplCopy.Options.Constants
73
}
74
tplCopy.Options = newBase
75
76
tplCopy.Options.ApplyNewEngineOptions(options)
77
if tplCopy.CompiledWorkflow != nil {
78
tplCopy.CompiledWorkflow.Options.ApplyNewEngineOptions(options)
79
for _, w := range tplCopy.CompiledWorkflow.Workflows {
80
for _, ex := range w.Executers {
81
ex.Options.ApplyNewEngineOptions(options)
82
}
83
}
84
}
85
86
// TODO: Reconsider whether to recompile requests. Compiling these is just as slow
87
// as not using a cache at all, but may be necessary.
88
89
for i, r := range tplCopy.RequestsDNS {
90
rCopy := *r
91
rCopy.UpdateOptions(tplCopy.Options)
92
// rCopy.Compile(tplCopy.Options)
93
tplCopy.RequestsDNS[i] = &rCopy
94
}
95
for i, r := range tplCopy.RequestsHTTP {
96
rCopy := *r
97
rCopy.UpdateOptions(tplCopy.Options)
98
// rCopy.Compile(tplCopy.Options)
99
tplCopy.RequestsHTTP[i] = &rCopy
100
}
101
for i, r := range tplCopy.RequestsCode {
102
rCopy := *r
103
rCopy.UpdateOptions(tplCopy.Options)
104
// rCopy.Compile(tplCopy.Options)
105
tplCopy.RequestsCode[i] = &rCopy
106
}
107
for i, r := range tplCopy.RequestsFile {
108
rCopy := *r
109
rCopy.UpdateOptions(tplCopy.Options)
110
// rCopy.Compile(tplCopy.Options)
111
tplCopy.RequestsFile[i] = &rCopy
112
}
113
for i, r := range tplCopy.RequestsHeadless {
114
rCopy := *r
115
rCopy.UpdateOptions(tplCopy.Options)
116
// rCopy.Compile(tplCopy.Options)
117
tplCopy.RequestsHeadless[i] = &rCopy
118
}
119
for i, r := range tplCopy.RequestsNetwork {
120
rCopy := *r
121
rCopy.UpdateOptions(tplCopy.Options)
122
// rCopy.Compile(tplCopy.Options)
123
tplCopy.RequestsNetwork[i] = &rCopy
124
}
125
for i, r := range tplCopy.RequestsJavascript {
126
rCopy := *r
127
rCopy.UpdateOptions(tplCopy.Options)
128
//rCopy.Compile(tplCopy.Options)
129
tplCopy.RequestsJavascript[i] = &rCopy
130
}
131
for i, r := range tplCopy.RequestsSSL {
132
rCopy := *r
133
rCopy.UpdateOptions(tplCopy.Options)
134
// rCopy.Compile(tplCopy.Options)
135
tplCopy.RequestsSSL[i] = &rCopy
136
}
137
for i, r := range tplCopy.RequestsWHOIS {
138
rCopy := *r
139
rCopy.UpdateOptions(tplCopy.Options)
140
// rCopy.Compile(tplCopy.Options)
141
tplCopy.RequestsWHOIS[i] = &rCopy
142
}
143
for i, r := range tplCopy.RequestsWebsocket {
144
rCopy := *r
145
rCopy.UpdateOptions(tplCopy.Options)
146
// rCopy.Compile(tplCopy.Options)
147
tplCopy.RequestsWebsocket[i] = &rCopy
148
}
149
template := &tplCopy
150
151
if template.isGlobalMatchersEnabled() {
152
item := &globalmatchers.Item{
153
TemplateID: template.ID,
154
TemplatePath: filePath,
155
TemplateInfo: template.Info,
156
}
157
for _, request := range template.RequestsHTTP {
158
item.Operators = append(item.Operators, request.CompiledOperators)
159
}
160
options.GlobalMatchers.AddOperator(item)
161
return nil, nil
162
}
163
// Compile the workflow request
164
if len(template.Workflows) > 0 {
165
compiled := &template.Workflow
166
compileWorkflow(filePath, preprocessor, tplCopy.Options, compiled, tplCopy.Options.WorkflowLoader)
167
template.CompiledWorkflow = compiled
168
template.CompiledWorkflow.Options = tplCopy.Options
169
}
170
171
if isCachedTemplateValid(template) {
172
// options.Logger.Error().Msgf("returning cached template %s after recompiling %d requests", tplCopy.Options.TemplateID, tplCopy.Requests())
173
return template, nil
174
}
175
// else: fallthrough to re-parse template from scratch
176
}
177
}
178
179
var reader io.ReadCloser
180
if !options.DoNotCache {
181
_, raw, err := parser.parsedTemplatesCache.Has(filePath)
182
if err == nil && raw != nil {
183
reader = io.NopCloser(bytes.NewReader(raw))
184
}
185
}
186
var err error
187
if reader == nil {
188
reader, err = utils.ReaderFromPathOrURL(filePath, options.Catalog)
189
if err != nil {
190
return nil, err
191
}
192
}
193
194
defer func() {
195
_ = reader.Close()
196
}()
197
198
// Make a copy of the options for this template
199
options = options.Copy()
200
options.TemplatePath = filePath
201
template, err := ParseTemplateFromReader(reader, preprocessor, options)
202
if err != nil {
203
return nil, err
204
}
205
if template.isGlobalMatchersEnabled() {
206
item := &globalmatchers.Item{
207
TemplateID: template.ID,
208
TemplatePath: filePath,
209
TemplateInfo: template.Info,
210
}
211
for _, request := range template.RequestsHTTP {
212
item.Operators = append(item.Operators, request.CompiledOperators)
213
}
214
options.GlobalMatchers.AddOperator(item)
215
return nil, nil
216
}
217
// Compile the workflow request
218
if len(template.Workflows) > 0 {
219
compiled := &template.Workflow
220
221
compileWorkflow(filePath, preprocessor, options, compiled, options.WorkflowLoader)
222
template.CompiledWorkflow = compiled
223
template.CompiledWorkflow.Options = options
224
}
225
template.Path = filePath
226
if !options.DoNotCache {
227
parser.compiledTemplatesCache.Store(filePath, template, nil, err)
228
}
229
return template, nil
230
}
231
232
// isGlobalMatchersEnabled checks if any of requests in the template
233
// have global matchers enabled. It iterates through all requests and
234
// returns true if at least one request has global matchers enabled;
235
// otherwise, it returns false. If global matchers templates are not
236
// enabled in the options, the method will immediately return false.
237
//
238
// Note: This method only checks the `RequestsHTTP`
239
// field of the template, which is specific to http-protocol-based
240
// templates.
241
//
242
// TODO: support all protocols.
243
func (template *Template) isGlobalMatchersEnabled() bool {
244
if !template.Options.Options.EnableGlobalMatchersTemplates {
245
return false
246
}
247
248
for _, request := range template.RequestsHTTP {
249
if request.GlobalMatchers {
250
return true
251
}
252
}
253
return false
254
}
255
256
// parseSelfContainedRequests parses the self contained template requests.
257
func (template *Template) parseSelfContainedRequests() {
258
if template.Signature.Value.String() != "" {
259
for _, request := range template.RequestsHTTP {
260
request.Signature = template.Signature
261
}
262
}
263
if !template.SelfContained {
264
return
265
}
266
for _, request := range template.RequestsHTTP {
267
request.SelfContained = true
268
}
269
for _, request := range template.RequestsNetwork {
270
request.SelfContained = true
271
}
272
for _, request := range template.RequestsHeadless {
273
request.SelfContained = true
274
}
275
}
276
277
// Requests returns the total request count for the template
278
func (template *Template) Requests() int {
279
return len(template.RequestsDNS) +
280
len(template.RequestsHTTP) +
281
len(template.RequestsFile) +
282
len(template.RequestsNetwork) +
283
len(template.RequestsHeadless) +
284
len(template.Workflows) +
285
len(template.RequestsSSL) +
286
len(template.RequestsWebsocket) +
287
len(template.RequestsWHOIS) +
288
len(template.RequestsCode) +
289
len(template.RequestsJavascript)
290
}
291
292
// compileProtocolRequests compiles all the protocol requests for the template
293
func (template *Template) compileProtocolRequests(options *protocols.ExecutorOptions) error {
294
templateRequests := template.Requests()
295
296
if templateRequests == 0 {
297
return fmt.Errorf("no requests defined for %s", template.ID)
298
}
299
300
if options.Options.OfflineHTTP {
301
return template.compileOfflineHTTPRequest(options)
302
}
303
304
var requests []protocols.Request
305
306
if template.hasMultipleRequests() {
307
// when multiple requests are present preserve the order of requests and protocols
308
// which is already done during unmarshalling
309
requests = template.RequestsQueue
310
if options.Flow == "" {
311
options.IsMultiProtocol = true
312
}
313
} else {
314
if len(template.RequestsDNS) > 0 {
315
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsDNS)...)
316
}
317
if len(template.RequestsFile) > 0 {
318
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsFile)...)
319
}
320
if len(template.RequestsNetwork) > 0 {
321
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsNetwork)...)
322
}
323
if len(template.RequestsHTTP) > 0 {
324
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsHTTP)...)
325
}
326
if len(template.RequestsHeadless) > 0 && options.Options.Headless {
327
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsHeadless)...)
328
}
329
if len(template.RequestsSSL) > 0 {
330
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsSSL)...)
331
}
332
if len(template.RequestsWebsocket) > 0 {
333
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsWebsocket)...)
334
}
335
if len(template.RequestsWHOIS) > 0 {
336
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsWHOIS)...)
337
}
338
if len(template.RequestsCode) > 0 && options.Options.EnableCodeTemplates {
339
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsCode)...)
340
}
341
if len(template.RequestsJavascript) > 0 {
342
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsJavascript)...)
343
}
344
}
345
var err error
346
template.Executer, err = tmplexec.NewTemplateExecuter(requests, options)
347
return err
348
}
349
350
// convertRequestToProtocolsRequest is a convenience wrapper to convert
351
// arbitrary interfaces which are slices of requests from the template to a
352
// slice of protocols.Request interface items.
353
func (template *Template) convertRequestToProtocolsRequest(requests interface{}) []protocols.Request {
354
switch reflect.TypeOf(requests).Kind() {
355
case reflect.Slice:
356
s := reflect.ValueOf(requests)
357
358
requestSlice := make([]protocols.Request, s.Len())
359
for i := 0; i < s.Len(); i++ {
360
value := s.Index(i)
361
valueInterface := value.Interface()
362
requestSlice[i] = valueInterface.(protocols.Request)
363
}
364
return requestSlice
365
}
366
return nil
367
}
368
369
// compileOfflineHTTPRequest iterates all requests if offline http mode is
370
// specified and collects all matchers for all the base request templates
371
// (those with URL {{BaseURL}} and it's slash variation.)
372
func (template *Template) compileOfflineHTTPRequest(options *protocols.ExecutorOptions) error {
373
operatorsList := []*operators.Operators{}
374
375
mainLoop:
376
for _, req := range template.RequestsHTTP {
377
hasPaths := len(req.Path) > 0
378
if !hasPaths {
379
break mainLoop
380
}
381
for _, path := range req.Path {
382
pathIsBaseURL := stringsutil.EqualFoldAny(path, "{{BaseURL}}", "{{BaseURL}}/", "/")
383
if !pathIsBaseURL {
384
break mainLoop
385
}
386
}
387
operatorsList = append(operatorsList, &req.Operators)
388
}
389
if len(operatorsList) > 0 {
390
options.Operators = operatorsList
391
var err error
392
template.Executer, err = tmplexec.NewTemplateExecuter([]protocols.Request{&offlinehttp.Request{}}, options)
393
if err != nil {
394
// it seems like flow executor cannot be used for offline http matching (ex:http(1) && http(2))
395
return ErrIncompatibleWithOfflineMatching
396
}
397
return err
398
}
399
400
return ErrIncompatibleWithOfflineMatching
401
}
402
403
// ParseTemplateFromReader reads the template from reader
404
// returns the parsed template
405
func ParseTemplateFromReader(reader io.Reader, preprocessor Preprocessor, options *protocols.ExecutorOptions) (*Template, error) {
406
data, err := io.ReadAll(reader)
407
if err != nil {
408
return nil, err
409
}
410
411
// a preprocessor is a variable like
412
// {{randstr}} which is replaced before unmarshalling
413
// as it is known to be a random static value per template
414
hasPreprocessor := false
415
allPreprocessors := getPreprocessors(preprocessor)
416
for _, preprocessor := range allPreprocessors {
417
if preprocessor.Exists(data) {
418
hasPreprocessor = true
419
break
420
}
421
}
422
423
if !hasPreprocessor {
424
// if no preprocessors exists parse template and exit
425
template, err := parseTemplate(data, options)
426
if err != nil {
427
return nil, err
428
}
429
if !template.Verified && len(template.Workflows) == 0 {
430
if config.DefaultConfig.LogAllEvents {
431
gologger.DefaultLogger.Print().Msgf("[%v] Template %s is not signed or tampered\n", aurora.Yellow("WRN").String(), template.ID)
432
}
433
}
434
return template, nil
435
}
436
437
// if preprocessor is required / exists in this template
438
// first unmarshal it and check if its verified
439
// persist verified status value and then
440
// expand all preprocessor and reparse template
441
442
// === signature verification before preprocessors ===
443
template, err := parseTemplate(data, options)
444
if err != nil {
445
return nil, err
446
}
447
isVerified := template.Verified
448
if !template.Verified && len(template.Workflows) == 0 {
449
// workflows are not signed by default
450
if config.DefaultConfig.LogAllEvents {
451
gologger.DefaultLogger.Print().Msgf("[%v] Template %s is not signed or tampered\n", aurora.Yellow("WRN").String(), template.ID)
452
}
453
}
454
455
generatedConstants := map[string]interface{}{}
456
// ==== execute preprocessors ======
457
for _, v := range allPreprocessors {
458
var replaced map[string]interface{}
459
data, replaced = v.ProcessNReturnData(data)
460
// preprocess kind of act like a constant and are generated while loading
461
// and stay constant for the template lifecycle
462
generatedConstants = generators.MergeMaps(generatedConstants, replaced)
463
}
464
reParsed, err := parseTemplate(data, options)
465
if err != nil {
466
return nil, err
467
}
468
// add generated constants to constants map and executer options
469
reParsed.Constants = generators.MergeMaps(reParsed.Constants, generatedConstants)
470
reParsed.Options.Constants = reParsed.Constants
471
reParsed.Verified = isVerified
472
return reParsed, nil
473
}
474
475
// this method does not include any kind of preprocessing
476
func parseTemplate(data []byte, srcOptions *protocols.ExecutorOptions) (*Template, error) {
477
// Create a copy of the options specifically for this template
478
options := srcOptions.Copy()
479
480
template := &Template{}
481
var err error
482
switch config.GetTemplateFormatFromExt(template.Path) {
483
case config.JSON:
484
err = json.Unmarshal(data, template)
485
case config.YAML:
486
err = yaml.Unmarshal(data, template)
487
default:
488
// assume its yaml
489
if err = yaml.Unmarshal(data, template); err != nil {
490
return nil, err
491
}
492
}
493
if err != nil {
494
return nil, errkit.Wrapf(err, "failed to parse %s", template.Path)
495
}
496
497
if utils.IsBlank(template.Info.Name) {
498
return nil, errors.New("no template name field provided")
499
}
500
if template.Info.Authors.IsEmpty() {
501
return nil, errors.New("no template author field provided")
502
}
503
504
numberOfWorkflows := len(template.Workflows)
505
if numberOfWorkflows > 0 && numberOfWorkflows != template.Requests() {
506
return nil, errors.New("workflows cannot have other protocols")
507
}
508
509
// use default unknown severity
510
if len(template.Workflows) == 0 {
511
if template.Info.SeverityHolder.Severity == severity.Undefined {
512
// set unknown severity with counter and forced warning
513
template.Info.SeverityHolder.Severity = severity.Unknown
514
if options.Options.Validate {
515
// when validating return error
516
return nil, errors.New("no template severity field provided")
517
}
518
}
519
}
520
521
// Setting up variables regarding template metadata
522
options.TemplateID = template.ID
523
options.TemplateInfo = template.Info
524
options.StopAtFirstMatch = template.StopAtFirstMatch
525
526
if template.Variables.Len() > 0 {
527
options.Variables = template.Variables
528
}
529
530
// if more than 1 request per protocol exist we add request id to protocol request
531
// since in template context we have proto_prefix for each protocol it is overwritten
532
// if request id is not present
533
template.validateAllRequestIDs()
534
535
// create empty context args for template scope
536
options.CreateTemplateCtxStore()
537
options.ProtocolType = template.Type()
538
options.Constants = template.Constants
539
540
// initialize the js compiler if missing
541
if options.JsCompiler == nil {
542
options.JsCompiler = GetJsCompiler() // this is a singleton
543
}
544
545
template.Options = options
546
// If no requests, and it is also not a workflow, return error.
547
if template.Requests() == 0 {
548
return nil, fmt.Errorf("no requests defined for %s", template.ID)
549
}
550
551
// load `flow` and `source` in code protocol from file
552
// if file is referenced instead of actual source code
553
if err := template.ImportFileRefs(template.Options); err != nil {
554
return nil, errkit.Wrapf(err, "failed to load file refs for %s", template.ID)
555
}
556
557
if err := template.compileProtocolRequests(template.Options); err != nil {
558
return nil, err
559
}
560
561
if template.Executer != nil {
562
if err := template.Executer.Compile(); err != nil {
563
return nil, errors.Wrap(err, "could not compile request")
564
}
565
template.TotalRequests = template.Executer.Requests()
566
}
567
if template.Executer == nil && template.CompiledWorkflow == nil {
568
return nil, ErrCreateTemplateExecutor
569
}
570
template.parseSelfContainedRequests()
571
572
// check if the template is verified
573
// only valid templates can be verified or signed
574
var verifier *signer.TemplateSigner
575
for _, verifier = range signer.DefaultTemplateVerifiers {
576
template.Verified, _ = verifier.Verify(data, template)
577
if config.DefaultConfig.LogAllEvents {
578
gologger.Verbose().Msgf("template %v verified by %s : %v", template.ID, verifier.Identifier(), template.Verified)
579
}
580
if template.Verified {
581
template.TemplateVerifier = verifier.Identifier()
582
break
583
}
584
}
585
options.TemplateVerifier = template.TemplateVerifier
586
//nolint
587
if !(template.Verified && verifier.Identifier() == "projectdiscovery/nuclei-templates") {
588
template.Options.RawTemplate = data
589
}
590
return template, nil
591
}
592
593
// isCachedTemplateValid validates that a cached template is still usable after
594
// option updates
595
func isCachedTemplateValid(template *Template) bool {
596
// no requests or workflows
597
if template.Requests() == 0 && len(template.Workflows) == 0 {
598
return false
599
}
600
601
// options not initialized
602
if template.Options == nil {
603
return false
604
}
605
606
// executer not available for non-workflow template
607
if len(template.Workflows) == 0 && template.Executer == nil {
608
return false
609
}
610
611
// compiled workflow not available
612
if len(template.Workflows) > 0 && template.CompiledWorkflow == nil {
613
return false
614
}
615
616
// template ID mismatch
617
if template.Options.TemplateID != template.ID {
618
return false
619
}
620
621
// executer exists but no requests or flow available
622
if template.Executer != nil {
623
// NOTE(dwisiswant0): This is a basic sanity check since we can't access
624
// private fields, but we can check requests tho
625
if template.Requests() == 0 && template.Options.Flow == "" {
626
return false
627
}
628
}
629
630
if template.Options.Options == nil {
631
return false
632
}
633
634
return true
635
}
636
637
var (
638
jsCompiler *compiler.Compiler
639
jsCompilerOnce = sync.OnceFunc(func() {
640
jsCompiler = compiler.New()
641
})
642
)
643
644
func GetJsCompiler() *compiler.Compiler {
645
jsCompilerOnce()
646
return jsCompiler
647
}
648
649