mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-18 05:05:23 +00:00
Added websocket protocol support to nuclei
This commit is contained in:
parent
0b11b80d8a
commit
396f17484e
@ -79,12 +79,16 @@ require (
|
||||
github.com/eggsampler/acme/v3 v3.2.1 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.1.0 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/google/uuid v1.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
|
||||
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
|
||||
|
||||
@ -143,6 +143,12 @@ github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
|
||||
github.com/go-rod/rod v0.91.1/go.mod h1:/W4lcZiCALPD603MnJGIvhtywP3R6yRB9EDfFfsHiiI=
|
||||
github.com/go-rod/rod v0.101.7 h1:kbI5CNvcRhf7feybBln4xDutsM0mbsF0ENNZfKcF6WA=
|
||||
github.com/go-rod/rod v0.101.7/go.mod h1:N/zlT53CfSpq74nb6rOR0K8UF0SPUPBmzBnArrms+mY=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@ -214,6 +220,8 @@ github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
|
||||
github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
|
||||
github.com/gosuri/uiprogress v0.0.1 h1:0kpv/XY/qTmFWl/SkaJykZXrBBzwwadmW8fRb7RJSxw=
|
||||
@ -637,6 +645,7 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
300
v2/pkg/protocols/others/websocket/websocket.go
Normal file
300
v2/pkg/protocols/others/websocket/websocket.go
Normal file
@ -0,0 +1,300 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/fastdialer/fastdialer"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/operators"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/network/networkclientpool"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/others/utils"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||
)
|
||||
|
||||
// Request is a request for the Websocket protocol
|
||||
type Request struct {
|
||||
// Operators for the current request go here.
|
||||
operators.Operators `yaml:",inline,omitempty"`
|
||||
CompiledOperators *operators.Operators `yaml:"-"`
|
||||
|
||||
// description: |
|
||||
// Inputs contains inputs for the websocket protocol
|
||||
Inputs []*Input `yaml:"inputs,omitempty" jsonschema:"title=inputs for the websocket request,description=Inputs contains any input/output for the current request"`
|
||||
|
||||
// description: |
|
||||
// Attack is the type of payload combinations to perform.
|
||||
//
|
||||
// Sniper is each payload once, pitchfork combines multiple payload sets and clusterbomb generates
|
||||
// permutations and combinations for all payloads.
|
||||
// values:
|
||||
// - "sniper"
|
||||
// - "pitchfork"
|
||||
// - "clusterbomb"
|
||||
AttackType string `yaml:"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"`
|
||||
// description: |
|
||||
// Payloads contains any payloads for the current request.
|
||||
//
|
||||
// Payloads support both key-values combinations where a list
|
||||
// of payloads is provided, or optionally a single file can also
|
||||
// be provided as payload which will be read on run-time.
|
||||
Payloads map[string]interface{} `yaml:"payloads,omitempty" jsonschema:"title=payloads for the webosocket request,description=Payloads contains any payloads for the current request"`
|
||||
|
||||
generator *generators.Generator
|
||||
attackType generators.Type
|
||||
|
||||
// cache any variables that may be needed for operation.
|
||||
dialer *fastdialer.Dialer
|
||||
options *protocols.ExecuterOptions
|
||||
}
|
||||
|
||||
// Input is an input for the websocket protocol
|
||||
type Input struct {
|
||||
// description: |
|
||||
// Data is the data to send as the input.
|
||||
//
|
||||
// It supports DSL Helper Functions as well as normal expressions.
|
||||
// examples:
|
||||
// - value: "\"TEST\""
|
||||
// - value: "\"hex_decode('50494e47')\""
|
||||
Data string `yaml:"data,omitempty" jsonschema:"title=data to send as input,description=Data is the data to send as the input"`
|
||||
// description: |
|
||||
// Name is the optional name of the data read to provide matching on.
|
||||
// examples:
|
||||
// - value: "\"prefix\""
|
||||
Name string `yaml:"name,omitempty" jsonschema:"title=optional name for data read,description=Optional name of the data read to provide matching on"`
|
||||
}
|
||||
|
||||
// Compile compiles the request generators preparing any requests possible.
|
||||
func (r *Request) Compile(options *protocols.ExecuterOptions) error {
|
||||
r.options = options
|
||||
|
||||
client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get network client")
|
||||
}
|
||||
r.dialer = client
|
||||
|
||||
if len(r.Payloads) > 0 {
|
||||
attackType := r.AttackType
|
||||
if attackType == "" {
|
||||
attackType = "sniper"
|
||||
}
|
||||
r.attackType = generators.StringToType[attackType]
|
||||
|
||||
// Resolve payload paths if they are files.
|
||||
for name, payload := range r.Payloads {
|
||||
payloadStr, ok := payload.(string)
|
||||
if ok {
|
||||
final, resolveErr := options.Catalog.ResolvePath(payloadStr, options.TemplatePath)
|
||||
if resolveErr != nil {
|
||||
return errors.Wrap(resolveErr, "could not read payload file")
|
||||
}
|
||||
r.Payloads[name] = final
|
||||
}
|
||||
}
|
||||
r.generator, err = generators.New(r.Payloads, r.attackType, r.options.TemplatePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse payloads")
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.Matchers) > 0 || len(r.Extractors) > 0 {
|
||||
compiled := &r.Operators
|
||||
if err := compiled.Compile(); err != nil {
|
||||
return errors.Wrap(err, "could not compile operators")
|
||||
}
|
||||
r.CompiledOperators = compiled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Requests returns the total number of requests the rule will perform
|
||||
func (r *Request) Requests() int {
|
||||
if r.generator != nil {
|
||||
return r.generator.NewIterator().Total()
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// GetID returns the ID for the request if any.
|
||||
func (r *Request) GetID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
|
||||
func (r *Request) ExecuteWithResults(input string, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
|
||||
hostname, err := getAddress(input)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.generator != nil {
|
||||
iterator := r.generator.NewIterator()
|
||||
|
||||
for {
|
||||
value, ok := iterator.Value()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if err := r.executeRequestWithPayloads(input, hostname, value, previous, callback); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value := make(map[string]interface{})
|
||||
if err := r.executeRequestWithPayloads(input, hostname, value, previous, callback); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
|
||||
func (r *Request) executeRequestWithPayloads(input, hostname string, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
|
||||
websocketDialer := ws.Dialer{
|
||||
Timeout: time.Duration(r.options.Options.Timeout) * time.Second,
|
||||
NetDial: r.dialer.Dial,
|
||||
TLSConfig: &tls.Config{InsecureSkipVerify: true, ServerName: hostname},
|
||||
}
|
||||
|
||||
conn, readBuffer, _, err := websocketDialer.Dial(context.Background(), input)
|
||||
if err != nil {
|
||||
r.options.Output.Request(r.options.TemplateID, input, "ssl", err)
|
||||
r.options.Progress.IncrementFailedRequestsBy(1)
|
||||
return errors.Wrap(err, "could not connect to server")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
responseBuilder := &strings.Builder{}
|
||||
if readBuffer != nil {
|
||||
io.Copy(responseBuilder, readBuffer) // Copy initial response
|
||||
}
|
||||
|
||||
reqBuilder := &strings.Builder{}
|
||||
|
||||
inputEvents := make(map[string]interface{})
|
||||
for _, req := range r.Inputs {
|
||||
reqBuilder.Grow(len(req.Data))
|
||||
reqBuilder.WriteString(req.Data)
|
||||
|
||||
finalData, dataErr := expressions.EvaluateByte([]byte(req.Data), dynamicValues)
|
||||
if dataErr != nil {
|
||||
r.options.Output.Request(r.options.TemplateID, input, "websocket", dataErr)
|
||||
r.options.Progress.IncrementFailedRequestsBy(1)
|
||||
return errors.Wrap(dataErr, "could not evaluate template expressions")
|
||||
}
|
||||
err = wsutil.WriteClientMessage(conn, ws.OpText, finalData)
|
||||
if err != nil {
|
||||
r.options.Output.Request(r.options.TemplateID, input, "websocket", err)
|
||||
r.options.Progress.IncrementFailedRequestsBy(1)
|
||||
return errors.Wrap(err, "could not write request to server")
|
||||
}
|
||||
|
||||
msg, _, err := wsutil.ReadServerData(conn)
|
||||
if err != nil {
|
||||
r.options.Output.Request(r.options.TemplateID, input, "websocket", err)
|
||||
r.options.Progress.IncrementFailedRequestsBy(1)
|
||||
return errors.Wrap(err, "could not write request to server")
|
||||
}
|
||||
|
||||
responseBuilder.Write(msg)
|
||||
if req.Name != "" {
|
||||
bufferStr := string(msg)
|
||||
if req.Name != "" {
|
||||
inputEvents[req.Name] = bufferStr
|
||||
}
|
||||
|
||||
// Run any internal extractors for the request here and add found values to map.
|
||||
if r.CompiledOperators != nil {
|
||||
values := r.CompiledOperators.ExecuteInternalExtractors(map[string]interface{}{req.Name: bufferStr}, utils.ExtractFunc)
|
||||
for k, v := range values {
|
||||
dynamicValues[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
r.options.Progress.IncrementRequests()
|
||||
|
||||
if r.options.Options.Debug || r.options.Options.DebugRequests {
|
||||
requestOutput := reqBuilder.String()
|
||||
gologger.Info().Str("address", input).Msgf("[%s] Dumped Websocket request for %s", r.options.TemplateID, input)
|
||||
gologger.Print().Msgf("%s", requestOutput)
|
||||
}
|
||||
|
||||
r.options.Output.Request(r.options.TemplateID, input, "websocket", err)
|
||||
gologger.Verbose().Msgf("Sent Websocket request to %s", input)
|
||||
|
||||
if r.options.Options.Debug || r.options.Options.DebugResponse {
|
||||
responseOutput := responseBuilder.String()
|
||||
gologger.Debug().Msgf("[%s] Dumped Websocket response for %s", r.options.TemplateID, input)
|
||||
gologger.Print().Msgf("%s", responseOutput)
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
for k, v := range previous {
|
||||
data[k] = v
|
||||
}
|
||||
for k, v := range dynamicValues {
|
||||
data[k] = v
|
||||
}
|
||||
for k, v := range inputEvents {
|
||||
data[k] = v
|
||||
}
|
||||
data["request"] = reqBuilder.String()
|
||||
data["response"] = responseBuilder.String()
|
||||
data["host"] = input
|
||||
data["ip"] = r.dialer.GetDialedIP(hostname)
|
||||
|
||||
event := &output.InternalWrappedEvent{InternalEvent: data}
|
||||
if r.CompiledOperators != nil {
|
||||
var ok bool
|
||||
event.OperatorsResult, ok = r.CompiledOperators.Execute(data, utils.MatchFunc, utils.ExtractFunc)
|
||||
if ok && event.OperatorsResult != nil {
|
||||
event.Results = utils.MakeResultEvent(event, r.makeResultEventItem)
|
||||
}
|
||||
callback(event)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAddress returns the address of the host to make request to
|
||||
func getAddress(toTest string) (string, error) {
|
||||
if !strings.HasPrefix(toTest, "ws://") && !strings.HasPrefix(toTest, "wss://") {
|
||||
return "", errors.New("invalid websocket provided")
|
||||
}
|
||||
parsed, _ := url.Parse(toTest)
|
||||
if parsed != nil && parsed.Host != "" {
|
||||
return parsed.Host, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (r *Request) makeResultEventItem(wrapped *output.InternalWrappedEvent) *output.ResultEvent {
|
||||
data := &output.ResultEvent{
|
||||
TemplateID: types.ToString(r.options.TemplateID),
|
||||
TemplatePath: types.ToString(r.options.TemplatePath),
|
||||
Info: r.options.TemplateInfo,
|
||||
Type: "websocket",
|
||||
Host: types.ToString(wrapped.InternalEvent["host"]),
|
||||
Matched: types.ToString(wrapped.InternalEvent["host"]),
|
||||
Metadata: wrapped.OperatorsResult.PayloadValues,
|
||||
ExtractedResults: wrapped.OperatorsResult.OutputExtracts,
|
||||
Timestamp: time.Now(),
|
||||
IP: types.ToString(wrapped.InternalEvent["ip"]),
|
||||
Request: types.ToString(wrapped.InternalEvent["request"]),
|
||||
Response: types.ToString(wrapped.InternalEvent["responses"]),
|
||||
}
|
||||
return data
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
package wss
|
||||
@ -137,7 +137,8 @@ func (t *Template) Requests() int {
|
||||
len(t.RequestsNetwork) +
|
||||
len(t.RequestsHeadless) +
|
||||
len(t.Workflows) +
|
||||
len(t.RequestsSSL)
|
||||
len(t.RequestsSSL) +
|
||||
len(t.RequestsWebsocket)
|
||||
return sum
|
||||
}
|
||||
|
||||
@ -175,4 +176,10 @@ func makeRequestsForTemplate(template *Template, options protocols.ExecuterOptio
|
||||
}
|
||||
template.Executer = executer.NewExecuter(requests, &options)
|
||||
}
|
||||
if len(template.RequestsWebsocket) > 0 {
|
||||
for _, req := range template.RequestsWebsocket {
|
||||
requests = append(requests, req)
|
||||
}
|
||||
template.Executer = executer.NewExecuter(requests, &options)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/network"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/others/ssl"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/others/websocket"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/workflows"
|
||||
)
|
||||
|
||||
@ -60,6 +61,9 @@ type Template struct {
|
||||
// description: |
|
||||
// SSL contains the SSL request to make in the template.
|
||||
RequestsSSL []*ssl.Request `yaml:"ssl,omitempty" json:"ssl,omitempty" jsonschema:"title=ssl requests to make,description=SSL requests to make for the template"`
|
||||
// description: |
|
||||
// Websocket contains the Websocket request to make in the template.
|
||||
RequestsWebsocket []*websocket.Request `yaml:"websocket,omitempty" json:"websocket,omitempty" jsonschema:"title=websocket requests to make,description=Websocket requests to make for the template"`
|
||||
|
||||
// description: |
|
||||
// Workflows is a yaml based workflow declaration code.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user