mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-02-01 08:13:09 +08:00
* fix(http): interactsh matching with `payloads` in parallel execution. Templates using `payloads` with Interactsh matchers failed to detect OAST interactions because the parallel HTTP execution path (used when `payloads` are present) did not register Interactsh request events, unlike the seq path. This caused incoming interactions to lack associated request context, preventing matchers from running and resulting in missed detections. Fix #5485 by wiring `(*interactsh.Client).RequestEvent` registration into the parallel worker goroutine, make sure both execution paths handle Interactsh correlation equally. Signed-off-by: Dwi Siswanto <git@dw1.io> * test: add interactsh with `payloads` integration Signed-off-by: Dwi Siswanto <git@dw1.io> * test: disable interactsh-with-payloads Signed-off-by: Dwi Siswanto <git@dw1.io> --------- Signed-off-by: Dwi Siswanto <git@dw1.io>
1310 lines
48 KiB
Go
1310 lines
48 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"go.uber.org/multierr"
|
|
"moul.io/http2curl"
|
|
|
|
"github.com/projectdiscovery/fastdialer/fastdialer"
|
|
"github.com/projectdiscovery/gologger"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
|
fuzzStats "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
|
|
"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/expressions"
|
|
"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/protocolstate"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httputils"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/signer"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/signerpool"
|
|
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/types/nucleierr"
|
|
"github.com/projectdiscovery/rawhttp"
|
|
convUtil "github.com/projectdiscovery/utils/conversion"
|
|
"github.com/projectdiscovery/utils/errkit"
|
|
httpUtils "github.com/projectdiscovery/utils/http"
|
|
"github.com/projectdiscovery/utils/reader"
|
|
sliceutil "github.com/projectdiscovery/utils/slice"
|
|
stringsutil "github.com/projectdiscovery/utils/strings"
|
|
unitutils "github.com/projectdiscovery/utils/unit"
|
|
urlutil "github.com/projectdiscovery/utils/url"
|
|
)
|
|
|
|
const (
|
|
defaultMaxWorkers = 150
|
|
// max unique errors to store & combine
|
|
// when executing requests in parallel
|
|
maxErrorsWhenParallel = 3
|
|
)
|
|
|
|
var (
|
|
MaxBodyRead = 10 * unitutils.Mega
|
|
// ErrMissingVars is error occurred when variables are missing
|
|
ErrMissingVars = errkit.New("stop execution due to unresolved variables").SetKind(nucleierr.ErrTemplateLogic).Build()
|
|
// ErrHttpEngineRequestDeadline is error occurred when request deadline set by http request engine is exceeded
|
|
ErrHttpEngineRequestDeadline = errkit.New("http request engine deadline exceeded").SetKind(errkit.ErrKindDeadline).Build()
|
|
)
|
|
|
|
// Type returns the type of the protocol request
|
|
func (request *Request) Type() templateTypes.ProtocolType {
|
|
return templateTypes.HTTPProtocol
|
|
}
|
|
|
|
// executeRaceRequest executes race condition request for a URL
|
|
func (request *Request) executeRaceRequest(input *contextargs.Context, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
|
|
reqURL := input.MetaInput.Input
|
|
var generatedRequests []*generatedRequest
|
|
|
|
// Requests within race condition should be dumped once and the output prefilled to allow DSL language to work
|
|
// This will introduce a delay and will populate in hacky way the field "request" of outputEvent
|
|
generator := request.newGenerator(false)
|
|
|
|
inputData, payloads, ok := generator.nextValue()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
ctx := request.newContext(input)
|
|
requestForDump, err := generator.Make(ctx, input, inputData, payloads, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.setCustomHeaders(requestForDump)
|
|
dumpedRequest, err := dump(requestForDump, reqURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.StoreResponse {
|
|
msg := fmt.Sprintf("[%s] Dumped HTTP request for %s\n\n", request.options.TemplateID, reqURL)
|
|
if request.options.Options.Debug || request.options.Options.DebugRequests {
|
|
gologger.Info().Msg(msg)
|
|
gologger.Print().Msgf("%s", string(dumpedRequest))
|
|
}
|
|
if request.options.Options.StoreResponse {
|
|
request.options.Output.WriteStoreDebugData(reqURL, request.options.TemplateID, request.Type().String(), fmt.Sprintf("%s\n%s", msg, dumpedRequest))
|
|
}
|
|
}
|
|
previous["request"] = string(dumpedRequest)
|
|
|
|
// Pre-Generate requests
|
|
for i := 0; i < request.RaceNumberRequests; i++ {
|
|
generator := request.newGenerator(false)
|
|
inputData, payloads, ok := generator.nextValue()
|
|
if !ok {
|
|
break
|
|
}
|
|
ctx := request.newContext(input)
|
|
generatedRequest, err := generator.Make(ctx, input, inputData, payloads, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
generatedRequests = append(generatedRequests, generatedRequest)
|
|
}
|
|
|
|
shouldStop := (request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch || request.options.StopAtFirstMatch)
|
|
|
|
childCtx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
spmHandler := httputils.NewNonBlockingSPMHandler[error](childCtx, maxErrorsWhenParallel, shouldStop)
|
|
defer spmHandler.Cancel()
|
|
|
|
gotMatches := &atomic.Bool{}
|
|
// wrappedCallback is a callback that wraps the original callback
|
|
// to implement stop at first match logic
|
|
wrappedCallback := func(event *output.InternalWrappedEvent) {
|
|
if !event.HasOperatorResult() {
|
|
callback(event) // not required but we can allow it
|
|
return
|
|
}
|
|
// this will execute match condition such that if stop at first match is enabled
|
|
// this will be only executed once
|
|
spmHandler.MatchCallback(func() {
|
|
gotMatches.Store(true)
|
|
callback(event)
|
|
})
|
|
if shouldStop {
|
|
// stop all running requests and exit
|
|
spmHandler.Trigger()
|
|
}
|
|
}
|
|
|
|
// look for unresponsive hosts and cancel inflight requests as well
|
|
spmHandler.SetOnResultCallback(func(err error) {
|
|
// marks this host as unresponsive if applicable
|
|
request.markHostError(input, err)
|
|
if request.isUnresponsiveAddress(input) {
|
|
// stop all inflight requests
|
|
spmHandler.Cancel()
|
|
}
|
|
})
|
|
|
|
for i := 0; i < request.RaceNumberRequests; i++ {
|
|
if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(input) {
|
|
// stop sending more requests condition is met
|
|
break
|
|
}
|
|
spmHandler.Acquire()
|
|
// execute http request
|
|
go func(httpRequest *generatedRequest) {
|
|
defer spmHandler.Release()
|
|
if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(input) {
|
|
// stop sending more requests condition is met
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-spmHandler.Done():
|
|
return
|
|
case spmHandler.ResultChan <- request.executeRequest(input, httpRequest, previous, false, wrappedCallback, 0):
|
|
return
|
|
}
|
|
}(generatedRequests[i])
|
|
request.options.Progress.IncrementRequests()
|
|
}
|
|
spmHandler.Wait()
|
|
|
|
if spmHandler.FoundFirstMatch() {
|
|
// ignore any context cancellation and in-transit execution errors
|
|
return nil
|
|
}
|
|
return multierr.Combine(spmHandler.CombinedResults()...)
|
|
}
|
|
|
|
// executeParallelHTTP executes parallel requests for a template
|
|
func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicValues output.InternalEvent, callback protocols.OutputEventCallback) error {
|
|
// Workers that keeps enqueuing new requests
|
|
maxWorkers := request.Threads
|
|
|
|
// if request threads matches global payload concurrency we follow it
|
|
shouldFollowGlobal := maxWorkers == request.options.Options.PayloadConcurrency
|
|
|
|
if protocolstate.IsLowOnMemory() {
|
|
maxWorkers = protocolstate.GuardThreadsOrDefault(request.Threads)
|
|
}
|
|
|
|
// Stop-at-first-match logic while executing requests
|
|
// parallelly using threads
|
|
shouldStop := (request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch || request.options.StopAtFirstMatch)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
spmHandler := httputils.NewBlockingSPMHandler[error](ctx, maxWorkers, maxErrorsWhenParallel, shouldStop)
|
|
defer spmHandler.Cancel()
|
|
|
|
// wrappedCallback is a callback that wraps the original callback
|
|
// to implement stop at first match logic
|
|
wrappedCallback := func(event *output.InternalWrappedEvent) {
|
|
if !event.HasOperatorResult() {
|
|
callback(event) // not required but we can allow it
|
|
return
|
|
}
|
|
// this will execute match condition such that if stop at first match is enabled
|
|
// this will be only executed once
|
|
spmHandler.MatchCallback(func() {
|
|
callback(event)
|
|
})
|
|
if shouldStop {
|
|
// stop all running requests and exit
|
|
spmHandler.Trigger()
|
|
}
|
|
}
|
|
|
|
// look for unresponsive hosts and cancel inflight requests as well
|
|
spmHandler.SetOnResultCallback(func(err error) {
|
|
// marks this host as unresponsive if applicable
|
|
request.markHostError(input, err)
|
|
if request.isUnresponsiveAddress(input) {
|
|
// stop all inflight requests
|
|
spmHandler.Cancel()
|
|
}
|
|
})
|
|
|
|
// bounded worker-pool to avoid spawning one goroutine per payload
|
|
type task struct {
|
|
req *generatedRequest
|
|
updatedInput *contextargs.Context
|
|
hasInteractMarkers bool
|
|
}
|
|
|
|
var workersWg sync.WaitGroup
|
|
currentWorkers := maxWorkers
|
|
tasks := make(chan task, maxWorkers)
|
|
spawnWorker := func(ctx context.Context) {
|
|
workersWg.Add(1)
|
|
go func() {
|
|
defer workersWg.Done()
|
|
for t := range tasks {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(t.updatedInput) || spmHandler.Cancelled() {
|
|
continue
|
|
}
|
|
spmHandler.Acquire()
|
|
if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(t.updatedInput) || spmHandler.Cancelled() {
|
|
spmHandler.Release()
|
|
continue
|
|
}
|
|
request.options.RateLimitTake()
|
|
hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)
|
|
needsRequestEvent := hasInteractMatchers && request.NeedsRequestCondition()
|
|
select {
|
|
case <-spmHandler.Done():
|
|
spmHandler.Release()
|
|
continue
|
|
case spmHandler.ResultChan <- request.executeRequest(t.updatedInput, t.req, make(map[string]interface{}), hasInteractMatchers, func(event *output.InternalWrappedEvent) {
|
|
if (t.hasInteractMarkers || needsRequestEvent) && request.options.Interactsh != nil {
|
|
requestData := &interactsh.RequestData{
|
|
MakeResultFunc: request.MakeResultEvent,
|
|
Event: event,
|
|
Operators: request.CompiledOperators,
|
|
MatchFunc: request.Match,
|
|
ExtractFunc: request.Extract,
|
|
}
|
|
allOASTUrls := httputils.GetInteractshURLSFromEvent(event.InternalEvent)
|
|
allOASTUrls = append(allOASTUrls, t.req.interactshURLs...)
|
|
request.options.Interactsh.RequestEvent(sliceutil.Dedupe(allOASTUrls), requestData)
|
|
}
|
|
wrappedCallback(event)
|
|
}, 0):
|
|
spmHandler.Release()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
for i := 0; i < currentWorkers; i++ {
|
|
spawnWorker(ctx)
|
|
}
|
|
|
|
// iterate payloads and make requests
|
|
generator := request.newGenerator(false)
|
|
for {
|
|
inputData, payloads, ok := generator.nextValue()
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-input.Context().Done():
|
|
close(tasks)
|
|
workersWg.Wait()
|
|
return input.Context().Err()
|
|
default:
|
|
}
|
|
|
|
// resize check point - nop if there are no changes
|
|
if shouldFollowGlobal && spmHandler.Size() != request.options.Options.PayloadConcurrency {
|
|
if err := spmHandler.Resize(input.Context(), request.options.Options.PayloadConcurrency); err != nil {
|
|
close(tasks)
|
|
workersWg.Wait()
|
|
return err
|
|
}
|
|
// if payload concurrency increased, add more workers
|
|
if spmHandler.Size() > currentWorkers {
|
|
for i := 0; i < spmHandler.Size()-currentWorkers; i++ {
|
|
spawnWorker(ctx)
|
|
}
|
|
currentWorkers = spmHandler.Size()
|
|
}
|
|
}
|
|
|
|
// break if stop at first match is found or host is unresponsive
|
|
if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(input) {
|
|
break
|
|
}
|
|
|
|
ctx := request.newContext(input)
|
|
generatedHttpRequest, err := generator.Make(ctx, input, inputData, payloads, dynamicValues)
|
|
if err != nil {
|
|
if err == types.ErrNoMoreRequests {
|
|
break
|
|
}
|
|
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
|
|
close(tasks)
|
|
workersWg.Wait()
|
|
return err
|
|
}
|
|
hasInteractMarkers := interactsh.HasMarkers(inputData) || len(generatedHttpRequest.interactshURLs) > 0
|
|
if input.MetaInput.Input == "" {
|
|
input.MetaInput.Input = generatedHttpRequest.URL()
|
|
}
|
|
updatedInput := contextargs.GetCopyIfHostOutdated(input, generatedHttpRequest.URL())
|
|
if request.isUnresponsiveAddress(updatedInput) {
|
|
// skip on unresponsive host no need to continue
|
|
spmHandler.Cancel()
|
|
close(tasks)
|
|
workersWg.Wait()
|
|
return nil
|
|
}
|
|
select {
|
|
case <-spmHandler.Done():
|
|
close(tasks)
|
|
workersWg.Wait()
|
|
spmHandler.Wait()
|
|
if spmHandler.FoundFirstMatch() {
|
|
return nil
|
|
}
|
|
return multierr.Combine(spmHandler.CombinedResults()...)
|
|
case tasks <- task{req: generatedHttpRequest, updatedInput: updatedInput, hasInteractMarkers: hasInteractMarkers}:
|
|
}
|
|
request.options.Progress.IncrementRequests()
|
|
}
|
|
close(tasks)
|
|
workersWg.Wait()
|
|
spmHandler.Wait()
|
|
if spmHandler.FoundFirstMatch() {
|
|
// ignore any context cancellation and in-transit execution errors
|
|
return nil
|
|
}
|
|
return multierr.Combine(spmHandler.CombinedResults()...)
|
|
}
|
|
|
|
// executeTurboHTTP executes turbo http request for a URL
|
|
func (request *Request) executeTurboHTTP(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
|
|
generator := request.newGenerator(false)
|
|
|
|
// need to extract the target from the url
|
|
URL, err := urlutil.Parse(input.MetaInput.Input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pipeOptions := rawhttp.DefaultPipelineOptions
|
|
pipeOptions.Host = URL.Host
|
|
pipeOptions.MaxConnections = 1
|
|
if request.PipelineConcurrentConnections > 0 {
|
|
pipeOptions.MaxConnections = request.PipelineConcurrentConnections
|
|
}
|
|
if request.PipelineRequestsPerConnection > 0 {
|
|
pipeOptions.MaxPendingRequests = request.PipelineRequestsPerConnection
|
|
}
|
|
pipeClient := rawhttp.NewPipelineClient(pipeOptions)
|
|
|
|
// defaultMaxWorkers should be a sufficient value to keep queues always full
|
|
// in case the queue is bigger increase the workers
|
|
maxWorkers := max(pipeOptions.MaxPendingRequests, defaultMaxWorkers)
|
|
|
|
// Stop-at-first-match logic while executing requests
|
|
// parallelly using threads
|
|
shouldStop := (request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch || request.options.StopAtFirstMatch)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
spmHandler := httputils.NewBlockingSPMHandler[error](ctx, maxWorkers, maxErrorsWhenParallel, shouldStop)
|
|
defer spmHandler.Cancel()
|
|
|
|
// wrappedCallback is a callback that wraps the original callback
|
|
// to implement stop at first match logic
|
|
wrappedCallback := func(event *output.InternalWrappedEvent) {
|
|
if !event.HasOperatorResult() {
|
|
callback(event) // not required but we can allow it
|
|
return
|
|
}
|
|
// this will execute match condition such that if stop at first match is enabled
|
|
// this will be only executed once
|
|
spmHandler.MatchCallback(func() {
|
|
callback(event)
|
|
})
|
|
if shouldStop {
|
|
// stop all running requests and exit
|
|
spmHandler.Trigger()
|
|
}
|
|
}
|
|
|
|
// look for unresponsive hosts and cancel inflight requests as well
|
|
spmHandler.SetOnResultCallback(func(err error) {
|
|
// marks this host as unresponsive if applicable
|
|
request.markHostError(input, err)
|
|
if request.isUnresponsiveAddress(input) {
|
|
// stop all inflight requests
|
|
spmHandler.Cancel()
|
|
}
|
|
})
|
|
|
|
for {
|
|
inputData, payloads, ok := generator.nextValue()
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-input.Context().Done():
|
|
return input.Context().Err()
|
|
default:
|
|
}
|
|
|
|
if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(input) || spmHandler.Cancelled() {
|
|
// skip if first match is found
|
|
break
|
|
}
|
|
|
|
ctx := request.newContext(input)
|
|
generatedHttpRequest, err := generator.Make(ctx, input, inputData, payloads, dynamicValues)
|
|
if err != nil {
|
|
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
|
|
return err
|
|
}
|
|
if input.MetaInput.Input == "" {
|
|
input.MetaInput.Input = generatedHttpRequest.URL()
|
|
}
|
|
updatedInput := contextargs.GetCopyIfHostOutdated(input, generatedHttpRequest.URL())
|
|
if request.isUnresponsiveAddress(updatedInput) {
|
|
// skip on unresponsive host no need to continue
|
|
spmHandler.Cancel()
|
|
return nil
|
|
}
|
|
generatedHttpRequest.pipelinedClient = pipeClient
|
|
spmHandler.Acquire()
|
|
go func(httpRequest *generatedRequest) {
|
|
defer spmHandler.Release()
|
|
if spmHandler.FoundFirstMatch() || request.isUnresponsiveAddress(updatedInput) {
|
|
// skip if first match is found
|
|
return
|
|
}
|
|
select {
|
|
case <-spmHandler.Done():
|
|
return
|
|
case spmHandler.ResultChan <- request.executeRequest(input, httpRequest, previous, false, wrappedCallback, 0):
|
|
return
|
|
}
|
|
}(generatedHttpRequest)
|
|
request.options.Progress.IncrementRequests()
|
|
}
|
|
spmHandler.Wait()
|
|
if spmHandler.FoundFirstMatch() {
|
|
// ignore any context cancellation and in-transit execution errors
|
|
return nil
|
|
}
|
|
return multierr.Combine(spmHandler.CombinedResults()...)
|
|
}
|
|
|
|
// ExecuteWithResults executes the final request on a URL
|
|
func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
|
|
// verify if pipeline was requested
|
|
if request.Pipeline {
|
|
return request.executeTurboHTTP(input, dynamicValues, previous, callback)
|
|
}
|
|
// verify if a basic race condition was requested
|
|
if request.Race && request.RaceNumberRequests > 0 {
|
|
return request.executeRaceRequest(input, dynamicValues, callback)
|
|
}
|
|
|
|
// verify if fuzz elaboration was requested
|
|
if len(request.Fuzzing) > 0 {
|
|
return request.executeFuzzingRule(input, dynamicValues, callback)
|
|
}
|
|
|
|
// verify if parallel elaboration was requested
|
|
if request.Threads > 0 && (len(request.Payloads) > 0 || request.Race) {
|
|
return request.executeParallelHTTP(input, dynamicValues, callback)
|
|
}
|
|
|
|
generator := request.newGenerator(false)
|
|
|
|
var gotDynamicValues map[string][]string
|
|
var requestErr error
|
|
|
|
for {
|
|
// returns two values, error and skip, which skips the execution for the request instance.
|
|
executeFunc := func(data string, payloads, dynamicValue map[string]interface{}) (bool, error) {
|
|
hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)
|
|
|
|
request.options.RateLimitTake()
|
|
|
|
ctx := request.newContext(input)
|
|
ctxWithTimeout, cancel := context.WithTimeoutCause(ctx, request.options.Options.GetTimeouts().HttpTimeout, ErrHttpEngineRequestDeadline)
|
|
defer cancel()
|
|
|
|
generatedHttpRequest, err := generator.Make(ctxWithTimeout, input, data, payloads, dynamicValue)
|
|
if err != nil {
|
|
if err == types.ErrNoMoreRequests {
|
|
return true, nil
|
|
}
|
|
return true, err
|
|
}
|
|
// ideally if http template used a custom port or hostname
|
|
// we would want to update it in input but currently templateCtx logic
|
|
// is closely tied to contextargs.Context so we are temporarily creating
|
|
// a copy and using it to check for host errors etc
|
|
// but this should be replaced once templateCtx is refactored properly
|
|
updatedInput := contextargs.GetCopyIfHostOutdated(input, generatedHttpRequest.URL())
|
|
|
|
if generatedHttpRequest.customCancelFunction != nil {
|
|
defer generatedHttpRequest.customCancelFunction()
|
|
}
|
|
|
|
hasInteractMarkers := interactsh.HasMarkers(data) || len(generatedHttpRequest.interactshURLs) > 0
|
|
if input.MetaInput.Input == "" {
|
|
input.MetaInput.Input = generatedHttpRequest.URL()
|
|
}
|
|
// Check if hosts keep erroring
|
|
if request.isUnresponsiveAddress(updatedInput) {
|
|
return true, nil
|
|
}
|
|
var gotMatches bool
|
|
execReqErr := request.executeRequest(input, generatedHttpRequest, previous, hasInteractMatchers, func(event *output.InternalWrappedEvent) {
|
|
// a special case where operators has interactsh matchers and multiple request are made
|
|
// ex: status_code_2 , interactsh_protocol (from 1st request) etc
|
|
needsRequestEvent := interactsh.HasMatchers(request.CompiledOperators) && request.NeedsRequestCondition()
|
|
if (hasInteractMarkers || needsRequestEvent) && request.options.Interactsh != nil {
|
|
requestData := &interactsh.RequestData{
|
|
MakeResultFunc: request.MakeResultEvent,
|
|
Event: event,
|
|
Operators: request.CompiledOperators,
|
|
MatchFunc: request.Match,
|
|
ExtractFunc: request.Extract,
|
|
}
|
|
allOASTUrls := httputils.GetInteractshURLSFromEvent(event.InternalEvent)
|
|
allOASTUrls = append(allOASTUrls, generatedHttpRequest.interactshURLs...)
|
|
request.options.Interactsh.RequestEvent(sliceutil.Dedupe(allOASTUrls), requestData)
|
|
gotMatches = request.options.Interactsh.AlreadyMatched(requestData)
|
|
}
|
|
// Add the extracts to the dynamic values if any.
|
|
if event.OperatorsResult != nil {
|
|
gotMatches = event.OperatorsResult.Matched
|
|
gotDynamicValues = generators.MergeMapsMany(event.OperatorsResult.DynamicValues, dynamicValues, gotDynamicValues)
|
|
}
|
|
// Note: This is a race condition prone zone i.e when request has interactsh_matchers
|
|
// Interactsh.RequestEvent tries to access/update output.InternalWrappedEvent depending on logic
|
|
// to avoid conflicts with `callback` mutex is used here and in Interactsh.RequestEvent
|
|
// Note: this only happens if requests > 1 and interactsh matcher is used
|
|
// TODO: interactsh logic in nuclei needs to be refactored to avoid such situations
|
|
callback(event)
|
|
}, generator.currentIndex)
|
|
|
|
// If a variable is unresolved, skip all further requests
|
|
if errors.Is(execReqErr, ErrMissingVars) {
|
|
return true, nil
|
|
}
|
|
|
|
if execReqErr != nil {
|
|
request.markHostError(updatedInput, execReqErr)
|
|
|
|
// if applicable mark the host as unresponsive
|
|
reqKitErr := errkit.FromError(execReqErr)
|
|
reqKitErr.Msgf("got err while executing %v", generatedHttpRequest.URL())
|
|
|
|
requestErr = reqKitErr
|
|
request.options.Progress.IncrementFailedRequestsBy(1)
|
|
} else {
|
|
request.options.Progress.IncrementRequests()
|
|
}
|
|
|
|
// If this was a match, and we want to stop at first match, skip all further requests.
|
|
shouldStopAtFirstMatch := generatedHttpRequest.original.options.Options.StopAtFirstMatch || generatedHttpRequest.original.options.StopAtFirstMatch || request.StopAtFirstMatch
|
|
if shouldStopAtFirstMatch && gotMatches {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
inputData, payloads, ok := generator.nextValue()
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-input.Context().Done():
|
|
return input.Context().Err()
|
|
default:
|
|
}
|
|
|
|
var gotErr error
|
|
var skip bool
|
|
if len(gotDynamicValues) > 0 {
|
|
operators.MakeDynamicValuesCallback(gotDynamicValues, request.IterateAll, func(data map[string]interface{}) bool {
|
|
if skip, gotErr = executeFunc(inputData, payloads, data); skip || gotErr != nil {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
} else {
|
|
skip, gotErr = executeFunc(inputData, payloads, dynamicValues)
|
|
}
|
|
if gotErr != nil && requestErr == nil {
|
|
requestErr = gotErr
|
|
}
|
|
if skip || gotErr != nil {
|
|
request.options.Progress.SetRequests(uint64(generator.Remaining() + 1))
|
|
break
|
|
}
|
|
}
|
|
return requestErr
|
|
}
|
|
|
|
const drainReqSize = int64(8 * unitutils.Kilo)
|
|
|
|
// executeRequest executes the actual generated request and returns error if occurred
|
|
func (request *Request) executeRequest(input *contextargs.Context, generatedRequest *generatedRequest, previousEvent output.InternalEvent, hasInteractMatchers bool, processEvent protocols.OutputEventCallback, requestCount int) (err error) {
|
|
// Check if hosts keep erroring
|
|
if request.isUnresponsiveAddress(input) {
|
|
return fmt.Errorf("hostErrorsCache : host %s is unresponsive", input.MetaInput.Input)
|
|
}
|
|
|
|
// wrap one more callback for validation and fixing event
|
|
callback := func(event *output.InternalWrappedEvent) {
|
|
// validateNFixEvent performs necessary validation on generated event
|
|
// and attempts to fix it , this includes things like making sure
|
|
// `template-id` is set , `request-url-pattern` is set etc
|
|
request.validateNFixEvent(input, generatedRequest, err, event)
|
|
processEvent(event)
|
|
}
|
|
|
|
request.setCustomHeaders(generatedRequest)
|
|
|
|
// Try to evaluate any payloads before replacement
|
|
finalMap := generators.MergeMaps(generatedRequest.dynamicValues, generatedRequest.meta)
|
|
|
|
// add known variables from metainput
|
|
if _, ok := finalMap["ip"]; !ok && input.MetaInput.CustomIP != "" {
|
|
finalMap["ip"] = input.MetaInput.CustomIP
|
|
}
|
|
|
|
for payloadName, payloadValue := range generatedRequest.meta {
|
|
if data, err := expressions.Evaluate(types.ToString(payloadValue), finalMap); err == nil {
|
|
generatedRequest.meta[payloadName] = data
|
|
}
|
|
}
|
|
|
|
var (
|
|
resp *http.Response
|
|
fromCache bool
|
|
dumpedRequest []byte
|
|
)
|
|
|
|
// Dump request for variables checks
|
|
// For race conditions we can't dump the request body at this point as it's already waiting the open-gate event, already handled with a similar code within the race function
|
|
if !generatedRequest.original.Race {
|
|
|
|
// change encoding type to content-length unless transfer-encoding header is manually set
|
|
if generatedRequest.request != nil && !stringsutil.EqualFoldAny(generatedRequest.request.Method, http.MethodGet, http.MethodHead) && generatedRequest.request.Body != nil && generatedRequest.request.Header.Get("Transfer-Encoding") != "chunked" {
|
|
var newReqBody *reader.ReusableReadCloser
|
|
newReqBody, ok := generatedRequest.request.Body.(*reader.ReusableReadCloser)
|
|
if !ok {
|
|
newReqBody, err = reader.NewReusableReadCloser(generatedRequest.request.Body)
|
|
}
|
|
if err == nil {
|
|
// update the request body with the reusable reader
|
|
generatedRequest.request.SetBodyReader(newReqBody)
|
|
// get content length
|
|
length, _ := io.Copy(io.Discard, newReqBody)
|
|
generatedRequest.request.ContentLength = length
|
|
} else {
|
|
// log error and continue
|
|
gologger.Verbose().Msgf("[%v] Could not read request body while forcing transfer encoding: %s\n", request.options.TemplateID, err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
// do the same for unsafe requests
|
|
if generatedRequest.rawRequest != nil && !stringsutil.EqualFoldAny(generatedRequest.rawRequest.Method, http.MethodGet, http.MethodHead) && generatedRequest.rawRequest.Data != "" && generatedRequest.rawRequest.Headers["Transfer-Encoding"] != "chunked" {
|
|
generatedRequest.rawRequest.Headers["Content-Length"] = strconv.Itoa(len(generatedRequest.rawRequest.Data))
|
|
}
|
|
|
|
var dumpError error
|
|
// TODO: dump is currently not working with post-processors - somehow it alters the signature
|
|
dumpedRequest, dumpError = dump(generatedRequest, input.MetaInput.Input)
|
|
if dumpError != nil {
|
|
return dumpError
|
|
}
|
|
dumpedRequestString := string(dumpedRequest)
|
|
|
|
if ignoreList := GetVariablesNamesSkipList(generatedRequest.original.Signature.Value); ignoreList != nil {
|
|
if varErr := expressions.ContainsVariablesWithIgnoreList(ignoreList, dumpedRequestString); varErr != nil && !request.SkipVariablesCheck {
|
|
gologger.Warning().Msgf("[%s] Could not make http request for %s: %v\n", request.options.TemplateID, input.MetaInput.Input, varErr)
|
|
return ErrMissingVars
|
|
}
|
|
} else { // Check if are there any unresolved variables. If yes, skip unless overridden by user.
|
|
if varErr := expressions.ContainsUnresolvedVariables(dumpedRequestString); varErr != nil && !request.SkipVariablesCheck {
|
|
gologger.Warning().Msgf("[%s] Could not make http request for %s: %v\n", request.options.TemplateID, input.MetaInput.Input, varErr)
|
|
return ErrMissingVars
|
|
}
|
|
}
|
|
}
|
|
|
|
// === apply auth strategies ===
|
|
if generatedRequest.request != nil && !request.SkipSecretFile {
|
|
generatedRequest.ApplyAuth(request.options.AuthProvider)
|
|
}
|
|
|
|
var formedURL string
|
|
var hostname string
|
|
timeStart := time.Now()
|
|
if generatedRequest.original.Pipeline {
|
|
// if request is a pipeline request, use the pipelined client
|
|
if generatedRequest.rawRequest != nil {
|
|
formedURL = generatedRequest.rawRequest.FullURL
|
|
if parsed, parseErr := urlutil.ParseURL(formedURL, true); parseErr == nil {
|
|
hostname = parsed.Host
|
|
}
|
|
resp, err = generatedRequest.pipelinedClient.DoRaw(generatedRequest.rawRequest.Method, input.MetaInput.Input, generatedRequest.rawRequest.Path, generators.ExpandMapValues(generatedRequest.rawRequest.Headers), io.NopCloser(strings.NewReader(generatedRequest.rawRequest.Data)))
|
|
} else if generatedRequest.request != nil {
|
|
resp, err = generatedRequest.pipelinedClient.Dor(generatedRequest.request)
|
|
}
|
|
} else if generatedRequest.original.Unsafe && generatedRequest.rawRequest != nil {
|
|
// if request is a unsafe request, use the rawhttp client
|
|
formedURL = generatedRequest.rawRequest.FullURL
|
|
// use request url as matched url if empty
|
|
if formedURL == "" {
|
|
urlx, err := urlutil.Parse(input.MetaInput.Input)
|
|
if err != nil {
|
|
formedURL = fmt.Sprintf("%s%s", input.MetaInput.Input, generatedRequest.rawRequest.Path)
|
|
} else {
|
|
_ = urlx.MergePath(generatedRequest.rawRequest.Path, true)
|
|
formedURL = urlx.String()
|
|
}
|
|
}
|
|
if parsed, parseErr := urlutil.ParseURL(formedURL, true); parseErr == nil {
|
|
hostname = parsed.Host
|
|
}
|
|
options := *generatedRequest.original.rawhttpClient.Options
|
|
options.FollowRedirects = request.Redirects
|
|
options.CustomRawBytes = generatedRequest.rawRequest.UnsafeRawBytes
|
|
options.ForceReadAllBody = request.ForceReadAllBody
|
|
options.SNI = request.options.Options.SNI
|
|
inputUrl := input.MetaInput.Input
|
|
if url, err := urlutil.ParseURL(inputUrl, false); err == nil {
|
|
url.Path = ""
|
|
url.Params = urlutil.NewOrderedParams() // donot include query params
|
|
// inputUrl should only contain scheme://host:port
|
|
inputUrl = url.String()
|
|
}
|
|
formedURL = fmt.Sprintf("%s%s", inputUrl, generatedRequest.rawRequest.Path)
|
|
|
|
// send rawhttp request and get response
|
|
resp, err = httpclientpool.SendRawRequest(generatedRequest.original.rawhttpClient, &httpclientpool.RawHttpRequestOpts{
|
|
Method: generatedRequest.rawRequest.Method,
|
|
URL: inputUrl,
|
|
Path: generatedRequest.rawRequest.Path,
|
|
Headers: generators.ExpandMapValues(generatedRequest.rawRequest.Headers),
|
|
Body: io.NopCloser(strings.NewReader(generatedRequest.rawRequest.Data)),
|
|
Options: &options,
|
|
})
|
|
} else {
|
|
//** For Normal requests **//
|
|
hostname = generatedRequest.request.Host
|
|
formedURL = generatedRequest.request.String()
|
|
// if nuclei-project is available check if the request was already sent previously
|
|
if request.options.ProjectFile != nil {
|
|
// if unavailable fail silently
|
|
fromCache = true
|
|
resp, err = request.options.ProjectFile.Get(dumpedRequest)
|
|
if err != nil {
|
|
fromCache = false
|
|
}
|
|
}
|
|
if resp == nil {
|
|
if errSignature := request.handleSignature(generatedRequest); errSignature != nil {
|
|
return errSignature
|
|
}
|
|
httpclient := request.httpClient
|
|
|
|
// this will be assigned/updated if this specific request has a custom configuration
|
|
var modifiedConfig *httpclientpool.Configuration
|
|
|
|
// check for cookie related configuration
|
|
if input.CookieJar != nil {
|
|
connConfiguration := request.connConfiguration.Clone()
|
|
connConfiguration.Connection.SetCookieJar(input.CookieJar)
|
|
modifiedConfig = connConfiguration
|
|
}
|
|
// check for request updatedTimeout annotation
|
|
updatedTimeout, ok := generatedRequest.request.Context().Value(httpclientpool.WithCustomTimeout{}).(httpclientpool.WithCustomTimeout)
|
|
if ok {
|
|
if modifiedConfig == nil {
|
|
connConfiguration := request.connConfiguration.Clone()
|
|
modifiedConfig = connConfiguration
|
|
}
|
|
|
|
modifiedConfig.ResponseHeaderTimeout = updatedTimeout.Timeout
|
|
}
|
|
|
|
if modifiedConfig != nil {
|
|
client, err := httpclientpool.Get(request.options.Options, modifiedConfig)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not get http client")
|
|
}
|
|
httpclient = client
|
|
}
|
|
|
|
resp, err = httpclient.Do(generatedRequest.request)
|
|
}
|
|
}
|
|
// use request url as matched url if empty
|
|
if formedURL == "" {
|
|
formedURL = input.MetaInput.Input
|
|
}
|
|
|
|
// converts whitespace and other chars that cannot be printed to url encoded values
|
|
formedURL = urlutil.URLEncodeWithEscapes(formedURL)
|
|
|
|
// Dump the requests containing all headers
|
|
if !generatedRequest.original.Race {
|
|
var dumpError error
|
|
dumpedRequest, dumpError = dump(generatedRequest, input.MetaInput.Input)
|
|
if dumpError != nil {
|
|
return dumpError
|
|
}
|
|
dumpedRequestString := string(dumpedRequest)
|
|
if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.StoreResponse {
|
|
msg := fmt.Sprintf("[%s] Dumped HTTP request for %s\n\n", request.options.TemplateID, formedURL)
|
|
|
|
if request.options.Options.Debug || request.options.Options.DebugRequests {
|
|
gologger.Info().Msg(msg)
|
|
gologger.Print().Msgf("%s", dumpedRequestString)
|
|
}
|
|
if request.options.Options.StoreResponse {
|
|
request.options.Output.WriteStoreDebugData(input.MetaInput.Input, request.options.TemplateID, request.Type().String(), fmt.Sprintf("%s\n%s", msg, dumpedRequestString))
|
|
}
|
|
}
|
|
}
|
|
|
|
dialers := protocolstate.GetDialersWithId(request.options.Options.ExecutionId)
|
|
if dialers == nil {
|
|
return fmt.Errorf("dialers not found for execution id %s", request.options.Options.ExecutionId)
|
|
}
|
|
|
|
if err != nil {
|
|
// rawhttp doesn't support draining response bodies.
|
|
if resp != nil && resp.Body != nil && generatedRequest.rawRequest == nil && !generatedRequest.original.Pipeline {
|
|
_, _ = io.CopyN(io.Discard, resp.Body, drainReqSize)
|
|
_ = resp.Body.Close()
|
|
}
|
|
request.options.Output.Request(request.options.TemplatePath, formedURL, request.Type().String(), err)
|
|
request.options.Progress.IncrementErrorsBy(1)
|
|
|
|
// In case of interactsh markers and request times out, still send
|
|
// a callback event so in case we receive an interaction, correlation is possible.
|
|
// Also, to log failed use-cases.
|
|
outputEvent := request.responseToDSLMap(&http.Response{}, input.MetaInput.Input, formedURL, convUtil.String(dumpedRequest), "", "", "", 0, generatedRequest.meta)
|
|
if i := strings.LastIndex(hostname, ":"); i != -1 {
|
|
hostname = hostname[:i]
|
|
}
|
|
|
|
if input.MetaInput.CustomIP != "" {
|
|
outputEvent["ip"] = input.MetaInput.CustomIP
|
|
} else {
|
|
outputEvent["ip"] = dialers.Fastdialer.GetDialedIP(hostname)
|
|
// try getting cname
|
|
request.addCNameIfAvailable(hostname, outputEvent)
|
|
}
|
|
|
|
if len(generatedRequest.interactshURLs) > 0 {
|
|
// according to logic we only need to trigger a callback if interactsh was used
|
|
// and request failed in hope that later on oast interaction will be received
|
|
event := &output.InternalWrappedEvent{}
|
|
if request.CompiledOperators != nil && request.CompiledOperators.HasDSL() {
|
|
event.InternalEvent = outputEvent
|
|
}
|
|
callback(event)
|
|
}
|
|
return err
|
|
}
|
|
|
|
var curlCommand string
|
|
if !request.Unsafe && resp != nil && generatedRequest.request != nil && resp.Request != nil && !request.Race {
|
|
bodyBytes, _ := generatedRequest.request.BodyBytes()
|
|
// Use a clone to avoid a race condition with the http transport
|
|
req := resp.Request.Clone(resp.Request.Context())
|
|
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
command, err := http2curl.GetCurlCommand(req)
|
|
if err == nil && command != nil {
|
|
curlCommand = command.String()
|
|
}
|
|
}
|
|
|
|
gologger.Verbose().Msgf("[%s] Sent HTTP request to %s", request.options.TemplateID, formedURL)
|
|
request.options.Output.Request(request.options.TemplatePath, formedURL, request.Type().String(), err)
|
|
|
|
duration := time.Since(timeStart)
|
|
|
|
// define max body read limit
|
|
maxBodylimit := MaxBodyRead // 10MB
|
|
if request.MaxSize > 0 {
|
|
maxBodylimit = request.MaxSize
|
|
}
|
|
if request.options.Options.ResponseReadSize != 0 {
|
|
maxBodylimit = request.options.Options.ResponseReadSize
|
|
}
|
|
|
|
// respChain is http response chain that reads response body
|
|
// efficiently by reusing buffers and does all decoding and optimizations
|
|
respChain := httpUtils.NewResponseChain(resp, int64(maxBodylimit))
|
|
defer respChain.Close() // reuse buffers
|
|
|
|
// we only intend to log/save the final redirected response
|
|
// i.e why we have to use sync.Once to ensure it's only done once
|
|
var errx error
|
|
onceFunc := sync.OnceFunc(func() {
|
|
// if nuclei-project is enabled store the response if not previously done
|
|
if request.options.ProjectFile != nil && !fromCache {
|
|
if err := request.options.ProjectFile.Set(dumpedRequest, resp, respChain.BodyBytes()); err != nil {
|
|
errx = errors.Wrap(err, "could not store in project file")
|
|
}
|
|
}
|
|
})
|
|
|
|
// evaluate responses continuously until first redirect request in reverse order
|
|
for respChain.Has() {
|
|
// fill buffers, read response body and reuse connection
|
|
if err := respChain.Fill(); err != nil {
|
|
return errors.Wrap(err, "could not generate response chain")
|
|
}
|
|
|
|
// Cache response strings once per Fill() to avoid repeated allocs.
|
|
// NOTE(dwisiswant0): These are valid until Previous() (which reloads
|
|
// the buffer).
|
|
fullResponseStr := respChain.FullResponseString()
|
|
bodyStr := respChain.BodyString()
|
|
headersStr := respChain.HeadersString()
|
|
|
|
// log request stats
|
|
request.options.Output.RequestStatsLog(strconv.Itoa(respChain.Response().StatusCode), fullResponseStr)
|
|
|
|
// save response to projectfile
|
|
onceFunc()
|
|
matchedURL := input.MetaInput.Input
|
|
if generatedRequest.rawRequest != nil {
|
|
if generatedRequest.rawRequest.FullURL != "" {
|
|
matchedURL = generatedRequest.rawRequest.FullURL
|
|
} else {
|
|
matchedURL = formedURL
|
|
}
|
|
}
|
|
if generatedRequest.request != nil {
|
|
matchedURL = generatedRequest.request.String()
|
|
}
|
|
// Give precedence to the final URL from response
|
|
if respChain.Request() != nil {
|
|
if responseURL := respChain.Request().URL.String(); responseURL != "" {
|
|
matchedURL = responseURL
|
|
}
|
|
}
|
|
|
|
finalEvent := make(output.InternalEvent)
|
|
|
|
if request.Analyzer != nil {
|
|
analyzer := analyzers.GetAnalyzer(request.Analyzer.Name)
|
|
analysisMatched, analysisDetails, err := analyzer.Analyze(&analyzers.Options{
|
|
FuzzGenerated: generatedRequest.fuzzGeneratedRequest,
|
|
HttpClient: request.httpClient,
|
|
ResponseTimeDelay: duration,
|
|
AnalyzerParameters: request.Analyzer.Parameters,
|
|
})
|
|
if err != nil {
|
|
gologger.Warning().Msgf("Could not analyze response: %v\n", err)
|
|
}
|
|
if analysisMatched {
|
|
finalEvent["analyzer_details"] = analysisDetails
|
|
finalEvent["analyzer"] = true
|
|
}
|
|
}
|
|
|
|
outputEvent := request.responseToDSLMap(respChain.Response(), input.MetaInput.Input, matchedURL, convUtil.String(dumpedRequest), fullResponseStr, bodyStr, headersStr, duration, generatedRequest.meta)
|
|
// add response fields to template context and merge templatectx variables to output event
|
|
request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, outputEvent)
|
|
if request.options.HasTemplateCtx(input.MetaInput) {
|
|
outputEvent = generators.MergeMaps(outputEvent, request.options.GetTemplateCtx(input.MetaInput).GetAll())
|
|
}
|
|
if i := strings.LastIndex(hostname, ":"); i != -1 {
|
|
hostname = hostname[:i]
|
|
}
|
|
outputEvent["curl-command"] = curlCommand
|
|
if input.MetaInput.CustomIP != "" {
|
|
outputEvent["ip"] = input.MetaInput.CustomIP
|
|
} else {
|
|
dialer := dialers.Fastdialer
|
|
if dialer != nil {
|
|
outputEvent["ip"] = dialer.GetDialedIP(hostname)
|
|
}
|
|
|
|
// try getting cname
|
|
request.addCNameIfAvailable(hostname, outputEvent)
|
|
}
|
|
if request.options.Interactsh != nil {
|
|
request.options.Interactsh.MakePlaceholders(generatedRequest.interactshURLs, outputEvent)
|
|
}
|
|
maps.Copy(finalEvent, previousEvent)
|
|
maps.Copy(finalEvent, outputEvent)
|
|
|
|
// Add to history the current request number metadata if asked by the user.
|
|
if request.NeedsRequestCondition() {
|
|
for k, v := range outputEvent {
|
|
key := fmt.Sprintf("%s_%d", k, requestCount)
|
|
if previousEvent != nil {
|
|
previousEvent[key] = v
|
|
}
|
|
finalEvent[key] = v
|
|
}
|
|
}
|
|
// prune signature internal values if any
|
|
request.pruneSignatureInternalValues(generatedRequest.meta)
|
|
|
|
interimEvent := generators.MergeMaps(generatedRequest.dynamicValues, finalEvent)
|
|
interimEvent["payloads"] = generatedRequest.meta
|
|
// add the request URL pattern to the event BEFORE operators execute
|
|
// so that interactsh events etc can also access it
|
|
if request.options.ExportReqURLPattern {
|
|
interimEvent[ReqURLPatternKey] = generatedRequest.requestURLPattern
|
|
}
|
|
isDebug := request.options.Options.Debug || request.options.Options.DebugResponse
|
|
event := eventcreator.CreateEventWithAdditionalOptions(request, interimEvent, isDebug, func(internalWrappedEvent *output.InternalWrappedEvent) {
|
|
internalWrappedEvent.OperatorsResult.PayloadValues = generatedRequest.meta
|
|
})
|
|
|
|
if hasInteractMatchers {
|
|
event.UsesInteractsh = true
|
|
}
|
|
|
|
if request.options.GlobalMatchers.HasMatchers() {
|
|
request.options.GlobalMatchers.Match(interimEvent, request.Match, request.Extract, isDebug, func(event output.InternalEvent, result *operators.Result) {
|
|
callback(eventcreator.CreateEventWithOperatorResults(request, event, result))
|
|
})
|
|
}
|
|
|
|
responseContentType := respChain.Response().Header.Get("Content-Type")
|
|
isResponseTruncated := request.MaxSize > 0 && respChain.Body().Len() >= request.MaxSize
|
|
dumpResponse(event, request, fullResponseStr, formedURL, responseContentType, isResponseTruncated, input.MetaInput.Input)
|
|
|
|
callback(event)
|
|
|
|
if request.options.FuzzStatsDB != nil && generatedRequest.fuzzGeneratedRequest.Request != nil {
|
|
request.options.FuzzStatsDB.RecordResultEvent(fuzzStats.FuzzingEvent{
|
|
URL: input.MetaInput.Target(),
|
|
TemplateID: request.options.TemplateID,
|
|
ComponentType: generatedRequest.fuzzGeneratedRequest.Component.Name(),
|
|
ComponentName: generatedRequest.fuzzGeneratedRequest.Parameter,
|
|
PayloadSent: generatedRequest.fuzzGeneratedRequest.Value,
|
|
StatusCode: respChain.Response().StatusCode,
|
|
Matched: event.HasResults(),
|
|
RawRequest: string(dumpedRequest),
|
|
RawResponse: fullResponseStr,
|
|
Severity: request.options.TemplateInfo.SeverityHolder.Severity.String(),
|
|
})
|
|
}
|
|
|
|
// Skip further responses if we have stop-at-first-match and a match
|
|
if (request.options.Options.StopAtFirstMatch || request.options.StopAtFirstMatch || request.StopAtFirstMatch) && event.HasResults() {
|
|
return nil
|
|
}
|
|
// proceed with previous response
|
|
// we evaluate operators recursively for each response
|
|
// until we reach the first redirect response
|
|
if !respChain.Previous() {
|
|
break
|
|
}
|
|
}
|
|
// return project file save error if any
|
|
return errx
|
|
}
|
|
|
|
// validateNFixEvent validates and fixes the event
|
|
// it adds any missing template-id and request-url-pattern
|
|
func (request *Request) validateNFixEvent(input *contextargs.Context, gr *generatedRequest, err error, event *output.InternalWrappedEvent) {
|
|
if event != nil {
|
|
if event.InternalEvent == nil {
|
|
event.InternalEvent = make(map[string]interface{})
|
|
event.InternalEvent["template-id"] = request.options.TemplateID
|
|
}
|
|
// add the request URL pattern to the event
|
|
event.InternalEvent[ReqURLPatternKey] = gr.requestURLPattern
|
|
if event.InternalEvent["host"] == nil {
|
|
event.InternalEvent["host"] = input.MetaInput.Input
|
|
}
|
|
if event.InternalEvent["template-id"] == nil {
|
|
event.InternalEvent["template-id"] = request.options.TemplateID
|
|
}
|
|
if event.InternalEvent["type"] == nil {
|
|
event.InternalEvent["type"] = request.Type().String()
|
|
}
|
|
if event.InternalEvent["template-path"] == nil {
|
|
event.InternalEvent["template-path"] = request.options.TemplatePath
|
|
}
|
|
if event.InternalEvent["template-info"] == nil {
|
|
event.InternalEvent["template-info"] = request.options.TemplateInfo
|
|
}
|
|
if err != nil {
|
|
event.InternalEvent["error"] = err.Error()
|
|
}
|
|
}
|
|
}
|
|
|
|
// addCNameIfAvailable adds the cname to the event if available
|
|
func (request *Request) addCNameIfAvailable(hostname string, outputEvent map[string]interface{}) {
|
|
if request.dialer == nil {
|
|
return
|
|
}
|
|
|
|
if request.options.Interactsh != nil {
|
|
interactshDomain := request.options.Interactsh.GetHostname()
|
|
if interactshDomain != "" {
|
|
if strings.EqualFold(hostname, interactshDomain) || strings.HasSuffix(hostname, "."+interactshDomain) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
data, err := request.dialer.GetDNSData(hostname)
|
|
if err == nil {
|
|
switch len(data.CNAME) {
|
|
case 0:
|
|
return
|
|
case 1:
|
|
outputEvent["cname"] = data.CNAME[0]
|
|
default:
|
|
// add 1st and put others in cname_all
|
|
outputEvent["cname"] = data.CNAME[0]
|
|
outputEvent["cname_all"] = data.CNAME
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleSignature of the http request
|
|
func (request *Request) handleSignature(generatedRequest *generatedRequest) error {
|
|
switch request.Signature.Value {
|
|
case AWSSignature:
|
|
var awsSigner signer.Signer
|
|
allvars := generators.MergeMaps(request.options.Options.Vars.AsMap(), generatedRequest.dynamicValues)
|
|
awsopts := signer.AWSOptions{
|
|
AwsID: types.ToString(allvars["aws-id"]),
|
|
AwsSecretToken: types.ToString(allvars["aws-secret"]),
|
|
}
|
|
awsSigner, err := signerpool.Get(request.options.Options, &signerpool.Configuration{SignerArgs: &awsopts})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx := signer.GetCtxWithArgs(allvars, signer.AwsDefaultVars)
|
|
err = awsSigner.SignHTTP(ctx, generatedRequest.request.Request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setCustomHeaders sets the custom headers for generated request
|
|
func (request *Request) setCustomHeaders(req *generatedRequest) {
|
|
for k, v := range request.customHeaders {
|
|
if req.rawRequest != nil {
|
|
req.rawRequest.Headers[k] = v
|
|
} else {
|
|
kk, vv := strings.TrimSpace(k), strings.TrimSpace(v)
|
|
// NOTE(dwisiswant0): Do we really not need to convert it first into
|
|
// lowercase?
|
|
if kk == "Host" {
|
|
req.request.Host = vv
|
|
|
|
continue
|
|
}
|
|
|
|
req.request.Header[kk] = []string{vv}
|
|
}
|
|
}
|
|
}
|
|
|
|
const CRLF = "\r\n"
|
|
|
|
func dumpResponse(event *output.InternalWrappedEvent, request *Request, redirectedResponse string, formedURL string, responseContentType string, isResponseTruncated bool, reqURL string) {
|
|
cliOptions := request.options.Options
|
|
if cliOptions.Debug || cliOptions.DebugResponse || cliOptions.StoreResponse {
|
|
response := redirectedResponse
|
|
|
|
var highlightedResult string
|
|
if (responseContentType == "application/octet-stream" || responseContentType == "application/x-www-form-urlencoded") && responsehighlighter.HasBinaryContent(response) {
|
|
highlightedResult = createResponseHexDump(event, response, cliOptions.NoColor)
|
|
} else {
|
|
highlightedResult = responsehighlighter.Highlight(event.OperatorsResult, response, cliOptions.NoColor, false)
|
|
}
|
|
|
|
msg := "[%s] Dumped HTTP response %s\n\n%s"
|
|
if isResponseTruncated {
|
|
msg = "[%s] Dumped HTTP response (Truncated) %s\n\n%s"
|
|
}
|
|
fMsg := fmt.Sprintf(msg, request.options.TemplateID, formedURL, highlightedResult)
|
|
if cliOptions.Debug || cliOptions.DebugResponse {
|
|
gologger.Debug().Msg(fMsg)
|
|
}
|
|
if cliOptions.StoreResponse {
|
|
request.options.Output.WriteStoreDebugData(reqURL, request.options.TemplateID, request.Type().String(), fMsg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func createResponseHexDump(event *output.InternalWrappedEvent, response string, noColor bool) string {
|
|
CRLFs := CRLF + CRLF
|
|
headerEndIndex := strings.Index(response, CRLFs) + len(CRLFs)
|
|
if headerEndIndex > 0 {
|
|
headers := response[0:headerEndIndex]
|
|
responseBodyHexDump := hex.Dump([]byte(response[headerEndIndex:]))
|
|
|
|
highlightedHeaders := responsehighlighter.Highlight(event.OperatorsResult, headers, noColor, false)
|
|
highlightedResponse := responsehighlighter.Highlight(event.OperatorsResult, responseBodyHexDump, noColor, true)
|
|
return fmt.Sprintf("%s\n%s", highlightedHeaders, highlightedResponse)
|
|
} else {
|
|
return responsehighlighter.Highlight(event.OperatorsResult, hex.Dump([]byte(response)), noColor, true)
|
|
}
|
|
}
|
|
|
|
func (request *Request) pruneSignatureInternalValues(maps ...map[string]interface{}) {
|
|
var signatureFieldsToSkip map[string]interface{}
|
|
switch request.Signature.Value {
|
|
case AWSSignature:
|
|
signatureFieldsToSkip = signer.AwsInternalOnlyVars
|
|
default:
|
|
return
|
|
}
|
|
|
|
for _, m := range maps {
|
|
for fieldName := range signatureFieldsToSkip {
|
|
delete(m, fieldName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (request *Request) newContext(input *contextargs.Context) context.Context {
|
|
if input.MetaInput.CustomIP != "" {
|
|
return context.WithValue(input.Context(), fastdialer.IP, input.MetaInput.CustomIP)
|
|
}
|
|
return input.Context()
|
|
}
|
|
|
|
// markHostError checks if the error is a unreponsive host error and marks it
|
|
func (request *Request) markHostError(input *contextargs.Context, err error) {
|
|
if request.options.HostErrorsCache != nil && err != nil {
|
|
request.options.HostErrorsCache.MarkFailedOrRemove(request.options.ProtocolType.String(), input, err)
|
|
}
|
|
}
|
|
|
|
// isUnresponsiveAddress checks if the error is a unreponsive based on its execution history
|
|
func (request *Request) isUnresponsiveAddress(input *contextargs.Context) bool {
|
|
if request.options.HostErrorsCache != nil {
|
|
return request.options.HostErrorsCache.Check(request.options.ProtocolType.String(), input)
|
|
}
|
|
return false
|
|
}
|