mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 21:35:26 +00:00
* feat: added initial live DAST server implementation * feat: more logging + misc additions * feat: auth file support enhancements for more complex scenarios + misc * feat: added io.Reader support to input providers for http * feat: added stats db to fuzzing + use sdk for dast server + misc * feat: more additions and enhancements * misc changes to live server * misc * use utils pprof server * feat: added simpler stats tracking system * feat: fixed analyzer timeout issue + missing case fix * misc changes fix * feat: changed the logics a bit + misc changes and additions * feat: re-added slope checks + misc * feat: added baseline measurements for time based checks * chore(server): fix typos Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix(templates): potential DOM XSS Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix(authx): potential NIL deref Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: misc review changes * removed debug logging * feat: remove existing cookies only * feat: lint fixes * misc * misc text update * request endpoint update * feat: added tracking for status code, waf-detection & grouped errors (#6028) * feat: added tracking for status code, waf-detection & grouped errors * lint error fixes * feat: review changes + moving to package + misc --------- Co-authored-by: sandeep <8293321+ehsandeep@users.noreply.github.com> * fix var dump (#5921) * fix var dump * fix dump test * Added filename length restriction for debug mode (-srd flag) (#5931) Co-authored-by: Andrey Matveenko <an.matveenko@vkteam.ru> * more updates * Update pkg/output/stats/waf/waf.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: sandeep <8293321+ehsandeep@users.noreply.github.com> Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Dogan Can Bakir <65292895+dogancanbakir@users.noreply.github.com> Co-authored-by: 9flowers <51699499+Lercas@users.noreply.github.com> Co-authored-by: Andrey Matveenko <an.matveenko@vkteam.ru> Co-authored-by: Sandeep Singh <sandeep@projectdiscovery.io>
297 lines
8.8 KiB
Go
297 lines
8.8 KiB
Go
package server
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/alitto/pond"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"github.com/projectdiscovery/gologger"
|
|
"github.com/projectdiscovery/nuclei/v3/internal/server/scope"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
|
"github.com/projectdiscovery/utils/env"
|
|
)
|
|
|
|
// DASTServer is a server that performs execution of fuzzing templates
|
|
// on user input passed to the API.
|
|
type DASTServer struct {
|
|
echo *echo.Echo
|
|
options *Options
|
|
tasksPool *pond.WorkerPool
|
|
deduplicator *requestDeduplicator
|
|
scopeManager *scope.Manager
|
|
startTime time.Time
|
|
|
|
// metrics
|
|
endpointsInQueue atomic.Int64
|
|
endpointsBeingTested atomic.Int64
|
|
|
|
nucleiExecutor *nucleiExecutor
|
|
}
|
|
|
|
// Options contains the configuration options for the server.
|
|
type Options struct {
|
|
// Address is the address to bind the server to
|
|
Address string
|
|
// Token is the token to use for authentication (optional)
|
|
Token string
|
|
// Templates is the list of templates to use for fuzzing
|
|
Templates []string
|
|
// Verbose is a flag that controls verbose output
|
|
Verbose bool
|
|
|
|
// Scope fields for fuzzer
|
|
InScope []string
|
|
OutScope []string
|
|
|
|
OutputWriter output.Writer
|
|
|
|
NucleiExecutorOptions *NucleiExecutorOptions
|
|
}
|
|
|
|
// New creates a new instance of the DAST server.
|
|
func New(options *Options) (*DASTServer, error) {
|
|
// If the user has specified no templates, use the default ones
|
|
// for DAST only.
|
|
if len(options.Templates) == 0 {
|
|
options.Templates = []string{"dast/"}
|
|
}
|
|
// Disable bulk mode and single threaded execution
|
|
// by auto adjusting in case of default values
|
|
if options.NucleiExecutorOptions.Options.BulkSize == 25 && options.NucleiExecutorOptions.Options.TemplateThreads == 25 {
|
|
options.NucleiExecutorOptions.Options.BulkSize = 1
|
|
options.NucleiExecutorOptions.Options.TemplateThreads = 1
|
|
}
|
|
maxWorkers := env.GetEnvOrDefault[int]("FUZZ_MAX_WORKERS", 1)
|
|
bufferSize := env.GetEnvOrDefault[int]("FUZZ_BUFFER_SIZE", 10000)
|
|
|
|
server := &DASTServer{
|
|
options: options,
|
|
tasksPool: pond.New(maxWorkers, bufferSize),
|
|
deduplicator: newRequestDeduplicator(),
|
|
startTime: time.Now(),
|
|
}
|
|
server.setupHandlers(false)
|
|
|
|
executor, err := newNucleiExecutor(options.NucleiExecutorOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server.nucleiExecutor = executor
|
|
|
|
scopeManager, err := scope.NewManager(
|
|
options.InScope,
|
|
options.OutScope,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server.scopeManager = scopeManager
|
|
|
|
var builder strings.Builder
|
|
gologger.Debug().Msgf("Using %d parallel tasks with %d buffer", maxWorkers, bufferSize)
|
|
if options.Token != "" {
|
|
builder.WriteString(" (with token)")
|
|
}
|
|
gologger.Info().Msgf("DAST Server API: %s", server.buildURL("/fuzz"))
|
|
gologger.Info().Msgf("DAST Server Stats URL: %s", server.buildURL("/stats"))
|
|
|
|
return server, nil
|
|
}
|
|
|
|
func NewStatsServer(fuzzStatsDB *stats.Tracker) (*DASTServer, error) {
|
|
server := &DASTServer{
|
|
nucleiExecutor: &nucleiExecutor{
|
|
executorOpts: protocols.ExecutorOptions{
|
|
FuzzStatsDB: fuzzStatsDB,
|
|
},
|
|
},
|
|
}
|
|
server.setupHandlers(true)
|
|
gologger.Info().Msgf("Stats UI URL: %s", server.buildURL("/stats"))
|
|
|
|
return server, nil
|
|
}
|
|
|
|
func (s *DASTServer) Close() {
|
|
s.nucleiExecutor.Close()
|
|
s.echo.Close()
|
|
s.tasksPool.StopAndWaitFor(1 * time.Minute)
|
|
}
|
|
|
|
func (s *DASTServer) buildURL(endpoint string) string {
|
|
values := make(url.Values)
|
|
if s.options.Token != "" {
|
|
values.Set("token", s.options.Token)
|
|
}
|
|
|
|
// Use url.URL struct to safely construct the URL
|
|
u := &url.URL{
|
|
Scheme: "http",
|
|
Host: s.options.Address,
|
|
Path: endpoint,
|
|
RawQuery: values.Encode(),
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
func (s *DASTServer) setupHandlers(onlyStats bool) {
|
|
e := echo.New()
|
|
e.Use(middleware.Recover())
|
|
if s.options.Verbose {
|
|
cfg := middleware.DefaultLoggerConfig
|
|
cfg.Skipper = func(c echo.Context) bool {
|
|
// Skip /stats and /stats.json
|
|
return c.Request().URL.Path == "/stats" || c.Request().URL.Path == "/stats.json"
|
|
}
|
|
e.Use(middleware.LoggerWithConfig(cfg))
|
|
}
|
|
e.Use(middleware.CORS())
|
|
|
|
if s.options.Token != "" {
|
|
e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
|
|
KeyLookup: "query:token",
|
|
Validator: func(key string, c echo.Context) (bool, error) {
|
|
return key == s.options.Token, nil
|
|
},
|
|
}))
|
|
}
|
|
|
|
e.HideBanner = true
|
|
// POST /fuzz - Queue a request for fuzzing
|
|
if !onlyStats {
|
|
e.POST("/fuzz", s.handleRequest)
|
|
}
|
|
e.GET("/stats", s.handleStats)
|
|
e.GET("/stats.json", s.handleStatsJSON)
|
|
|
|
s.echo = e
|
|
}
|
|
|
|
func (s *DASTServer) Start() error {
|
|
if err := s.echo.Start(s.options.Address); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PostReuestsHandlerRequest is the request body for the /fuzz POST handler.
|
|
type PostRequestsHandlerRequest struct {
|
|
RawHTTP string `json:"raw_http"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func (s *DASTServer) handleRequest(c echo.Context) error {
|
|
var req PostRequestsHandlerRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
fmt.Printf("Error binding request: %s\n", err)
|
|
return err
|
|
}
|
|
|
|
// Validate the request
|
|
if req.RawHTTP == "" || req.URL == "" {
|
|
fmt.Printf("Missing required fields\n")
|
|
return c.JSON(400, map[string]string{"error": "missing required fields"})
|
|
}
|
|
|
|
s.endpointsInQueue.Add(1)
|
|
s.tasksPool.Submit(func() {
|
|
s.consumeTaskRequest(req)
|
|
})
|
|
return c.NoContent(200)
|
|
}
|
|
|
|
type StatsResponse struct {
|
|
DASTServerInfo DASTServerInfo `json:"dast_server_info"`
|
|
DASTScanStatistics DASTScanStatistics `json:"dast_scan_statistics"`
|
|
DASTScanStatusStatistics map[string]int64 `json:"dast_scan_status_statistics"`
|
|
DASTScanSeverityBreakdown map[string]int64 `json:"dast_scan_severity_breakdown"`
|
|
DASTScanErrorStatistics map[string]int64 `json:"dast_scan_error_statistics"`
|
|
DASTScanStartTime time.Time `json:"dast_scan_start_time"`
|
|
}
|
|
|
|
type DASTServerInfo struct {
|
|
NucleiVersion string `json:"nuclei_version"`
|
|
NucleiTemplateVersion string `json:"nuclei_template_version"`
|
|
NucleiDastServerAPI string `json:"nuclei_dast_server_api"`
|
|
ServerAuthEnabled bool `json:"sever_auth_enabled"`
|
|
}
|
|
|
|
type DASTScanStatistics struct {
|
|
EndpointsInQueue int64 `json:"endpoints_in_queue"`
|
|
EndpointsBeingTested int64 `json:"endpoints_being_tested"`
|
|
TotalTemplatesLoaded int64 `json:"total_dast_templates_loaded"`
|
|
TotalTemplatesTested int64 `json:"total_dast_templates_tested"`
|
|
TotalMatchedResults int64 `json:"total_matched_results"`
|
|
TotalComponentsTested int64 `json:"total_components_tested"`
|
|
TotalEndpointsTested int64 `json:"total_endpoints_tested"`
|
|
TotalFuzzedRequests int64 `json:"total_fuzzed_requests"`
|
|
TotalErroredRequests int64 `json:"total_errored_requests"`
|
|
}
|
|
|
|
func (s *DASTServer) getStats() (StatsResponse, error) {
|
|
cfg := config.DefaultConfig
|
|
|
|
resp := StatsResponse{
|
|
DASTServerInfo: DASTServerInfo{
|
|
NucleiVersion: config.Version,
|
|
NucleiTemplateVersion: cfg.TemplateVersion,
|
|
NucleiDastServerAPI: s.buildURL("/fuzz"),
|
|
ServerAuthEnabled: s.options.Token != "",
|
|
},
|
|
DASTScanStartTime: s.startTime,
|
|
DASTScanStatistics: DASTScanStatistics{
|
|
EndpointsInQueue: s.endpointsInQueue.Load(),
|
|
EndpointsBeingTested: s.endpointsBeingTested.Load(),
|
|
TotalTemplatesLoaded: int64(len(s.nucleiExecutor.store.Templates())),
|
|
},
|
|
}
|
|
if s.nucleiExecutor.executorOpts.FuzzStatsDB != nil {
|
|
fuzzStats := s.nucleiExecutor.executorOpts.FuzzStatsDB.GetStats()
|
|
resp.DASTScanSeverityBreakdown = fuzzStats.SeverityCounts
|
|
resp.DASTScanStatusStatistics = fuzzStats.StatusCodes
|
|
resp.DASTScanStatistics.TotalMatchedResults = fuzzStats.TotalMatchedResults
|
|
resp.DASTScanStatistics.TotalComponentsTested = fuzzStats.TotalComponentsTested
|
|
resp.DASTScanStatistics.TotalEndpointsTested = fuzzStats.TotalEndpointsTested
|
|
resp.DASTScanStatistics.TotalFuzzedRequests = fuzzStats.TotalFuzzedRequests
|
|
resp.DASTScanStatistics.TotalTemplatesTested = fuzzStats.TotalTemplatesTested
|
|
resp.DASTScanStatistics.TotalErroredRequests = fuzzStats.TotalErroredRequests
|
|
resp.DASTScanErrorStatistics = fuzzStats.ErrorGroupedStats
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
//go:embed templates/index.html
|
|
var indexTemplate string
|
|
|
|
func (s *DASTServer) handleStats(c echo.Context) error {
|
|
stats, err := s.getStats()
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
tmpl, err := template.New("index").Parse(indexTemplate)
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
|
}
|
|
return tmpl.Execute(c.Response().Writer, stats)
|
|
}
|
|
|
|
func (s *DASTServer) handleStatsJSON(c echo.Context) error {
|
|
resp, err := s.getStats()
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
|
}
|
|
return c.JSONPretty(200, resp, " ")
|
|
}
|