Dwi Siswanto a326f3925c
fix(tmplexec): memory blowup in multiproto (#6258)
* bugfix: fix memory blowup using previousEvent for multi-proto execution

* refactor(tmplexec): uses supported protocol types

Signed-off-by: Dwi Siswanto <git@dw1.io>

* add co-author

Co-authored-by: Nakul Bharti <knakul853@users.noreply.github.com>
Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(tmplexec): mv builder inside loop scope

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(tmplexec): skip existing keys in `FillPreviousEvent`

The `FillPreviousEvent` func was modified to
prevent overwriting/duplicating entries in the
previous map.

It now checks if a key `k` from
`event.InternalEvent` already exists in the
previous map. If it does, the key is skipped. This
ensures that if `k` was already set (potentially
w/o a prefix), it's not re-added with an `ID_`
prefix.

Additionally, keys in `event.InternalEvent` that
already start with the current `ID_` prefix are
also skipped to avoid redundant prefixing.

This change simplifies the logic by removing the
`reqTypeWithIndexRegex` and directly addresses the
potential for duplicate / incorrectly prefixed
keys when `event.InternalEvent` grows during
protocol request execution.

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(tmplexec): naming convention, `ID` => `protoID`

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(tmplexec): it's request ID lol sorry

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Ice3man <nizamulrana@gmail.com>
Co-authored-by: Nakul Bharti <knakul853@users.noreply.github.com>
2025-06-17 04:53:32 +05:30

145 lines
5.9 KiB
Go

package multiproto
import (
"strconv"
"sync/atomic"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/utils"
mapsutil "github.com/projectdiscovery/utils/maps"
stringsutil "github.com/projectdiscovery/utils/strings"
)
// Mutliprotocol is a template executer engine that executes multiple protocols
// with logic in between
type MultiProtocol struct {
requests []protocols.Request
options *protocols.ExecutorOptions
results *atomic.Bool
readOnlyArgs map[string]interface{} // readOnlyArgs are readonly args that are available after compilation
}
// NewMultiProtocol creates a new multiprotocol template engine from a list of requests
func NewMultiProtocol(requests []protocols.Request, options *protocols.ExecutorOptions, results *atomic.Bool) *MultiProtocol {
if results == nil {
results = &atomic.Bool{}
}
return &MultiProtocol{requests: requests, options: options, results: results}
}
// Compile engine specific compilation
func (m *MultiProtocol) Compile() error {
// load all variables and evaluate with existing data
variableMap := m.options.Variables.GetAll()
// cli options
optionVars := generators.BuildPayloadFromOptions(m.options.Options)
// constants
constants := m.options.Constants
allVars := generators.MergeMaps(variableMap, constants, optionVars)
allVars = m.options.Variables.Evaluate(allVars)
m.readOnlyArgs = allVars
// no need to load files since they are done at template level
return nil
}
// ExecuteWithResults executes the template and returns results
func (m *MultiProtocol) ExecuteWithResults(ctx *scan.ScanContext) error {
select {
case <-ctx.Context().Done():
return ctx.Context().Err()
default:
}
// put all readonly args into template context
m.options.GetTemplateCtx(ctx.Input.MetaInput).Merge(m.readOnlyArgs)
// add all input args to template context
ctx.Input.ForEach(func(key string, value interface{}) {
m.options.GetTemplateCtx(ctx.Input.MetaInput).Set(key, value)
})
previous := mapsutil.NewSyncLockMap[string, any]()
// template context: contains values extracted using `internal` extractor from previous protocols
// these values are extracted from each protocol in queue and are passed to next protocol in queue
// instead of adding seperator field to handle such cases these values are appended to `dynamicValues` (which are meant to be used in workflows)
// this makes it possible to use multi protocol templates in workflows
// Note: internal extractor values take precedence over dynamicValues from workflows (i.e other templates in workflow)
// execute all protocols in the queue
for _, req := range m.requests {
select {
case <-ctx.Context().Done():
return ctx.Context().Err()
default:
}
inputItem := ctx.Input.Clone()
if m.options.InputHelper != nil && ctx.Input.MetaInput.Input != "" {
if inputItem.MetaInput.Input = m.options.InputHelper.Transform(inputItem.MetaInput.Input, req.Type()); inputItem.MetaInput.Input == "" {
return nil
}
}
// FIXME: this hack of using hash to get templateCtx has known issues scan context based approach should be adopted ASAP
values := m.options.GetTemplateCtx(inputItem.MetaInput).GetAll()
err := req.ExecuteWithResults(inputItem, output.InternalEvent(values), output.InternalEvent(previous.GetAll()), func(event *output.InternalWrappedEvent) {
if event == nil {
return
}
utils.FillPreviousEvent(req.GetID(), event, previous)
// log event and generate result for the event
ctx.LogEvent(event)
// export dynamic values from operators (i.e internal:true)
if event.OperatorsResult != nil && len(event.OperatorsResult.DynamicValues) > 0 {
for k, v := range event.OperatorsResult.DynamicValues {
// TBD: iterate-all is only supported in `http` protocol
// we either need to add support for iterate-all in other protocols or implement a different logic (specific to template context)
// currently if dynamic value array only contains one value we replace it with the value
if len(v) == 1 {
m.options.GetTemplateCtx(ctx.Input.MetaInput).Set(k, v[0])
} else {
// Note: if extracted value contains multiple values then they can be accessed by indexing
// ex: if values are dynamic = []string{"a","b","c"} then they are available as
// dynamic = "a" , dynamic1 = "b" , dynamic2 = "c"
// we intentionally omit first index for unknown situations (where no of extracted values are not known)
for i, val := range v {
if i == 0 {
m.options.GetTemplateCtx(ctx.Input.MetaInput).Set(k, val)
} else {
m.options.GetTemplateCtx(ctx.Input.MetaInput).Set(k+strconv.Itoa(i), val)
}
}
}
}
}
// evaluate all variables after execution of each protocol
variableMap := m.options.Variables.Evaluate(m.options.GetTemplateCtx(ctx.Input.MetaInput).GetAll())
m.options.GetTemplateCtx(ctx.Input.MetaInput).Merge(variableMap) // merge all variables into template context
})
// in case of fatal error skip execution of next protocols
if err != nil {
// always log errors
ctx.LogError(err)
// for some classes of protocols (i.e ssl) errors like tls handshake are a legitimate behavior so we don't stop execution
// connection failures are already tracked by the internal host error cache
// we use strings comparison as the error is not formalized into instance within the standard library
// within a flow instead we consider ssl errors as fatal, since a specific logic was requested
if req.Type() == types.SSLProtocol && stringsutil.ContainsAnyI(err.Error(), "protocol version not supported", "could not do tls handshake") {
continue
}
}
}
return nil
}
// Name of the template engine
func (m *MultiProtocol) Name() string {
return "multiproto"
}