package code
import (
"bytes"
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/Mzack9999/goja"
"github.com/alecthomas/chroma/quick"
"github.com/ditashi/jsbeautifier-go/jsbeautifier"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gozero"
gozerotypes "github.com/projectdiscovery/gozero/types"
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/operators/extractors"
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
contextutil "github.com/projectdiscovery/utils/context"
"github.com/projectdiscovery/utils/errkit"
)
const (
pythonEnvRegex = `os\.getenv\(['"]([^'"]+)['"]\)`
)
var (
pythonEnvRegexCompiled = regexp.MustCompile(pythonEnvRegex)
ErrCodeExecutionDeadline = errkit.New("code execution deadline exceeded").SetKind(errkit.ErrKindDeadline).Build()
)
type Request struct {
operators.Operators `yaml:",inline,omitempty"`
CompiledOperators *operators.Operators `yaml:"-" json:"-"`
ID string `yaml:"id,omitempty" json:"id,omitempty" jsonschema:"title=id of the request,description=ID is the optional ID of the Request"`
Engine []string `yaml:"engine,omitempty" json:"engine,omitempty" jsonschema:"title=engine,description=Engine"`
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"`
Args []string `yaml:"args,omitempty" json:"args,omitempty" jsonschema:"title=args,description=Args"`
Pattern string `yaml:"pattern,omitempty" json:"pattern,omitempty" jsonschema:"title=pattern,description=Pattern"`
Source string `yaml:"source,omitempty" json:"source,omitempty" jsonschema:"title=source file/snippet,description=Source snippet"`
options *protocols.ExecutorOptions `yaml:"-" json:"-"`
preConditionCompiled *goja.Program `yaml:"-" json:"-"`
gozero *gozero.Gozero `yaml:"-" json:"-"`
src *gozero.Source `yaml:"-" json:"-"`
}
func (request *Request) Compile(options *protocols.ExecutorOptions) error {
request.options = options
gozeroOptions := &gozero.Options{
Engines: request.Engine,
Args: request.Args,
EarlyCloseFileDescriptor: true,
}
if options.Options.Debug || options.Options.DebugResponse {
gozeroOptions.DebugMode = true
}
engine, err := gozero.New(gozeroOptions)
if err != nil {
errMsg := fmt.Sprintf("[%s] engines '%s' not available on host", options.TemplateID, strings.Join(request.Engine, ","))
if options.Options.Validate {
options.Logger.Error().Msgf("%s <- %s", errMsg, err)
} else {
return errkit.Wrap(err, errMsg)
}
} else {
request.gozero = engine
}
var src *gozero.Source
src, err = gozero.NewSourceWithString(request.Source, request.Pattern, request.options.TemporaryDirectory)
if err != nil {
return err
}
request.src = src
if len(request.Matchers) > 0 || len(request.Extractors) > 0 {
compiled := &request.Operators
compiled.ExcludeMatchers = options.ExcludeMatchers
compiled.TemplateID = options.TemplateID
if err := compiled.Compile(); err != nil {
return errkit.Wrap(err, "could not compile operators")
}
for _, matcher := range compiled.Matchers {
if matcher.Part == "" || matcher.Part == "body" {
matcher.Part = "response"
}
}
for _, extractor := range compiled.Extractors {
if extractor.Part == "" || extractor.Part == "body" {
extractor.Part = "response"
}
}
request.CompiledOperators = compiled
}
if request.PreCondition != "" {
preConditionCompiled, err := compiler.SourceAutoMode(request.PreCondition, false)
if err != nil {
return errkit.Newf("could not compile pre-condition: %s", err)
}
request.preConditionCompiled = preConditionCompiled
}
return nil
}
func (request *Request) Requests() int {
return 1
}
func (request *Request) GetID() string {
return request.ID
}
func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) (err error) {
metaSrc, err := gozero.NewSourceWithString(input.MetaInput.Input, "", request.options.TemporaryDirectory)
if err != nil {
return err
}
defer func() {
if err := metaSrc.Cleanup(); err != nil {
gologger.Warning().Msgf("%s\n", err)
}
}()
var interactshURLs []string
allvars := protocolutils.GenerateVariables(input.MetaInput.Input, false, nil)
if request.options.HasTemplateCtx(input.MetaInput) {
allvars = generators.MergeMaps(allvars, request.options.GetTemplateCtx(input.MetaInput).GetAll())
}
allvars = generators.MergeMaps(allvars, dynamicValues, previous)
optionVars := generators.BuildPayloadFromOptions(request.options.Options)
variablesMap := request.options.Variables.Evaluate(allvars)
allvars = generators.MergeMaps(allvars, variablesMap, optionVars, request.options.Constants)
for name, value := range allvars {
v := fmt.Sprint(value)
v, interactshURLs = request.options.Interactsh.Replace(v, interactshURLs)
allvars[name] = v
metaSrc.AddVariable(gozerotypes.Variable{Name: name, Value: v})
}
if request.PreCondition != "" {
if request.options.Options.Debug || request.options.Options.DebugRequests {
gologger.Debug().Msgf("[%s] Executing Precondition for Code request\n", request.TemplateID)
var highlightFormatter = "terminal256"
if request.options.Options.NoColor {
highlightFormatter = "text"
}
var buff bytes.Buffer
_ = quick.Highlight(&buff, beautifyJavascript(request.PreCondition), "javascript", highlightFormatter, "monokai")
prettyPrint(request.TemplateID, buff.String())
}
args := compiler.NewExecuteArgs()
args.TemplateCtx = allvars
result, err := request.options.JsCompiler.ExecuteWithOptions(request.preConditionCompiled, args,
&compiler.ExecuteOptions{
ExecutionId: request.options.Options.ExecutionId,
TimeoutVariants: request.options.Options.GetTimeouts(),
Source: &request.PreCondition,
Callback: registerPreConditionFunctions,
Cleanup: cleanUpPreConditionFunctions,
Context: input.Context(),
})
if err != nil {
return errkit.Newf("could not execute pre-condition: %s", err)
}
if !result.GetSuccess() || types.ToString(result["error"]) != "" {
gologger.Warning().Msgf("[%s] Precondition for request %s was not satisfied\n", request.TemplateID, request.PreCondition)
request.options.Progress.IncrementFailedRequestsBy(1)
return nil
}
if request.options.Options.Debug || request.options.Options.DebugRequests {
gologger.Debug().Msgf("[%s] Precondition for request was satisfied\n", request.TemplateID)
}
}
ctx, cancel := context.WithTimeoutCause(input.Context(), request.options.Options.GetTimeouts().CodeExecutionTimeout, ErrCodeExecutionDeadline)
defer cancel()
gOutput, err := contextutil.ExecFuncWithTwoReturns(ctx, func() (*gozerotypes.Result, error) {
return request.gozero.Eval(ctx, request.src, metaSrc)
})
if gOutput == nil {
var buff bytes.Buffer
if err != nil {
buff.WriteString(err.Error())
} else {
buff.WriteString("no output something went wrong")
}
gOutput = &gozerotypes.Result{
Stderr: buff,
}
}
gologger.Verbose().Msgf("[%s] Executed code on local machine %v", request.options.TemplateID, input.MetaInput.Input)
if vardump.EnableVarDump {
gologger.Debug().Msgf("Code Protocol request variables: %s\n", vardump.DumpVariables(allvars))
}
if request.options.Options.Debug || request.options.Options.DebugRequests {
gologger.Debug().MsgFunc(func() string {
dashes := strings.Repeat("-", 15)
sb := &strings.Builder{}
fmt.Fprintf(sb, "[%s] Dumped Executed Source Code for input/stdin: '%v'", request.options.TemplateID, input.MetaInput.Input)
fmt.Fprintf(sb, "\n%v\n%v\n%v\n", dashes, "Source Code:", dashes)
sb.WriteString(interpretEnvVars(request.Source, allvars))
sb.WriteString("\n")
fmt.Fprintf(sb, "\n%v\n%v\n%v\n", dashes, "Command Executed:", dashes)
sb.WriteString(interpretEnvVars(gOutput.Command, allvars))
sb.WriteString("\n")
fmt.Fprintf(sb, "\n%v\n%v\n%v\n", dashes, "Command Output:", dashes)
sb.WriteString(gOutput.DebugData.String())
sb.WriteString("\n")
sb.WriteString("[WRN] Command Output here is stdout+sterr, in response variables they are seperate (use -v -svd flags for more details)")
return sb.String()
})
}
dataOutputString := fmtStdout(gOutput.Stdout.String())
data := make(output.InternalEvent)
for _, value := range metaSrc.Variables {
data[value.Name] = value.Value
}
data["type"] = request.Type().String()
data["response"] = dataOutputString
data["input"] = input.MetaInput.Input
data["template-path"] = request.options.TemplatePath
data["template-id"] = request.options.TemplateID
data["template-info"] = request.options.TemplateInfo
if gOutput.Stderr.Len() > 0 {
data["stderr"] = fmtStdout(gOutput.Stderr.String())
}
request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, data)
if request.options.HasTemplateCtx(input.MetaInput) {
data = generators.MergeMaps(data, request.options.GetTemplateCtx(input.MetaInput).GetAll())
}
if request.options.Interactsh != nil {
request.options.Interactsh.MakePlaceholders(interactshURLs, data)
}
event := eventcreator.CreateEvent(request, data, request.options.Options.Debug || request.options.Options.DebugResponse)
if request.options.Interactsh != nil {
event.UsesInteractsh = true
request.options.Interactsh.RequestEvent(interactshURLs, &interactsh.RequestData{
MakeResultFunc: request.MakeResultEvent,
Event: event,
Operators: request.CompiledOperators,
MatchFunc: request.Match,
ExtractFunc: request.Extract,
})
}
if request.options.Options.Debug || request.options.Options.DebugResponse || request.options.Options.StoreResponse {
msg := fmt.Sprintf("[%s] Dumped Code Execution for %s\n\n", request.options.TemplateID, input.MetaInput.Input)
if request.options.Options.Debug || request.options.Options.DebugResponse {
gologger.Debug().Msg(msg)
gologger.Print().Msgf("%s\n\n", responsehighlighter.Highlight(event.OperatorsResult, dataOutputString, request.options.Options.NoColor, false))
}
if request.options.Options.StoreResponse {
request.options.Output.WriteStoreDebugData(input.MetaInput.Input, request.options.TemplateID, request.Type().String(), fmt.Sprintf("%s\n%s", msg, dataOutputString))
}
}
callback(event)
return nil
}
var RequestPartDefinitions = map[string]string{
"type": "Type is the type of request made",
"host": "Host is the input to the template",
"matched": "Matched is the input which was matched upon",
}
func (request *Request) Match(data map[string]interface{}, matcher *matchers.Matcher) (bool, []string) {
return protocols.MakeDefaultMatchFunc(data, matcher)
}
func (request *Request) Extract(data map[string]interface{}, matcher *extractors.Extractor) map[string]struct{} {
return protocols.MakeDefaultExtractFunc(data, matcher)
}
func (request *Request) MakeResultEvent(wrapped *output.InternalWrappedEvent) []*output.ResultEvent {
return protocols.MakeDefaultResultEvent(request, wrapped)
}
func (request *Request) GetCompiledOperators() []*operators.Operators {
return []*operators.Operators{request.CompiledOperators}
}
func (request *Request) Type() templateTypes.ProtocolType {
return templateTypes.CodeProtocol
}
func (request *Request) MakeResultEventItem(wrapped *output.InternalWrappedEvent) *output.ResultEvent {
fields := protocolutils.GetJsonFieldsFromURL(types.ToString(wrapped.InternalEvent["input"]))
if types.ToString(wrapped.InternalEvent["ip"]) != "" {
fields.Ip = types.ToString(wrapped.InternalEvent["ip"])
}
data := &output.ResultEvent{
TemplateID: types.ToString(request.options.TemplateID),
TemplatePath: types.ToString(request.options.TemplatePath),
Info: request.options.TemplateInfo,
TemplateVerifier: request.options.TemplateVerifier,
Type: types.ToString(wrapped.InternalEvent["type"]),
Matched: types.ToString(wrapped.InternalEvent["input"]),
Host: fields.Host,
Port: fields.Port,
Scheme: fields.Scheme,
URL: fields.URL,
IP: fields.Ip,
Metadata: wrapped.OperatorsResult.PayloadValues,
ExtractedResults: wrapped.OperatorsResult.OutputExtracts,
Timestamp: time.Now(),
MatcherStatus: true,
TemplateEncoded: request.options.EncodeTemplate(),
Error: types.ToString(wrapped.InternalEvent["error"]),
}
return data
}
func fmtStdout(data string) string {
return strings.Trim(data, " \n\r\t")
}
func interpretEnvVars(source string, vars map[string]interface{}) string {
if strings.Contains(source, "$") {
for k, v := range vars {
source = strings.ReplaceAll(source, "$"+k, fmt.Sprintf("%s", v))
}
}
if strings.Contains(source, "os.getenv") {
matches := pythonEnvRegexCompiled.FindAllStringSubmatch(source, -1)
for _, match := range matches {
if len(match) == 0 {
continue
}
source = strings.ReplaceAll(source, fmt.Sprintf("os.getenv('%s')", match), fmt.Sprintf("'%s'", vars[match[0]]))
}
}
return source
}
func beautifyJavascript(code string) string {
opts := jsbeautifier.DefaultOptions()
beautified, err := jsbeautifier.Beautify(&code, opts)
if err != nil {
return code
}
return beautified
}
func prettyPrint(templateId string, buff string) {
lines := strings.Split(buff, "\n")
final := []string{}
for _, v := range lines {
if v != "" {
final = append(final, "\t"+v)
}
}
gologger.Debug().Msgf(" [%v] Pre-condition Code:\n\n%v\n\n", templateId, strings.Join(final, "\n"))
}
func (r *Request) UpdateOptions(opts *protocols.ExecutorOptions) {
r.options.ApplyNewEngineOptions(opts)
}