Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/protocols/code/code.go
2843 views
1
package code
2
3
import (
4
"bytes"
5
"context"
6
"fmt"
7
"regexp"
8
"strings"
9
"time"
10
11
"github.com/Mzack9999/goja"
12
"github.com/alecthomas/chroma/quick"
13
"github.com/ditashi/jsbeautifier-go/jsbeautifier"
14
15
"github.com/projectdiscovery/gologger"
16
"github.com/projectdiscovery/gozero"
17
"github.com/projectdiscovery/gozero/sandbox"
18
gozerotypes "github.com/projectdiscovery/gozero/types"
19
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
20
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
21
"github.com/projectdiscovery/nuclei/v3/pkg/operators/extractors"
22
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
23
"github.com/projectdiscovery/nuclei/v3/pkg/output"
24
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
25
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
26
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
27
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
28
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
29
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
30
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
31
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
32
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
33
"github.com/projectdiscovery/nuclei/v3/pkg/types"
34
contextutil "github.com/projectdiscovery/utils/context"
35
"github.com/projectdiscovery/utils/errkit"
36
)
37
38
const (
39
pythonEnvRegex = `os\.getenv\(['"]([^'"]+)['"]\)`
40
)
41
42
var (
43
// pythonEnvRegexCompiled is the compiled regex for python environment variables
44
pythonEnvRegexCompiled = regexp.MustCompile(pythonEnvRegex)
45
// ErrCodeExecutionDeadline is the error returned when allotted time for script execution exceeds
46
ErrCodeExecutionDeadline = errkit.New("code execution deadline exceeded").SetKind(errkit.ErrKindDeadline).Build()
47
)
48
49
type Sandbox struct {
50
WorkingDir string `yaml:"working-dir,omitempty" json:"working-dir,omitempty" jsonschema:"title=working-dir,description=Working directory"`
51
Image string `yaml:"image,omitempty" json:"image,omitempty" jsonschema:"title=image,description=Image"`
52
}
53
54
// Request is a request for the SSL protocol
55
type Request struct {
56
// Operators for the current request go here.
57
operators.Operators `yaml:",inline,omitempty"`
58
CompiledOperators *operators.Operators `yaml:"-" json:"-"`
59
60
// ID is the optional id of the request
61
ID string `yaml:"id,omitempty" json:"id,omitempty" jsonschema:"title=id of the request,description=ID is the optional ID of the Request"`
62
// description: |
63
// Engine type
64
Engine []string `yaml:"engine,omitempty" json:"engine,omitempty" jsonschema:"title=engine,description=Engine"`
65
Sandbox *Sandbox `yaml:"sandbox,omitempty" json:"sandbox,omitempty" jsonschema:"title=sandbox,description=Sandbox"`
66
// description: |
67
// PreCondition is a condition which is evaluated before sending the request.
68
PreCondition string `yaml:"pre-condition,omitempty" json:"pre-condition,omitempty" jsonschema:"title=pre-condition for the request,description=PreCondition is a condition which is evaluated before sending the request"`
69
// description: |
70
// Engine Arguments
71
Args []string `yaml:"args,omitempty" json:"args,omitempty" jsonschema:"title=args,description=Args"`
72
// description: |
73
// Pattern preferred for file name
74
Pattern string `yaml:"pattern,omitempty" json:"pattern,omitempty" jsonschema:"title=pattern,description=Pattern"`
75
// description: |
76
// Source File/Snippet
77
Source string `yaml:"source,omitempty" json:"source,omitempty" jsonschema:"title=source file/snippet,description=Source snippet"`
78
79
options *protocols.ExecutorOptions `yaml:"-" json:"-"`
80
preConditionCompiled *goja.Program `yaml:"-" json:"-"`
81
gozero *gozero.Gozero `yaml:"-" json:"-"`
82
src *gozero.Source `yaml:"-" json:"-"`
83
}
84
85
// Compile compiles the request generators preparing any requests possible.
86
func (request *Request) Compile(options *protocols.ExecutorOptions) error {
87
request.options = options
88
89
gozeroOptions := &gozero.Options{
90
Engines: request.Engine,
91
Args: request.Args,
92
EarlyCloseFileDescriptor: true,
93
}
94
95
if options.Options.Debug || options.Options.DebugResponse {
96
// enable debug mode for gozero
97
gozeroOptions.DebugMode = true
98
}
99
100
engine, err := gozero.New(gozeroOptions)
101
if err != nil {
102
errMsg := fmt.Sprintf("[%s] engines '%s' not available on host", options.TemplateID, strings.Join(request.Engine, ","))
103
104
// NOTE(dwisiswant0): In validation mode, skip engine avail check to
105
// allow template validation w/o requiring all engines to be installed
106
// on the host.
107
//
108
// TODO: Ideally, error checking should be done at the highest level
109
// (e.g. runner, main function). For example, we can reuse errors[1][2]
110
// from the `projectdiscovery/gozero` package and wrap (yes, not string
111
// format[3][4]) em inside `projectdiscovery/utils/errors` package to
112
// preserve error semantics and enable runtime type assertion via
113
// builtin `errors.Is` func for granular err handling in the call stack.
114
//
115
// [1]: https://github.com/projectdiscovery/gozero/blob/v0.0.3/gozero.go#L20
116
// [2]: https://github.com/projectdiscovery/gozero/blob/v0.0.3/gozero.go#L35
117
// [3]: https://github.com/projectdiscovery/utils/blob/v0.4.21/errors/enriched.go#L85
118
// [4]: https://github.com/projectdiscovery/utils/blob/v0.4.21/errors/enriched.go#L137
119
if options.Options.Validate {
120
options.Logger.Error().Msgf("%s <- %s", errMsg, err)
121
} else {
122
return errkit.Wrap(err, errMsg)
123
}
124
} else {
125
request.gozero = engine
126
}
127
128
var src *gozero.Source
129
130
src, err = gozero.NewSourceWithString(request.Source, request.Pattern, request.options.TemporaryDirectory)
131
if err != nil {
132
return err
133
}
134
request.src = src
135
136
if len(request.Matchers) > 0 || len(request.Extractors) > 0 {
137
compiled := &request.Operators
138
compiled.ExcludeMatchers = options.ExcludeMatchers
139
compiled.TemplateID = options.TemplateID
140
if err := compiled.Compile(); err != nil {
141
return errkit.Wrap(err, "could not compile operators")
142
}
143
for _, matcher := range compiled.Matchers {
144
// default matcher part for code protocol is response
145
if matcher.Part == "" || matcher.Part == "body" {
146
matcher.Part = "response"
147
}
148
}
149
for _, extractor := range compiled.Extractors {
150
// default extractor part for code protocol is response
151
if extractor.Part == "" || extractor.Part == "body" {
152
extractor.Part = "response"
153
}
154
}
155
request.CompiledOperators = compiled
156
}
157
158
// compile pre-condition if any
159
if request.PreCondition != "" {
160
preConditionCompiled, err := compiler.SourceAutoMode(request.PreCondition, false)
161
if err != nil {
162
return errkit.Newf("could not compile pre-condition: %s", err)
163
}
164
request.preConditionCompiled = preConditionCompiled
165
}
166
return nil
167
}
168
169
// Requests returns the total number of requests the rule will perform
170
func (request *Request) Requests() int {
171
return 1
172
}
173
174
// GetID returns the ID for the request if any.
175
func (request *Request) GetID() string {
176
return request.ID
177
}
178
179
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
180
func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) (err error) {
181
metaSrc, err := gozero.NewSourceWithString(input.MetaInput.Input, "", request.options.TemporaryDirectory)
182
if err != nil {
183
return err
184
}
185
defer func() {
186
if err := metaSrc.Cleanup(); err != nil {
187
gologger.Warning().Msgf("%s\n", err)
188
}
189
}()
190
191
var interactshURLs []string
192
193
// inject all template context values as gozero env allvars
194
allvars := protocolutils.GenerateVariables(input.MetaInput.Input, false, nil)
195
// add template context values if available
196
if request.options.HasTemplateCtx(input.MetaInput) {
197
allvars = generators.MergeMaps(allvars, request.options.GetTemplateCtx(input.MetaInput).GetAll())
198
}
199
// add dynamic and previous variables
200
allvars = generators.MergeMaps(allvars, dynamicValues, previous)
201
// optionvars are vars passed from CLI or env variables
202
optionVars := generators.BuildPayloadFromOptions(request.options.Options)
203
variablesMap := request.options.Variables.Evaluate(allvars)
204
// since we evaluate variables using allvars, give precedence to variablesMap
205
allvars = generators.MergeMaps(allvars, variablesMap, optionVars, request.options.Constants)
206
for name, value := range allvars {
207
v := fmt.Sprint(value)
208
v, interactshURLs = request.options.Interactsh.Replace(v, interactshURLs)
209
// if value is updated by interactsh, update allvars to reflect the change downstream
210
allvars[name] = v
211
metaSrc.AddVariable(gozerotypes.Variable{Name: name, Value: v})
212
}
213
214
if request.PreCondition != "" {
215
if request.options.Options.Debug || request.options.Options.DebugRequests {
216
gologger.Debug().Msgf("[%s] Executing Precondition for Code request\n", request.TemplateID)
217
var highlightFormatter = "terminal256"
218
if request.options.Options.NoColor {
219
highlightFormatter = "text"
220
}
221
var buff bytes.Buffer
222
_ = quick.Highlight(&buff, beautifyJavascript(request.PreCondition), "javascript", highlightFormatter, "monokai")
223
prettyPrint(request.TemplateID, buff.String())
224
}
225
226
args := compiler.NewExecuteArgs()
227
args.TemplateCtx = allvars
228
229
result, err := request.options.JsCompiler.ExecuteWithOptions(request.preConditionCompiled, args,
230
&compiler.ExecuteOptions{
231
ExecutionId: request.options.Options.ExecutionId,
232
TimeoutVariants: request.options.Options.GetTimeouts(),
233
Source: &request.PreCondition,
234
Callback: registerPreConditionFunctions,
235
Cleanup: cleanUpPreConditionFunctions,
236
Context: input.Context(),
237
})
238
if err != nil {
239
return errkit.Newf("could not execute pre-condition: %s", err)
240
}
241
if !result.GetSuccess() || types.ToString(result["error"]) != "" {
242
gologger.Warning().Msgf("[%s] Precondition for request %s was not satisfied\n", request.TemplateID, request.PreCondition)
243
request.options.Progress.IncrementFailedRequestsBy(1)
244
return nil
245
}
246
if request.options.Options.Debug || request.options.Options.DebugRequests {
247
gologger.Debug().Msgf("[%s] Precondition for request was satisfied\n", request.TemplateID)
248
}
249
}
250
251
ctx, cancel := context.WithTimeoutCause(input.Context(), request.options.Options.GetTimeouts().CodeExecutionTimeout, ErrCodeExecutionDeadline)
252
defer cancel()
253
// Note: we use contextutil despite the fact that gozero accepts context as argument
254
gOutput, err := contextutil.ExecFuncWithTwoReturns(ctx, func() (*gozerotypes.Result, error) {
255
if request.useSandbox() {
256
return request.gozero.EvalWithVirtualEnv(
257
ctx, gozero.VirtualEnvDocker,
258
request.src,
259
metaSrc,
260
&sandbox.DockerConfiguration{
261
WorkingDir: request.Sandbox.WorkingDir,
262
Image: request.Sandbox.Image,
263
},
264
)
265
}
266
return request.gozero.Eval(ctx, request.src, metaSrc)
267
})
268
if gOutput == nil {
269
// write error to stderr buff
270
var buff bytes.Buffer
271
if err != nil {
272
buff.WriteString(err.Error())
273
} else {
274
buff.WriteString("no output something went wrong")
275
}
276
gOutput = &gozerotypes.Result{
277
Stderr: buff,
278
}
279
}
280
gologger.Verbose().Msgf("[%s] Executed code on local machine %v", request.options.TemplateID, input.MetaInput.Input)
281
282
if vardump.EnableVarDump {
283
gologger.Debug().Msgf("Code Protocol request variables: %s\n", vardump.DumpVariables(allvars))
284
}
285
286
if request.options.Options.Debug || request.options.Options.DebugRequests {
287
gologger.Debug().MsgFunc(func() string {
288
dashes := strings.Repeat("-", 15)
289
sb := &strings.Builder{}
290
fmt.Fprintf(sb, "[%s] Dumped Executed Source Code for input/stdin: '%v'", request.options.TemplateID, input.MetaInput.Input)
291
fmt.Fprintf(sb, "\n%v\n%v\n%v\n", dashes, "Source Code:", dashes)
292
sb.WriteString(interpretEnvVars(request.Source, allvars))
293
sb.WriteString("\n")
294
fmt.Fprintf(sb, "\n%v\n%v\n%v\n", dashes, "Command Executed:", dashes)
295
sb.WriteString(interpretEnvVars(gOutput.Command, allvars))
296
sb.WriteString("\n")
297
fmt.Fprintf(sb, "\n%v\n%v\n%v\n", dashes, "Command Output:", dashes)
298
sb.WriteString(gOutput.DebugData.String())
299
sb.WriteString("\n")
300
sb.WriteString("[WRN] Command Output here is stdout+sterr, in response variables they are separate (use -v -svd flags for more details)")
301
return sb.String()
302
})
303
}
304
305
dataOutputString := fmtStdout(gOutput.Stdout.String())
306
307
data := make(output.InternalEvent)
308
// also include all request variables in result event
309
for _, value := range metaSrc.Variables {
310
data[value.Name] = value.Value
311
}
312
313
data["type"] = request.Type().String()
314
data["response"] = dataOutputString // response contains filtered output (eg without trailing \n)
315
data["input"] = input.MetaInput.Input
316
data["template-path"] = request.options.TemplatePath
317
data["template-id"] = request.options.TemplateID
318
data["template-info"] = request.options.TemplateInfo
319
if gOutput.Stderr.Len() > 0 {
320
data["stderr"] = fmtStdout(gOutput.Stderr.String())
321
}
322
323
// expose response variables in proto_var format
324
// this is no-op if the template is not a multi protocol template
325
request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, data)
326
327
// add variables from template context before matching/extraction
328
if request.options.HasTemplateCtx(input.MetaInput) {
329
data = generators.MergeMaps(data, request.options.GetTemplateCtx(input.MetaInput).GetAll())
330
}
331
332
if request.options.Interactsh != nil {
333
request.options.Interactsh.MakePlaceholders(interactshURLs, data)
334
}
335
336
// todo #1: interactsh async callback should be eliminated as it lead to ton of code duplication
337
// todo #2: various structs InternalWrappedEvent, InternalEvent should be unwrapped and merged into minimal callbacks and a unique struct (eg. event?)
338
event := eventcreator.CreateEvent(request, data, request.options.Options.Debug || request.options.Options.DebugResponse)
339
if request.options.Interactsh != nil {
340
event.UsesInteractsh = true
341
request.options.Interactsh.RequestEvent(interactshURLs, &interactsh.RequestData{
342
MakeResultFunc: request.MakeResultEvent,
343
Event: event,
344
Operators: request.CompiledOperators,
345
MatchFunc: request.Match,
346
ExtractFunc: request.Extract,
347
})
348
}
349
350
if request.options.Options.Debug || request.options.Options.DebugResponse || request.options.Options.StoreResponse {
351
msg := fmt.Sprintf("[%s] Dumped Code Execution for %s\n\n", request.options.TemplateID, input.MetaInput.Input)
352
if request.options.Options.Debug || request.options.Options.DebugResponse {
353
gologger.Debug().Msg(msg)
354
gologger.Print().Msgf("%s\n\n", responsehighlighter.Highlight(event.OperatorsResult, dataOutputString, request.options.Options.NoColor, false))
355
}
356
if request.options.Options.StoreResponse {
357
request.options.Output.WriteStoreDebugData(input.MetaInput.Input, request.options.TemplateID, request.Type().String(), fmt.Sprintf("%s\n%s", msg, dataOutputString))
358
}
359
}
360
361
callback(event)
362
363
return nil
364
}
365
366
// RequestPartDefinitions contains a mapping of request part definitions and their
367
// description. Multiple definitions are separated by commas.
368
// Definitions not having a name (generated on runtime) are prefixed & suffixed by <>.
369
var RequestPartDefinitions = map[string]string{
370
"type": "Type is the type of request made",
371
"host": "Host is the input to the template",
372
"matched": "Matched is the input which was matched upon",
373
}
374
375
// Match performs matching operation for a matcher on model and returns:
376
// true and a list of matched snippets if the matcher type is supports it
377
// otherwise false and an empty string slice
378
func (request *Request) Match(data map[string]interface{}, matcher *matchers.Matcher) (bool, []string) {
379
return protocols.MakeDefaultMatchFunc(data, matcher)
380
}
381
382
// Extract performs extracting operation for an extractor on model and returns true or false.
383
func (request *Request) Extract(data map[string]interface{}, matcher *extractors.Extractor) map[string]struct{} {
384
return protocols.MakeDefaultExtractFunc(data, matcher)
385
}
386
387
// MakeResultEvent creates a result event from internal wrapped event
388
func (request *Request) MakeResultEvent(wrapped *output.InternalWrappedEvent) []*output.ResultEvent {
389
return protocols.MakeDefaultResultEvent(request, wrapped)
390
}
391
392
// GetCompiledOperators returns a list of the compiled operators
393
func (request *Request) GetCompiledOperators() []*operators.Operators {
394
return []*operators.Operators{request.CompiledOperators}
395
}
396
397
// Type returns the type of the protocol request
398
func (request *Request) Type() templateTypes.ProtocolType {
399
return templateTypes.CodeProtocol
400
}
401
402
func (request *Request) MakeResultEventItem(wrapped *output.InternalWrappedEvent) *output.ResultEvent {
403
fields := protocolutils.GetJsonFieldsFromURL(types.ToString(wrapped.InternalEvent["input"]))
404
if types.ToString(wrapped.InternalEvent["ip"]) != "" {
405
fields.Ip = types.ToString(wrapped.InternalEvent["ip"])
406
}
407
data := &output.ResultEvent{
408
TemplateID: types.ToString(request.options.TemplateID),
409
TemplatePath: types.ToString(request.options.TemplatePath),
410
Info: request.options.TemplateInfo,
411
TemplateVerifier: request.options.TemplateVerifier,
412
Type: types.ToString(wrapped.InternalEvent["type"]),
413
Matched: types.ToString(wrapped.InternalEvent["input"]),
414
Host: fields.Host,
415
Port: fields.Port,
416
Scheme: fields.Scheme,
417
URL: fields.URL,
418
IP: fields.Ip,
419
Metadata: wrapped.OperatorsResult.PayloadValues,
420
ExtractedResults: wrapped.OperatorsResult.OutputExtracts,
421
Timestamp: time.Now(),
422
MatcherStatus: true,
423
TemplateEncoded: request.options.EncodeTemplate(),
424
Error: types.ToString(wrapped.InternalEvent["error"]),
425
}
426
return data
427
}
428
429
func fmtStdout(data string) string {
430
return strings.Trim(data, " \n\r\t")
431
}
432
433
// interpretEnvVars replaces environment variables in the input string
434
func interpretEnvVars(source string, vars map[string]interface{}) string {
435
// bash mode
436
if strings.Contains(source, "$") {
437
for k, v := range vars {
438
source = strings.ReplaceAll(source, "$"+k, fmt.Sprintf("%s", v))
439
}
440
}
441
// python mode
442
if strings.Contains(source, "os.getenv") {
443
matches := pythonEnvRegexCompiled.FindAllStringSubmatch(source, -1)
444
for _, match := range matches {
445
if len(match) == 0 {
446
continue
447
}
448
source = strings.ReplaceAll(source, fmt.Sprintf("os.getenv('%s')", match), fmt.Sprintf("'%s'", vars[match[0]]))
449
}
450
}
451
return source
452
}
453
454
func beautifyJavascript(code string) string {
455
opts := jsbeautifier.DefaultOptions()
456
beautified, err := jsbeautifier.Beautify(&code, opts)
457
if err != nil {
458
return code
459
}
460
return beautified
461
}
462
463
func prettyPrint(templateId string, buff string) {
464
lines := strings.Split(buff, "\n")
465
final := []string{}
466
for _, v := range lines {
467
if v != "" {
468
final = append(final, "\t"+v)
469
}
470
}
471
gologger.Debug().Msgf(" [%v] Pre-condition Code:\n\n%v\n\n", templateId, strings.Join(final, "\n"))
472
}
473
474
// UpdateOptions replaces this request's options with a new copy
475
func (r *Request) UpdateOptions(opts *protocols.ExecutorOptions) {
476
r.options.ApplyNewEngineOptions(opts)
477
}
478
479
func (r *Request) useSandbox() bool {
480
return r.Sandbox != nil && r.Sandbox.Image != ""
481
}
482
483