2023-08-31 18:03:01 +05:30
|
|
|
package flow
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
|
|
|
|
|
"github.com/dop251/goja"
|
|
|
|
|
"github.com/projectdiscovery/gologger"
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
2023-11-27 19:54:45 +01:00
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
2023-10-17 17:44:13 +05:30
|
|
|
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
|
|
|
|
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
2023-08-31 18:03:01 +05:30
|
|
|
errorutil "github.com/projectdiscovery/utils/errors"
|
|
|
|
|
fileutil "github.com/projectdiscovery/utils/file"
|
|
|
|
|
mapsutil "github.com/projectdiscovery/utils/maps"
|
|
|
|
|
"go.uber.org/multierr"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
// ErrInvalidRequestID is a request id error
|
2023-10-13 13:17:27 +05:30
|
|
|
ErrInvalidRequestID = errorutil.NewWithFmt("[%s] invalid request id '%s' provided")
|
2023-08-31 18:03:01 +05:30
|
|
|
)
|
|
|
|
|
|
2023-11-02 13:33:40 +05:30
|
|
|
// ProtoOptions are options that can be passed to flow protocol callback
|
|
|
|
|
// ex: dns(protoOptions) <- protoOptions are optional and can be anything
|
|
|
|
|
type ProtoOptions struct {
|
|
|
|
|
protoName string
|
|
|
|
|
reqIDS []string
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 18:03:01 +05:30
|
|
|
// FlowExecutor is a flow executor for executing a flow
|
|
|
|
|
type FlowExecutor struct {
|
2024-01-08 05:12:11 +05:30
|
|
|
ctx *scan.ScanContext // scan context (includes target etc)
|
2023-08-31 18:03:01 +05:30
|
|
|
options *protocols.ExecutorOptions
|
|
|
|
|
|
|
|
|
|
// javascript runtime reference and compiled program
|
2024-01-08 05:12:11 +05:30
|
|
|
jsVM *goja.Runtime
|
|
|
|
|
program *goja.Program // compiled js program
|
2023-08-31 18:03:01 +05:30
|
|
|
|
|
|
|
|
// protocol requests and their callback functions
|
|
|
|
|
allProtocols map[string][]protocols.Request
|
|
|
|
|
protoFunctions map[string]func(call goja.FunctionCall) goja.Value // reqFunctions contains functions that allow executing requests/protocols from js
|
|
|
|
|
|
|
|
|
|
// logic related variables
|
|
|
|
|
results *atomic.Bool
|
|
|
|
|
allErrs mapsutil.SyncLockMap[string, error]
|
2024-01-29 05:20:01 +05:30
|
|
|
// these are keys whose values are meant to be flatten before executing
|
|
|
|
|
// a request ex: if dynamic extractor returns ["value"] it will be converted to "value"
|
|
|
|
|
flattenKeys []string
|
2023-08-31 18:03:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewFlowExecutor creates a new flow executor from a list of requests
|
2024-01-08 05:12:11 +05:30
|
|
|
// Note: Unlike other engine for every target x template flow needs to be compiled and executed everytime
|
|
|
|
|
// unlike other engines where we compile once and execute multiple times
|
2024-01-17 23:16:57 +05:30
|
|
|
func NewFlowExecutor(requests []protocols.Request, ctx *scan.ScanContext, options *protocols.ExecutorOptions, results *atomic.Bool) (*FlowExecutor, error) {
|
2023-08-31 18:03:01 +05:30
|
|
|
allprotos := make(map[string][]protocols.Request)
|
|
|
|
|
for _, req := range requests {
|
|
|
|
|
switch req.Type() {
|
|
|
|
|
case templateTypes.DNSProtocol:
|
|
|
|
|
allprotos[templateTypes.DNSProtocol.String()] = append(allprotos[templateTypes.DNSProtocol.String()], req)
|
|
|
|
|
case templateTypes.HTTPProtocol:
|
|
|
|
|
allprotos[templateTypes.HTTPProtocol.String()] = append(allprotos[templateTypes.HTTPProtocol.String()], req)
|
|
|
|
|
case templateTypes.NetworkProtocol:
|
|
|
|
|
allprotos[templateTypes.NetworkProtocol.String()] = append(allprotos[templateTypes.NetworkProtocol.String()], req)
|
|
|
|
|
case templateTypes.FileProtocol:
|
|
|
|
|
allprotos[templateTypes.FileProtocol.String()] = append(allprotos[templateTypes.FileProtocol.String()], req)
|
|
|
|
|
case templateTypes.HeadlessProtocol:
|
|
|
|
|
allprotos[templateTypes.HeadlessProtocol.String()] = append(allprotos[templateTypes.HeadlessProtocol.String()], req)
|
|
|
|
|
case templateTypes.SSLProtocol:
|
|
|
|
|
allprotos[templateTypes.SSLProtocol.String()] = append(allprotos[templateTypes.SSLProtocol.String()], req)
|
|
|
|
|
case templateTypes.WebsocketProtocol:
|
|
|
|
|
allprotos[templateTypes.WebsocketProtocol.String()] = append(allprotos[templateTypes.WebsocketProtocol.String()], req)
|
|
|
|
|
case templateTypes.WHOISProtocol:
|
|
|
|
|
allprotos[templateTypes.WHOISProtocol.String()] = append(allprotos[templateTypes.WHOISProtocol.String()], req)
|
|
|
|
|
case templateTypes.CodeProtocol:
|
|
|
|
|
allprotos[templateTypes.CodeProtocol.String()] = append(allprotos[templateTypes.CodeProtocol.String()], req)
|
2023-11-02 13:33:40 +05:30
|
|
|
case templateTypes.JavascriptProtocol:
|
|
|
|
|
allprotos[templateTypes.JavascriptProtocol.String()] = append(allprotos[templateTypes.JavascriptProtocol.String()], req)
|
2024-01-17 23:16:57 +05:30
|
|
|
case templateTypes.OfflineHTTPProtocol:
|
|
|
|
|
// offlinehttp is run in passive mode but templates are same so instead of using offlinehttp() we use http() in flow
|
|
|
|
|
allprotos[templateTypes.HTTPProtocol.String()] = append(allprotos[templateTypes.OfflineHTTPProtocol.String()], req)
|
2023-08-31 18:03:01 +05:30
|
|
|
default:
|
2024-01-17 23:16:57 +05:30
|
|
|
return nil, fmt.Errorf("invalid request type %s", req.Type().String())
|
2023-08-31 18:03:01 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
f := &FlowExecutor{
|
|
|
|
|
allProtocols: allprotos,
|
|
|
|
|
options: options,
|
|
|
|
|
allErrs: mapsutil.SyncLockMap[string, error]{
|
|
|
|
|
ReadOnly: atomic.Bool{},
|
|
|
|
|
Map: make(map[string]error),
|
|
|
|
|
},
|
|
|
|
|
protoFunctions: map[string]func(call goja.FunctionCall) goja.Value{},
|
|
|
|
|
results: results,
|
2023-10-13 13:17:27 +05:30
|
|
|
jsVM: protocolstate.NewJSRuntime(),
|
2024-01-08 05:12:11 +05:30
|
|
|
ctx: ctx,
|
2023-08-31 18:03:01 +05:30
|
|
|
}
|
2024-01-17 23:16:57 +05:30
|
|
|
return f, nil
|
2023-08-31 18:03:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compile compiles js program and registers all functions
|
|
|
|
|
func (f *FlowExecutor) Compile() error {
|
|
|
|
|
if f.results == nil {
|
|
|
|
|
f.results = new(atomic.Bool)
|
|
|
|
|
}
|
|
|
|
|
// load all variables and evaluate with existing data
|
2024-01-08 05:12:11 +05:30
|
|
|
variableMap := f.options.Variables.Evaluate(f.options.GetTemplateCtx(f.ctx.Input.MetaInput).GetAll())
|
2023-08-31 18:03:01 +05:30
|
|
|
// cli options
|
|
|
|
|
optionVars := generators.BuildPayloadFromOptions(f.options.Options)
|
|
|
|
|
// constants
|
|
|
|
|
constants := f.options.Constants
|
|
|
|
|
allVars := generators.MergeMaps(variableMap, constants, optionVars)
|
|
|
|
|
// we support loading variables from files in variables , cli options and constants
|
|
|
|
|
// try to load if files exist
|
|
|
|
|
for k, v := range allVars {
|
|
|
|
|
if str, ok := v.(string); ok && len(str) < 150 && fileutil.FileExists(str) {
|
|
|
|
|
if value, err := f.ReadDataFromFile(str); err == nil {
|
|
|
|
|
allVars[k] = value
|
|
|
|
|
} else {
|
2024-01-08 05:12:11 +05:30
|
|
|
f.ctx.LogWarning("could not load file '%s' for variable '%s': %s", str, k, err)
|
2023-08-31 18:03:01 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-08 05:12:11 +05:30
|
|
|
f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Merge(allVars) // merge all variables into template context
|
2023-08-31 18:03:01 +05:30
|
|
|
|
|
|
|
|
// ---- define callback functions/objects----
|
|
|
|
|
f.protoFunctions = map[string]func(call goja.FunctionCall) goja.Value{}
|
|
|
|
|
// iterate over all protocols and generate callback functions for each protocol
|
|
|
|
|
for p, requests := range f.allProtocols {
|
|
|
|
|
// for each protocol build a requestMap with reqID and protocol request
|
|
|
|
|
reqMap := mapsutil.Map[string, protocols.Request]{}
|
|
|
|
|
counter := 0
|
|
|
|
|
proto := strings.ToLower(p) // donot use loop variables in callback functions directly
|
|
|
|
|
for index := range requests {
|
2023-10-13 13:17:27 +05:30
|
|
|
counter++ // start index from 1
|
2023-08-31 18:03:01 +05:30
|
|
|
request := f.allProtocols[proto][index]
|
|
|
|
|
if request.GetID() != "" {
|
|
|
|
|
// if id is present use it
|
|
|
|
|
reqMap[request.GetID()] = request
|
|
|
|
|
}
|
|
|
|
|
// fallback to using index as id
|
|
|
|
|
// always allow index as id as a fallback
|
|
|
|
|
reqMap[strconv.Itoa(counter)] = request
|
|
|
|
|
}
|
|
|
|
|
// ---define hook that allows protocol/request execution from js-----
|
|
|
|
|
// --- this is the actual callback that is executed when function is invoked in js----
|
|
|
|
|
f.protoFunctions[proto] = func(call goja.FunctionCall) goja.Value {
|
|
|
|
|
opts := &ProtoOptions{
|
|
|
|
|
protoName: proto,
|
|
|
|
|
}
|
|
|
|
|
for _, v := range call.Arguments {
|
|
|
|
|
switch value := v.Export().(type) {
|
|
|
|
|
default:
|
|
|
|
|
opts.reqIDS = append(opts.reqIDS, types.ToString(value))
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-29 05:20:01 +05:30
|
|
|
// before executing any protocol function flatten tracked values
|
|
|
|
|
if len(f.flattenKeys) > 0 {
|
|
|
|
|
ctx := f.options.GetTemplateCtx(f.ctx.Input.MetaInput)
|
|
|
|
|
for _, key := range f.flattenKeys {
|
|
|
|
|
if value, ok := ctx.Get(key); ok {
|
|
|
|
|
ctx.Set(key, flatten(value))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-31 18:03:01 +05:30
|
|
|
return f.jsVM.ToValue(f.requestExecutor(reqMap, opts))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return f.registerBuiltInFunctions()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExecuteWithResults executes the flow and returns results
|
2023-11-27 19:54:45 +01:00
|
|
|
func (f *FlowExecutor) ExecuteWithResults(ctx *scan.ScanContext) error {
|
2023-08-31 18:03:01 +05:30
|
|
|
defer func() {
|
|
|
|
|
if e := recover(); e != nil {
|
2024-01-08 05:12:11 +05:30
|
|
|
f.ctx.LogError(fmt.Errorf("panic occurred while executing target %v with flow: %v", ctx.Input.MetaInput.Input, e))
|
2023-11-27 19:54:45 +01:00
|
|
|
gologger.Error().Label(f.options.TemplateID).Msgf("panic occurred while executing target %v with flow: %v", ctx.Input.MetaInput.Input, e)
|
2023-08-31 18:03:01 +05:30
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2024-01-08 05:12:11 +05:30
|
|
|
f.ctx.Input = ctx.Input
|
2023-08-31 18:03:01 +05:30
|
|
|
// -----Load all types of variables-----
|
|
|
|
|
// add all input args to template context
|
2024-01-08 05:12:11 +05:30
|
|
|
if f.ctx.Input != nil && f.ctx.Input.HasArgs() {
|
|
|
|
|
f.ctx.Input.ForEach(func(key string, value interface{}) {
|
|
|
|
|
f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Set(key, value)
|
2023-08-31 18:03:01 +05:30
|
|
|
})
|
|
|
|
|
}
|
2023-11-27 19:54:45 +01:00
|
|
|
if ctx.OnResult == nil {
|
2023-08-31 18:03:01 +05:30
|
|
|
return fmt.Errorf("output callback cannot be nil")
|
|
|
|
|
}
|
|
|
|
|
// pass flow and execute the js vm and handle errors
|
2024-01-08 05:12:11 +05:30
|
|
|
_, err := f.jsVM.RunProgram(f.program)
|
2023-08-31 18:03:01 +05:30
|
|
|
if err != nil {
|
2023-11-27 19:54:45 +01:00
|
|
|
ctx.LogError(err)
|
2023-08-31 18:03:01 +05:30
|
|
|
return errorutil.NewWithErr(err).Msgf("failed to execute flow\n%v\n", f.options.Flow)
|
|
|
|
|
}
|
|
|
|
|
runtimeErr := f.GetRuntimeErrors()
|
|
|
|
|
if runtimeErr != nil {
|
2023-11-27 19:54:45 +01:00
|
|
|
ctx.LogError(runtimeErr)
|
2023-08-31 18:03:01 +05:30
|
|
|
return errorutil.NewWithErr(runtimeErr).Msgf("got following errors while executing flow")
|
|
|
|
|
}
|
2024-01-08 05:12:11 +05:30
|
|
|
|
2023-08-31 18:03:01 +05:30
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetRuntimeErrors returns all runtime errors (i.e errors from all protocol combined)
|
|
|
|
|
func (f *FlowExecutor) GetRuntimeErrors() error {
|
|
|
|
|
errs := []error{}
|
|
|
|
|
for proto, err := range f.allErrs.GetAll() {
|
|
|
|
|
errs = append(errs, errorutil.NewWithErr(err).Msgf("failed to execute %v protocol", proto))
|
|
|
|
|
}
|
|
|
|
|
return multierr.Combine(errs...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReadDataFromFile reads data from file respecting sandbox options
|
|
|
|
|
func (f *FlowExecutor) ReadDataFromFile(payload string) ([]string, error) {
|
|
|
|
|
values := []string{}
|
|
|
|
|
// load file respecting sandbox
|
|
|
|
|
reader, err := f.options.Options.LoadHelperFile(payload, f.options.TemplatePath, f.options.Catalog)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return values, err
|
|
|
|
|
}
|
|
|
|
|
defer reader.Close()
|
|
|
|
|
bin, err := io.ReadAll(reader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return values, err
|
|
|
|
|
}
|
|
|
|
|
for _, line := range strings.Split(string(bin), "\n") {
|
|
|
|
|
line = strings.TrimSpace(line)
|
|
|
|
|
if line != "" {
|
|
|
|
|
values = append(values, line)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return values, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Name returns the type of engine
|
|
|
|
|
func (f *FlowExecutor) Name() string {
|
|
|
|
|
return "flow"
|
|
|
|
|
}
|