Skip to content

Commit

Permalink
feat: added initial live DAST server implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ice3man543 committed Oct 26, 2024
1 parent 49811d2 commit 0db2332
Show file tree
Hide file tree
Showing 12 changed files with 589 additions and 2 deletions.
10 changes: 10 additions & 0 deletions cmd/nuclei/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ func main() {
go func() {
for range c {
gologger.Info().Msgf("CTRL+C pressed: Exiting\n")
if options.DASTServer {
nucleiRunner.Close()
os.Exit(1)
}

gologger.Info().Msgf("Attempting graceful shutdown...")
if options.EnableCloudUpload {
gologger.Info().Msgf("Uploading scan results to cloud...")
Expand Down Expand Up @@ -354,9 +359,14 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVarP(&options.FuzzingMode, "fuzzing-mode", "fm", "", "overrides fuzzing mode set in template (multiple, single)"),
flagSet.BoolVar(&fuzzFlag, "fuzz", false, "enable loading fuzzing templates (Deprecated: use -dast instead)"),
flagSet.BoolVar(&options.DAST, "dast", false, "enable / run dast (fuzz) nuclei templates"),
flagSet.BoolVarP(&options.DASTServer, "dast-server", "dts", false, "enable dast server mode (live fuzzing)"),
flagSet.StringVarP(&options.DASTServerToken, "dast-server-token", "dtst", "", "dast server token (optional)"),
flagSet.StringVarP(&options.DASTServerAddress, "dast-server-address", "dtsa", "localhost:9055", "dast server address"),
flagSet.BoolVarP(&options.DisplayFuzzPoints, "display-fuzz-points", "dfp", false, "display fuzz points in the output for debugging"),
flagSet.IntVar(&options.FuzzParamFrequency, "fuzz-param-frequency", 10, "frequency of uninteresting parameters for fuzzing before skipping"),
flagSet.StringVarP(&options.FuzzAggressionLevel, "fuzz-aggression", "fa", "low", "fuzzing aggression level controls payload count for fuzz (low, medium, high)"),
flagSet.StringSliceVarP(&options.Scope, "fuzz-scope", "cs", nil, "in scope url regex to be followed by fuzzer", goflags.FileCommaSeparatedStringSliceOptions),
flagSet.StringSliceVarP(&options.OutOfScope, "fuzz-out-scope", "cos", nil, "out of scope url regex to be excluded by fuzzer", goflags.FileCommaSeparatedStringSliceOptions),
)

flagSet.CreateGroup("uncover", "Uncover",
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ require (
github.com/h2non/filetype v1.1.3
github.com/invopop/yaml v0.3.1
github.com/kitabisa/go-ci v1.0.3
github.com/labstack/echo/v4 v4.10.2
github.com/labstack/echo/v4 v4.12.0
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.22
Expand Down Expand Up @@ -102,6 +102,7 @@ require (
github.com/redis/go-redis/v9 v9.1.0
github.com/seh-msft/burpxml v1.0.1
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
github.com/sourcegraph/conc v0.3.0
github.com/stretchr/testify v1.9.0
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
github.com/yassinebenaid/godump v0.10.0
Expand Down Expand Up @@ -347,7 +348,7 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa h1:KQKuQDgA3DZX6C396lt3WDYB9Um1gLITLbvficVbqXk=
Expand Down Expand Up @@ -997,6 +1001,8 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
Expand Down
20 changes: 20 additions & 0 deletions internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/projectdiscovery/nuclei/v3/internal/pdcp"
"github.com/projectdiscovery/nuclei/v3/internal/server"
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/frequency"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
Expand Down Expand Up @@ -436,6 +437,25 @@ func (r *Runner) setupPDCPUpload(writer output.Writer) output.Writer {
// RunEnumeration sets up the input layer for giving input nuclei.
// binary and runs the actual enumeration
func (r *Runner) RunEnumeration() error {
// If the user has asked for DAST server mode, run the live
// DAST fuzzing server.
if r.options.DASTServer {
dastServer, err := server.New(&server.Options{
Address: r.options.DASTServerAddress,
Concurrency: r.options.BulkSize,
Templates: r.options.Templates,
OutputWriter: r.output,
Verbose: r.options.Verbose,
Token: r.options.DASTServerToken,
InScope: r.options.Scope,
OutScope: r.options.OutOfScope,
})
if err != nil {
return err
}
return dastServer.Start()
}

// If user asked for new templates to be executed, collect the list from the templates' directory.
if r.options.NewTemplates {
if arr := config.DefaultConfig.GetNewAdditions(); len(arr) > 0 {
Expand Down
122 changes: 122 additions & 0 deletions internal/server/dedupe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package server

import (
"crypto/sha256"
"encoding/hex"
"net/url"
"sort"
"strings"
"sync"

"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
mapsutil "github.com/projectdiscovery/utils/maps"
)

var dynamicHeaders = map[string]bool{
"date": true,
"if-modified-since": true,
"if-unmodified-since": true,
"cache-control": true,
"if-none-match": true,
"if-match": true,
"authorization": true,
"cookie": true,
"x-csrf-token": true,
"content-length": true,
"content-md5": true,
"host": true,
"x-request-id": true,
"x-correlation-id": true,
"user-agent": true,
"referer": true,
}

type requestDeduplicator struct {
hashes map[string]struct{}
lock *sync.RWMutex
}

func newRequestDeduplicator() *requestDeduplicator {
return &requestDeduplicator{
hashes: make(map[string]struct{}),
lock: &sync.RWMutex{},
}
}

func (r *requestDeduplicator) isDuplicate(req *types.RequestResponse) bool {
hash, err := hashRequest(req)
if err != nil {
return false
}

r.lock.RLock()
_, ok := r.hashes[hash]
r.lock.RUnlock()
if ok {
return true
}

r.lock.Lock()
r.hashes[hash] = struct{}{}
r.lock.Unlock()
return false
}

func hashRequest(req *types.RequestResponse) (string, error) {
normalizedURL, err := normalizeURL(req.URL.URL)
if err != nil {
return "", err
}

var hashContent strings.Builder
hashContent.WriteString(req.Request.Method)
hashContent.WriteString(normalizedURL)

headers := sortedNonDynamicHeaders(req.Request.Headers)
for _, header := range headers {
hashContent.WriteString(header.Key)
hashContent.WriteString(header.Value)
}

if len(req.Request.Body) > 0 {
hashContent.Write([]byte(req.Request.Body))
}

// Calculate the SHA256 hash
hash := sha256.Sum256([]byte(hashContent.String()))
return hex.EncodeToString(hash[:]), nil
}

func normalizeURL(u *url.URL) (string, error) {
query := u.Query()
sortedQuery := make(url.Values)
for k, v := range query {
sort.Strings(v)
sortedQuery[k] = v
}
u.RawQuery = sortedQuery.Encode()

if u.Path == "" {
u.Path = "/"
}
return u.String(), nil
}

type header struct {
Key string
Value string
}

func sortedNonDynamicHeaders(headers mapsutil.OrderedMap[string, string]) []header {
var result []header
headers.Iterate(func(k, v string) bool {
if !dynamicHeaders[strings.ToLower(k)] {
result = append(result, header{Key: k, Value: v})
}
return true
})
sort.Slice(result, func(i, j int) bool {
return result[i].Key < result[j].Key
})
return result
}
90 changes: 90 additions & 0 deletions internal/server/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package server

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"

"github.com/projectdiscovery/nuclei/v3/pkg/output"
"gopkg.in/yaml.v2"
)

// proxifyRequest is a request for proxify
type proxifyRequest struct {
URL string `json:"url"`
Request struct {
Header map[string]string `json:"header"`
Body string `json:"body"`
Raw string `json:"raw"`
} `json:"request"`
}

func runNucleiWithFuzzingInput(target PostReuestsHandlerRequest, templates []string) ([]output.ResultEvent, error) {
cmd := exec.Command("nuclei")

tempFile, err := os.CreateTemp("", "nuclei-fuzz-*.yaml")
if err != nil {
return nil, fmt.Errorf("error creating temp file: %s", err)
}
defer os.Remove(tempFile.Name())

payload := proxifyRequest{
URL: target.URL,
Request: struct {
Header map[string]string `json:"header"`
Body string `json:"body"`
Raw string `json:"raw"`
}{
Raw: target.RawHTTP,
},
}

marshalledYaml, err := yaml.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("error marshalling yaml: %s", err)
}

if _, err := tempFile.Write(marshalledYaml); err != nil {
return nil, fmt.Errorf("error writing to temp file: %s", err)
}

argsArray := []string{
"-duc",
"-dast",
"-silent",
"-no-color",
"-jsonl",
}
for _, template := range templates {
argsArray = append(argsArray, "-t", template)
}
argsArray = append(argsArray, "-l", tempFile.Name())
argsArray = append(argsArray, "-im=yaml")
cmd.Args = append(cmd.Args, argsArray...)

data, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("error running nuclei: %w", err)
}

var nucleiResult []output.ResultEvent
decoder := json.NewDecoder(bytes.NewReader(data))
for {
var result output.ResultEvent
if err := decoder.Decode(&result); err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("error decoding nuclei output: %w", err)
}
// Filter results with a valid template-id
if result.TemplateID != "" {
nucleiResult = append(nucleiResult, result)
}
}

return nucleiResult, nil
}
63 changes: 63 additions & 0 deletions internal/server/requests_worker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package server

import (
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
)

func (s *DASTServer) setupWorkers() {
go s.tasksConsumer()
}

func (s *DASTServer) tasksConsumer() {
for req := range s.fuzzRequests {
parsedReq, err := parseRawRequest(req)
if err != nil {
gologger.Warning().Msgf("Could not parse raw request: %s\n", err)
continue
}

inScope, err := s.scopeManager.Validate(parsedReq.URL.URL, "")
if err != nil {
gologger.Warning().Msgf("Could not validate scope: %s\n", err)
continue
}
if !inScope {
gologger.Warning().Msgf("Request is out of scope: %s %s\n", parsedReq.Request.Method, parsedReq.URL.String())
continue
}

if s.deduplicator.isDuplicate(parsedReq) {
gologger.Warning().Msgf("Duplicate request detected: %s %s\n", parsedReq.Request.Method, parsedReq.URL.String())
continue
}

gologger.Verbose().Msgf("Fuzzing request: %s %s\n", parsedReq.Request.Method, parsedReq.URL.String())
s.tasksPool.Go(func() {
s.fuzzRequest(req)
})
}
}

func (s *DASTServer) fuzzRequest(req PostReuestsHandlerRequest) {
results, err := runNucleiWithFuzzingInput(req, s.options.Templates)
if err != nil {
gologger.Warning().Msgf("Could not run nuclei: %s\n", err)
return
}

for _, result := range results {
if err := s.options.OutputWriter.Write(&result); err != nil {
gologger.Error().Msgf("Could not write result: %s\n", err)
}
}
}

func parseRawRequest(req PostReuestsHandlerRequest) (*types.RequestResponse, error) {
parsedReq, err := types.ParseRawRequestWithURL(req.RawHTTP, req.URL)
if err != nil {
return nil, errors.Wrap(err, "could not parse raw HTTP")
}
return parsedReq, nil
}
Loading

0 comments on commit 0db2332

Please sign in to comment.