Path: blob/dev/pkg/protocols/network/request.go
2070 views
package network12import (3"encoding/hex"4"fmt"5maps0 "maps"6"net"7"net/url"8"os"9"strings"10"sync"11"sync/atomic"12"time"1314"github.com/pkg/errors"15"go.uber.org/multierr"16"golang.org/x/exp/maps"1718"github.com/projectdiscovery/gologger"19"github.com/projectdiscovery/nuclei/v3/pkg/operators"20"github.com/projectdiscovery/nuclei/v3/pkg/output"21"github.com/projectdiscovery/nuclei/v3/pkg/protocols"22"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"23"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"24"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"25"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"26"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"27"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"28"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"29"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"30"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool"31protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"32templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"33"github.com/projectdiscovery/utils/errkit"34mapsutil "github.com/projectdiscovery/utils/maps"35"github.com/projectdiscovery/utils/reader"36syncutil "github.com/projectdiscovery/utils/sync"37)3839var _ protocols.Request = &Request{}4041// Type returns the type of the protocol request42func (request *Request) Type() templateTypes.ProtocolType {43return templateTypes.NetworkProtocol44}4546// getOpenPorts returns all open ports from list of ports provided in template47// if only 1 port is provided, no need to check if port is open or not48func (request *Request) getOpenPorts(target *contextargs.Context) ([]string, error) {49if len(request.ports) == 1 {50// no need to check if port is open or not51return request.ports, nil52}53errs := []error{}54// if more than 1 port is provided, check if port is open or not55openPorts := make([]string, 0)56for _, port := range request.ports {57cloned := target.Clone()58if err := cloned.UseNetworkPort(port, request.ExcludePorts); err != nil {59errs = append(errs, err)60continue61}62addr, err := getAddress(cloned.MetaInput.Input)63if err != nil {64errs = append(errs, err)65continue66}67if request.dialer == nil {68request.dialer, _ = networkclientpool.Get(request.options.Options, &networkclientpool.Configuration{})69}7071conn, err := request.dialer.Dial(target.Context(), "tcp", addr)72if err != nil {73errs = append(errs, err)74continue75}76_ = conn.Close()77openPorts = append(openPorts, port)78}79if len(openPorts) == 0 {80return nil, multierr.Combine(errs...)81}82return openPorts, nil83}8485// ExecuteWithResults executes the protocol requests and returns results instead of writing them.86func (request *Request) ExecuteWithResults(target *contextargs.Context, metadata, previous output.InternalEvent, callback protocols.OutputEventCallback) error {87visitedAddresses := make(mapsutil.Map[string, struct{}])8889if request.Port == "" {90// backwords compatibility or for other use cases91// where port is not provided in template92if err := request.executeOnTarget(target, visitedAddresses, metadata, previous, callback); err != nil {93return err94}95}9697// get open ports from list of ports provided in template98ports, err := request.getOpenPorts(target)99if len(ports) == 0 {100return err101}102if err != nil {103// TODO: replace this after scan context is implemented104gologger.Verbose().Msgf("[%v] got errors while checking open ports: %s\n", request.options.TemplateID, err)105}106107// stop at first match if requested108atomicBool := &atomic.Bool{}109shouldStopAtFirstMatch := request.StopAtFirstMatch || request.options.StopAtFirstMatch || request.options.Options.StopAtFirstMatch110wrappedCallback := func(event *output.InternalWrappedEvent) {111if event != nil && event.HasOperatorResult() {112atomicBool.Store(true)113}114callback(event)115}116117for _, port := range ports {118input := target.Clone()119// use network port updates input with new port requested in template file120// and it is ignored if input port is not standard http(s) ports like 80,8080,8081 etc121// idea is to reduce redundant dials to http ports122if err := input.UseNetworkPort(port, request.ExcludePorts); err != nil {123gologger.Debug().Msgf("Could not network port from constants: %s\n", err)124}125if err := request.executeOnTarget(input, visitedAddresses, metadata, previous, wrappedCallback); err != nil {126return err127}128if shouldStopAtFirstMatch && atomicBool.Load() {129break130}131}132133return nil134}135136func (request *Request) executeOnTarget(input *contextargs.Context, visited mapsutil.Map[string, struct{}], metadata, previous output.InternalEvent, callback protocols.OutputEventCallback) error {137var address string138var err error139if request.isUnresponsiveAddress(input) {140// skip on unresponsive address no need to continue141return nil142}143144if request.SelfContained {145address = ""146} else {147address, err = getAddress(input.MetaInput.Input)148}149if err != nil {150request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)151request.options.Progress.IncrementFailedRequestsBy(1)152return errors.Wrap(err, "could not get address from url")153}154variables := protocolutils.GenerateVariables(address, false, nil)155// add template ctx variables to varMap156if request.options.HasTemplateCtx(input.MetaInput) {157variables = generators.MergeMaps(variables, request.options.GetTemplateCtx(input.MetaInput).GetAll())158}159variablesMap := request.options.Variables.Evaluate(variables)160variables = generators.MergeMaps(variablesMap, variables, request.options.Constants)161162// stop at first match if requested163atomicBool := &atomic.Bool{}164shouldStopAtFirstMatch := request.StopAtFirstMatch || request.options.StopAtFirstMatch || request.options.Options.StopAtFirstMatch165wrappedCallback := func(event *output.InternalWrappedEvent) {166if event != nil && event.HasOperatorResult() {167atomicBool.Store(true)168}169callback(event)170}171172for _, kv := range request.addresses {173select {174case <-input.Context().Done():175return input.Context().Err()176default:177}178179actualAddress := replacer.Replace(kv.address, variables)180181if visited.Has(actualAddress) && !request.options.Options.DisableClustering {182continue183}184visited.Set(actualAddress, struct{}{})185if err = request.executeAddress(variables, actualAddress, address, input, kv.tls, previous, wrappedCallback); err != nil {186outputEvent := request.responseToDSLMap("", "", "", address, "")187callback(&output.InternalWrappedEvent{InternalEvent: outputEvent})188gologger.Warning().Msgf("[%v] Could not make network request for (%s) : %s\n", request.options.TemplateID, actualAddress, err)189}190if shouldStopAtFirstMatch && atomicBool.Load() {191break192}193}194return err195}196197// executeAddress executes the request for an address198func (request *Request) executeAddress(variables map[string]interface{}, actualAddress, address string, input *contextargs.Context, shouldUseTLS bool, previous output.InternalEvent, callback protocols.OutputEventCallback) error {199variables = generators.MergeMaps(variables, map[string]interface{}{"Hostname": address})200payloads := generators.BuildPayloadFromOptions(request.options.Options)201202if !strings.Contains(actualAddress, ":") {203err := errors.New("no port provided in network protocol request")204request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)205request.options.Progress.IncrementFailedRequestsBy(1)206return err207}208updatedTarget := input.Clone()209updatedTarget.MetaInput.Input = actualAddress210211// if request threads matches global payload concurrency we follow it212shouldFollowGlobal := request.Threads == request.options.Options.PayloadConcurrency213214if request.generator != nil {215iterator := request.generator.NewIterator()216var multiErr error217m := &sync.Mutex{}218swg, err := syncutil.New(syncutil.WithSize(request.Threads))219if err != nil {220return err221}222223for {224value, ok := iterator.Value()225if !ok {226break227}228229select {230case <-input.Context().Done():231return input.Context().Err()232default:233}234235// resize check point - nop if there are no changes236if shouldFollowGlobal && swg.Size != request.options.Options.PayloadConcurrency {237if err := swg.Resize(input.Context(), request.options.Options.PayloadConcurrency); err != nil {238m.Lock()239multiErr = multierr.Append(multiErr, err)240m.Unlock()241}242}243if request.isUnresponsiveAddress(updatedTarget) {244// skip on unresponsive address no need to continue245return nil246}247248value = generators.MergeMaps(value, payloads)249swg.Add()250go func(vars map[string]interface{}) {251defer swg.Done()252if request.isUnresponsiveAddress(updatedTarget) {253// skip on unresponsive address no need to continue254return255}256if err := request.executeRequestWithPayloads(variables, actualAddress, address, input, shouldUseTLS, vars, previous, callback); err != nil {257m.Lock()258multiErr = multierr.Append(multiErr, err)259m.Unlock()260}261}(value)262}263swg.Wait()264if multiErr != nil {265return multiErr266}267} else {268value := maps.Clone(payloads)269if err := request.executeRequestWithPayloads(variables, actualAddress, address, input, shouldUseTLS, value, previous, callback); err != nil {270return err271}272}273return nil274}275276func (request *Request) executeRequestWithPayloads(variables map[string]interface{}, actualAddress, address string, input *contextargs.Context, shouldUseTLS bool, payloads map[string]interface{}, previous output.InternalEvent, callback protocols.OutputEventCallback) error {277var (278hostname string279conn net.Conn280err error281)282if host, _, err := net.SplitHostPort(actualAddress); err == nil {283hostname = host284}285updatedTarget := input.Clone()286updatedTarget.MetaInput.Input = actualAddress287288if request.isUnresponsiveAddress(updatedTarget) {289// skip on unresponsive address no need to continue290return nil291}292293if shouldUseTLS {294conn, err = request.dialer.DialTLS(input.Context(), "tcp", actualAddress)295} else {296conn, err = request.dialer.Dial(input.Context(), "tcp", actualAddress)297}298// adds it to unresponsive address list if applicable299request.markHostError(updatedTarget, err)300if err != nil {301request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)302request.options.Progress.IncrementFailedRequestsBy(1)303return errors.Wrap(err, "could not connect to server")304}305defer func() {306_ = conn.Close()307}()308_ = conn.SetDeadline(time.Now().Add(time.Duration(request.options.Options.Timeout) * time.Second))309310var interactshURLs []string311312var responseBuilder, reqBuilder strings.Builder313314interimValues := generators.MergeMaps(variables, payloads)315316if vardump.EnableVarDump {317gologger.Debug().Msgf("Network Protocol request variables: %s\n", vardump.DumpVariables(interimValues))318}319320inputEvents := make(map[string]interface{})321322for _, input := range request.Inputs {323dataInBytes := []byte(input.Data)324var err error325326dataInBytes, err = expressions.EvaluateByte(dataInBytes, interimValues)327if err != nil {328request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)329request.options.Progress.IncrementFailedRequestsBy(1)330return errors.Wrap(err, "could not evaluate template expressions")331}332333data := string(dataInBytes)334if request.options.Interactsh != nil {335data, interactshURLs = request.options.Interactsh.Replace(data, []string{})336dataInBytes = []byte(data)337}338339reqBuilder.Write(dataInBytes)340341if err := expressions.ContainsUnresolvedVariables(data); err != nil {342gologger.Warning().Msgf("[%s] Could not make network request for %s: %v\n", request.options.TemplateID, actualAddress, err)343return nil344}345346if input.Type.GetType() == hexType {347dataInBytes, err = hex.DecodeString(data)348if err != nil {349request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)350request.options.Progress.IncrementFailedRequestsBy(1)351return errors.Wrap(err, "could not write request to server")352}353}354355if _, err := conn.Write(dataInBytes); err != nil {356request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)357request.options.Progress.IncrementFailedRequestsBy(1)358return errors.Wrap(err, "could not write request to server")359}360361if input.Read > 0 {362buffer, err := ConnReadNWithTimeout(conn, int64(input.Read), request.options.Options.GetTimeouts().TcpReadTimeout)363if err != nil {364return errkit.Wrap(err, "could not read response from connection")365}366367responseBuilder.Write(buffer)368369bufferStr := string(buffer)370if input.Name != "" {371inputEvents[input.Name] = bufferStr372interimValues[input.Name] = bufferStr373}374375// Run any internal extractors for the request here and add found values to map.376if request.CompiledOperators != nil {377values := request.CompiledOperators.ExecuteInternalExtractors(map[string]interface{}{input.Name: bufferStr}, request.Extract)378maps0.Copy(payloads, values)379}380}381}382383request.options.Progress.IncrementRequests()384385if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.StoreResponse {386requestBytes := []byte(reqBuilder.String())387msg := fmt.Sprintf("[%s] Dumped Network request for %s\n%s", request.options.TemplateID, actualAddress, hex.Dump(requestBytes))388if request.options.Options.Debug || request.options.Options.DebugRequests {389gologger.Info().Str("address", actualAddress).Msg(msg)390}391if request.options.Options.StoreResponse {392request.options.Output.WriteStoreDebugData(address, request.options.TemplateID, request.Type().String(), msg)393}394if request.options.Options.VerboseVerbose {395gologger.Print().Msgf("\nCompact HEX view:\n%s", hex.EncodeToString(requestBytes))396}397}398399request.options.Output.Request(request.options.TemplatePath, actualAddress, request.Type().String(), err)400gologger.Verbose().Msgf("Sent TCP request to %s", actualAddress)401402bufferSize := 1024403if request.ReadSize != 0 {404bufferSize = request.ReadSize405}406if request.ReadAll {407bufferSize = -1408}409410final, err := ConnReadNWithTimeout(conn, int64(bufferSize), request.options.Options.GetTimeouts().TcpReadTimeout)411if err != nil {412request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)413gologger.Verbose().Msgf("could not read more data from %s: %s", actualAddress, err)414}415responseBuilder.Write(final)416417response := responseBuilder.String()418outputEvent := request.responseToDSLMap(reqBuilder.String(), string(final), response, input.MetaInput.Input, actualAddress)419// add response fields to template context and merge templatectx variables to output event420request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, outputEvent)421if request.options.HasTemplateCtx(input.MetaInput) {422outputEvent = generators.MergeMaps(outputEvent, request.options.GetTemplateCtx(input.MetaInput).GetAll())423}424outputEvent["ip"] = request.dialer.GetDialedIP(hostname)425if request.options.StopAtFirstMatch {426outputEvent["stop-at-first-match"] = true427}428maps0.Copy(outputEvent, previous)429maps0.Copy(outputEvent, interimValues)430maps0.Copy(outputEvent, inputEvents)431if request.options.Interactsh != nil {432request.options.Interactsh.MakePlaceholders(interactshURLs, outputEvent)433}434435var event *output.InternalWrappedEvent436if len(interactshURLs) == 0 {437event = eventcreator.CreateEventWithAdditionalOptions(request, generators.MergeMaps(payloads, outputEvent), request.options.Options.Debug || request.options.Options.DebugResponse, func(wrappedEvent *output.InternalWrappedEvent) {438wrappedEvent.OperatorsResult.PayloadValues = payloads439})440callback(event)441} else if request.options.Interactsh != nil {442event = &output.InternalWrappedEvent{InternalEvent: outputEvent}443request.options.Interactsh.RequestEvent(interactshURLs, &interactsh.RequestData{444MakeResultFunc: request.MakeResultEvent,445Event: event,446Operators: request.CompiledOperators,447MatchFunc: request.Match,448ExtractFunc: request.Extract,449})450}451if len(interactshURLs) > 0 {452event.UsesInteractsh = true453}454455dumpResponse(event, request, response, actualAddress, address)456457return nil458}459460func dumpResponse(event *output.InternalWrappedEvent, request *Request, response string, actualAddress, address string) {461cliOptions := request.options.Options462if cliOptions.Debug || cliOptions.DebugResponse || cliOptions.StoreResponse {463requestBytes := []byte(response)464highlightedResponse := responsehighlighter.Highlight(event.OperatorsResult, hex.Dump(requestBytes), cliOptions.NoColor, true)465msg := fmt.Sprintf("[%s] Dumped Network response for %s\n\n", request.options.TemplateID, actualAddress)466if cliOptions.Debug || cliOptions.DebugResponse {467gologger.Debug().Msg(fmt.Sprintf("%s%s", msg, highlightedResponse))468}469if cliOptions.StoreResponse {470request.options.Output.WriteStoreDebugData(address, request.options.TemplateID, request.Type().String(), fmt.Sprintf("%s%s", msg, hex.Dump(requestBytes)))471}472if cliOptions.VerboseVerbose {473displayCompactHexView(event, response, cliOptions.NoColor)474}475}476}477478func displayCompactHexView(event *output.InternalWrappedEvent, response string, noColor bool) {479operatorsResult := event.OperatorsResult480if operatorsResult != nil {481var allMatches []string482for _, namedMatch := range operatorsResult.Matches {483for _, matchElement := range namedMatch {484allMatches = append(allMatches, hex.EncodeToString([]byte(matchElement)))485}486}487tempOperatorResult := &operators.Result{Matches: map[string][]string{"matchesInHex": allMatches}}488gologger.Print().Msgf("\nCompact HEX view:\n%s", responsehighlighter.Highlight(tempOperatorResult, hex.EncodeToString([]byte(response)), noColor, false))489}490}491492// getAddress returns the address of the host to make request to493func getAddress(toTest string) (string, error) {494if strings.Contains(toTest, "://") {495parsed, err := url.Parse(toTest)496if err != nil {497return "", err498}499toTest = parsed.Host500}501return toTest, nil502}503504func ConnReadNWithTimeout(conn net.Conn, n int64, timeout time.Duration) ([]byte, error) {505switch n {506case -1:507// if n is -1 then read all available data from connection508return reader.ConnReadNWithTimeout(conn, -1, timeout)509case 0:510n = 4096 // default buffer size511}512b := make([]byte, n)513_ = conn.SetDeadline(time.Now().Add(timeout))514count, err := conn.Read(b)515_ = conn.SetDeadline(time.Time{})516if err != nil && os.IsTimeout(err) && count > 0 {517// in case of timeout with some value read, return the value518return b[:count], nil519}520if err != nil {521return nil, err522}523return b[:count], nil524}525526// markHostError checks if the error is a unreponsive host error and marks it527func (request *Request) markHostError(input *contextargs.Context, err error) {528if request.options.HostErrorsCache != nil {529request.options.HostErrorsCache.MarkFailedOrRemove(request.options.ProtocolType.String(), input, err)530}531}532533// isUnresponsiveAddress checks if the error is a unreponsive based on its execution history534func (request *Request) isUnresponsiveAddress(input *contextargs.Context) bool {535if request.options.HostErrorsCache != nil {536return request.options.HostErrorsCache.Check(request.options.ProtocolType.String(), input)537}538return false539}540541542