Added websocket protocol support to nuclei

This commit is contained in:
Ice3man543 2021-09-27 18:02:49 +05:30
parent 0b11b80d8a
commit 396f17484e
6 changed files with 325 additions and 2 deletions

View File

@ -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

View File

@ -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=

View 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
}

View File

@ -1 +0,0 @@
package wss

View File

@ -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)
}
}

View File

@ -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.