github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/websocket/websocket.go (about)

     1  package websocket
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"path"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/gobwas/ws"
    16  	"github.com/gobwas/ws/wsutil"
    17  	"github.com/pkg/errors"
    18  
    19  	"github.com/projectdiscovery/fastdialer/fastdialer"
    20  	"github.com/projectdiscovery/gologger"
    21  	"github.com/projectdiscovery/nuclei/v2/pkg/operators"
    22  	"github.com/projectdiscovery/nuclei/v2/pkg/operators/extractors"
    23  	"github.com/projectdiscovery/nuclei/v2/pkg/operators/matchers"
    24  	"github.com/projectdiscovery/nuclei/v2/pkg/output"
    25  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
    26  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
    27  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions"
    28  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators"
    29  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/eventcreator"
    30  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/responsehighlighter"
    31  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump"
    32  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/network/networkclientpool"
    33  	protocolutils "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
    34  	templateTypes "github.com/projectdiscovery/nuclei/v2/pkg/templates/types"
    35  	"github.com/projectdiscovery/nuclei/v2/pkg/types"
    36  	urlutil "github.com/projectdiscovery/utils/url"
    37  )
    38  
    39  // Request is a request for the Websocket protocol
    40  type Request struct {
    41  	// Operators for the current request go here.
    42  	operators.Operators `yaml:",inline,omitempty" json:",inline,omitempty"`
    43  	CompiledOperators   *operators.Operators `yaml:"-" json:"-"`
    44  
    45  	// description: |
    46  	//   Address contains address for the request
    47  	Address string `yaml:"address,omitempty" json:"address,omitempty" jsonschema:"title=address for the websocket request,description=Address contains address for the request"`
    48  	// description: |
    49  	//   Inputs contains inputs for the websocket protocol
    50  	Inputs []*Input `yaml:"inputs,omitempty" json:"inputs,omitempty" jsonschema:"title=inputs for the websocket request,description=Inputs contains any input/output for the current request"`
    51  	// description: |
    52  	//   Headers contains headers for the request.
    53  	Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" jsonschema:"title=headers contains the request headers,description=Headers contains headers for the request"`
    54  
    55  	// description: |
    56  	//   Attack is the type of payload combinations to perform.
    57  	//
    58  	//   Sniper is each payload once, pitchfork combines multiple payload sets and clusterbomb generates
    59  	//   permutations and combinations for all payloads.
    60  	AttackType generators.AttackTypeHolder `yaml:"attack,omitempty" json:"attack,omitempty" jsonschema:"title=attack is the payload combination,description=Attack is the type of payload combinations to perform,enum=sniper,enum=pitchfork,enum=clusterbomb"`
    61  	// description: |
    62  	//   Payloads contains any payloads for the current request.
    63  	//
    64  	//   Payloads support both key-values combinations where a list
    65  	//   of payloads is provided, or optionally a single file can also
    66  	//   be provided as payload which will be read on run-time.
    67  	Payloads map[string]interface{} `yaml:"payloads,omitempty" json:"payloads,omitempty" jsonschema:"title=payloads for the websocket request,description=Payloads contains any payloads for the current request"`
    68  
    69  	generator *generators.PayloadGenerator
    70  
    71  	// cache any variables that may be needed for operation.
    72  	dialer  *fastdialer.Dialer
    73  	options *protocols.ExecutorOptions
    74  }
    75  
    76  // Input is an input for the websocket protocol
    77  type Input struct {
    78  	// description: |
    79  	//   Data is the data to send as the input.
    80  	//
    81  	//   It supports DSL Helper Functions as well as normal expressions.
    82  	// examples:
    83  	//   - value: "\"TEST\""
    84  	//   - value: "\"hex_decode('50494e47')\""
    85  	Data string `yaml:"data,omitempty" json:"data,omitempty" jsonschema:"title=data to send as input,description=Data is the data to send as the input"`
    86  	// description: |
    87  	//   Name is the optional name of the data read to provide matching on.
    88  	// examples:
    89  	//   - value: "\"prefix\""
    90  	Name string `yaml:"name,omitempty" json:"name,omitempty" jsonschema:"title=optional name for data read,description=Optional name of the data read to provide matching on"`
    91  }
    92  
    93  const (
    94  	parseUrlErrorMessage                   = "could not parse input url"
    95  	evaluateTemplateExpressionErrorMessage = "could not evaluate template expressions"
    96  )
    97  
    98  // Compile compiles the request generators preparing any requests possible.
    99  func (request *Request) Compile(options *protocols.ExecutorOptions) error {
   100  	request.options = options
   101  
   102  	client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{})
   103  	if err != nil {
   104  		return errors.Wrap(err, "could not get network client")
   105  	}
   106  	request.dialer = client
   107  
   108  	if len(request.Payloads) > 0 {
   109  		request.generator, err = generators.New(request.Payloads, request.AttackType.Value, request.options.TemplatePath, request.options.Options.AllowLocalFileAccess, options.Catalog, options.Options.AttackType)
   110  		if err != nil {
   111  			return errors.Wrap(err, "could not parse payloads")
   112  		}
   113  	}
   114  
   115  	if len(request.Matchers) > 0 || len(request.Extractors) > 0 {
   116  		compiled := &request.Operators
   117  		compiled.ExcludeMatchers = options.ExcludeMatchers
   118  		compiled.TemplateID = options.TemplateID
   119  		if err := compiled.Compile(); err != nil {
   120  			return errors.Wrap(err, "could not compile operators")
   121  		}
   122  		request.CompiledOperators = compiled
   123  	}
   124  	return nil
   125  }
   126  
   127  // Requests returns the total number of requests the rule will perform
   128  func (request *Request) Requests() int {
   129  	if request.generator != nil {
   130  		return request.generator.NewIterator().Total()
   131  	}
   132  	return 1
   133  }
   134  
   135  // GetID returns the ID for the request if any.
   136  func (request *Request) GetID() string {
   137  	return ""
   138  }
   139  
   140  // ExecuteWithResults executes the protocol requests and returns results instead of writing them.
   141  func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
   142  	hostname, err := getAddress(input.MetaInput.Input)
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	if request.generator != nil {
   148  		iterator := request.generator.NewIterator()
   149  
   150  		for {
   151  			value, ok := iterator.Value()
   152  			if !ok {
   153  				break
   154  			}
   155  			if err := request.executeRequestWithPayloads(input.MetaInput.Input, hostname, value, previous, callback); err != nil {
   156  				return err
   157  			}
   158  		}
   159  	} else {
   160  		value := make(map[string]interface{})
   161  		if err := request.executeRequestWithPayloads(input.MetaInput.Input, hostname, value, previous, callback); err != nil {
   162  			return err
   163  		}
   164  	}
   165  	return nil
   166  }
   167  
   168  // ExecuteWithResults executes the protocol requests and returns results instead of writing them.
   169  func (request *Request) executeRequestWithPayloads(input, hostname string, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
   170  	header := http.Header{}
   171  
   172  	parsed, err := urlutil.Parse(input)
   173  	if err != nil {
   174  		return errors.Wrap(err, parseUrlErrorMessage)
   175  	}
   176  	defaultVars := protocolutils.GenerateVariables(parsed, false, nil)
   177  	optionVars := generators.BuildPayloadFromOptions(request.options.Options)
   178  	variables := request.options.Variables.Evaluate(generators.MergeMaps(defaultVars, optionVars, dynamicValues))
   179  	payloadValues := generators.MergeMaps(variables, defaultVars, optionVars, dynamicValues, request.options.Constants)
   180  
   181  	requestOptions := request.options
   182  	for key, value := range request.Headers {
   183  		finalData, dataErr := expressions.EvaluateByte([]byte(value), payloadValues)
   184  		if dataErr != nil {
   185  			requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), dataErr)
   186  			requestOptions.Progress.IncrementFailedRequestsBy(1)
   187  			return errors.Wrap(dataErr, evaluateTemplateExpressionErrorMessage)
   188  		}
   189  		header.Set(key, string(finalData))
   190  	}
   191  	tlsConfig := &tls.Config{
   192  		InsecureSkipVerify: true,
   193  		ServerName:         hostname,
   194  		MinVersion:         tls.VersionTLS10,
   195  	}
   196  	if requestOptions.Options.SNI != "" {
   197  		tlsConfig.ServerName = requestOptions.Options.SNI
   198  	}
   199  	websocketDialer := ws.Dialer{
   200  		Header:    ws.HandshakeHeaderHTTP(header),
   201  		Timeout:   time.Duration(requestOptions.Options.Timeout) * time.Second,
   202  		NetDial:   request.dialer.Dial,
   203  		TLSConfig: tlsConfig,
   204  	}
   205  
   206  	if vardump.EnableVarDump {
   207  		gologger.Debug().Msgf("Protocol request variables: \n%s\n", vardump.DumpVariables(payloadValues))
   208  	}
   209  
   210  	finalAddress, dataErr := expressions.EvaluateByte([]byte(request.Address), payloadValues)
   211  	if dataErr != nil {
   212  		requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), dataErr)
   213  		requestOptions.Progress.IncrementFailedRequestsBy(1)
   214  		return errors.Wrap(dataErr, evaluateTemplateExpressionErrorMessage)
   215  	}
   216  
   217  	addressToDial := string(finalAddress)
   218  	parsedAddress, err := url.Parse(addressToDial)
   219  	if err != nil {
   220  		requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), err)
   221  		requestOptions.Progress.IncrementFailedRequestsBy(1)
   222  		return errors.Wrap(err, parseUrlErrorMessage)
   223  	}
   224  	parsedAddress.Path = path.Join(parsedAddress.Path, parsed.Path)
   225  	addressToDial = parsedAddress.String()
   226  
   227  	conn, readBuffer, _, err := websocketDialer.Dial(context.Background(), addressToDial)
   228  	if err != nil {
   229  		requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), err)
   230  		requestOptions.Progress.IncrementFailedRequestsBy(1)
   231  		return errors.Wrap(err, "could not connect to server")
   232  	}
   233  	defer conn.Close()
   234  
   235  	responseBuilder := &strings.Builder{}
   236  	if readBuffer != nil {
   237  		_, _ = io.Copy(responseBuilder, readBuffer) // Copy initial response
   238  	}
   239  
   240  	events, requestOutput, err := request.readWriteInputWebsocket(conn, payloadValues, input, responseBuilder)
   241  	if err != nil {
   242  		requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), err)
   243  		requestOptions.Progress.IncrementFailedRequestsBy(1)
   244  		return errors.Wrap(err, "could not read write response")
   245  	}
   246  	requestOptions.Progress.IncrementRequests()
   247  
   248  	if requestOptions.Options.Debug || requestOptions.Options.DebugRequests {
   249  		gologger.Debug().Str("address", input).Msgf("[%s] Dumped Websocket request for %s", requestOptions.TemplateID, input)
   250  		gologger.Print().Msgf("%s", requestOutput)
   251  	}
   252  
   253  	requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), err)
   254  	gologger.Verbose().Msgf("Sent Websocket request to %s", input)
   255  
   256  	data := make(map[string]interface{})
   257  	for k, v := range previous {
   258  		data[k] = v
   259  	}
   260  	for k, v := range events {
   261  		data[k] = v
   262  	}
   263  
   264  	data["type"] = request.Type().String()
   265  	data["success"] = "true"
   266  	data["request"] = requestOutput
   267  	data["response"] = responseBuilder.String()
   268  	data["host"] = input
   269  	data["matched"] = addressToDial
   270  	data["ip"] = request.dialer.GetDialedIP(hostname)
   271  
   272  	event := eventcreator.CreateEventWithAdditionalOptions(request, data, requestOptions.Options.Debug || requestOptions.Options.DebugResponse, func(internalWrappedEvent *output.InternalWrappedEvent) {
   273  		internalWrappedEvent.OperatorsResult.PayloadValues = payloadValues
   274  	})
   275  	if requestOptions.Options.Debug || requestOptions.Options.DebugResponse {
   276  		responseOutput := responseBuilder.String()
   277  		gologger.Debug().Msgf("[%s] Dumped Websocket response for %s", requestOptions.TemplateID, input)
   278  		gologger.Print().Msgf("%s", responsehighlighter.Highlight(event.OperatorsResult, responseOutput, requestOptions.Options.NoColor, false))
   279  	}
   280  
   281  	callback(event)
   282  	return nil
   283  }
   284  
   285  func (request *Request) readWriteInputWebsocket(conn net.Conn, payloadValues map[string]interface{}, input string, respBuilder *strings.Builder) (events map[string]interface{}, req string, err error) {
   286  	reqBuilder := &strings.Builder{}
   287  	inputEvents := make(map[string]interface{})
   288  
   289  	requestOptions := request.options
   290  	for _, req := range request.Inputs {
   291  		reqBuilder.Grow(len(req.Data))
   292  
   293  		finalData, dataErr := expressions.EvaluateByte([]byte(req.Data), payloadValues)
   294  		if dataErr != nil {
   295  			requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), dataErr)
   296  			requestOptions.Progress.IncrementFailedRequestsBy(1)
   297  			return nil, "", errors.Wrap(dataErr, evaluateTemplateExpressionErrorMessage)
   298  		}
   299  		reqBuilder.WriteString(string(finalData))
   300  
   301  		err = wsutil.WriteClientMessage(conn, ws.OpText, finalData)
   302  		if err != nil {
   303  			requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), err)
   304  			requestOptions.Progress.IncrementFailedRequestsBy(1)
   305  			return nil, "", errors.Wrap(err, "could not write request to server")
   306  		}
   307  
   308  		msg, opCode, err := wsutil.ReadServerData(conn)
   309  		if err != nil {
   310  			requestOptions.Output.Request(requestOptions.TemplateID, input, request.Type().String(), err)
   311  			requestOptions.Progress.IncrementFailedRequestsBy(1)
   312  			return nil, "", errors.Wrap(err, "could not write request to server")
   313  		}
   314  		// Only perform matching and writes in case we receive
   315  		// text or binary opcode from the websocket server.
   316  		if opCode != ws.OpText && opCode != ws.OpBinary {
   317  			continue
   318  		}
   319  
   320  		respBuilder.Write(msg)
   321  		if req.Name != "" {
   322  			bufferStr := string(msg)
   323  			inputEvents[req.Name] = bufferStr
   324  
   325  			// Run any internal extractors for the request here and add found values to map.
   326  			if request.CompiledOperators != nil {
   327  				values := request.CompiledOperators.ExecuteInternalExtractors(map[string]interface{}{req.Name: bufferStr}, protocols.MakeDefaultExtractFunc)
   328  				for k, v := range values {
   329  					inputEvents[k] = v
   330  				}
   331  			}
   332  		}
   333  	}
   334  	return inputEvents, reqBuilder.String(), nil
   335  }
   336  
   337  // getAddress returns the address of the host to make request to
   338  func getAddress(toTest string) (string, error) {
   339  	parsed, err := url.Parse(toTest)
   340  	if err != nil {
   341  		return "", errors.Wrap(err, parseUrlErrorMessage)
   342  	}
   343  	scheme := strings.ToLower(parsed.Scheme)
   344  
   345  	if scheme != "ws" && scheme != "wss" {
   346  		return "", fmt.Errorf("invalid url scheme provided: %s", scheme)
   347  	}
   348  	if parsed != nil && parsed.Host != "" {
   349  		return parsed.Host, nil
   350  	}
   351  	return "", nil
   352  }
   353  
   354  // Match performs matching operation for a matcher on model and returns:
   355  // true and a list of matched snippets if the matcher type is supports it
   356  // otherwise false and an empty string slice
   357  func (request *Request) Match(data map[string]interface{}, matcher *matchers.Matcher) (bool, []string) {
   358  	return protocols.MakeDefaultMatchFunc(data, matcher)
   359  }
   360  
   361  // Extract performs extracting operation for an extractor on model and returns true or false.
   362  func (request *Request) Extract(data map[string]interface{}, matcher *extractors.Extractor) map[string]struct{} {
   363  	return protocols.MakeDefaultExtractFunc(data, matcher)
   364  }
   365  
   366  // MakeResultEvent creates a result event from internal wrapped event
   367  func (request *Request) MakeResultEvent(wrapped *output.InternalWrappedEvent) []*output.ResultEvent {
   368  	return protocols.MakeDefaultResultEvent(request, wrapped)
   369  }
   370  
   371  // GetCompiledOperators returns a list of the compiled operators
   372  func (request *Request) GetCompiledOperators() []*operators.Operators {
   373  	return []*operators.Operators{request.CompiledOperators}
   374  }
   375  
   376  // RequestPartDefinitions contains a mapping of request part definitions and their
   377  // description. Multiple definitions are separated by commas.
   378  // Definitions not having a name (generated on runtime) are prefixed & suffixed by <>.
   379  var RequestPartDefinitions = map[string]string{
   380  	"type":     "Type is the type of request made",
   381  	"success":  "Success specifies whether websocket connection was successful",
   382  	"request":  "Websocket request made to the server",
   383  	"response": "Websocket response received from the server",
   384  	"host":     "Host is the input to the template",
   385  	"matched":  "Matched is the input which was matched upon",
   386  }
   387  
   388  func (request *Request) MakeResultEventItem(wrapped *output.InternalWrappedEvent) *output.ResultEvent {
   389  	data := &output.ResultEvent{
   390  		TemplateID:       types.ToString(request.options.TemplateID),
   391  		TemplatePath:     types.ToString(request.options.TemplatePath),
   392  		Info:             request.options.TemplateInfo,
   393  		Type:             types.ToString(wrapped.InternalEvent["type"]),
   394  		Host:             types.ToString(wrapped.InternalEvent["host"]),
   395  		Matched:          types.ToString(wrapped.InternalEvent["matched"]),
   396  		Metadata:         wrapped.OperatorsResult.PayloadValues,
   397  		ExtractedResults: wrapped.OperatorsResult.OutputExtracts,
   398  		Timestamp:        time.Now(),
   399  		MatcherStatus:    true,
   400  		IP:               types.ToString(wrapped.InternalEvent["ip"]),
   401  		Request:          types.ToString(wrapped.InternalEvent["request"]),
   402  		Response:         types.ToString(wrapped.InternalEvent["response"]),
   403  	}
   404  	return data
   405  }
   406  
   407  // Type returns the type of the protocol request
   408  func (request *Request) Type() templateTypes.ProtocolType {
   409  	return templateTypes.WebsocketProtocol
   410  }