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