Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
2 changes: 1 addition & 1 deletion README_ES.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
2 changes: 1 addition & 1 deletion README_ID.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
2 changes: 1 addition & 1 deletion README_JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
2 changes: 1 addition & 1 deletion README_KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
2 changes: 1 addition & 1 deletion README_PT-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
2 changes: 1 addition & 1 deletion README_TR.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)

TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
Expand Down
53 changes: 53 additions & 0 deletions internal/tests/integration/fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ package integration_test

import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"

"github.com/julienschmidt/httprouter"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
Expand Down Expand Up @@ -36,6 +40,55 @@ var fuzzingTestCases = []integrationCase{
{Path: "fuzz/fuzz-body-params-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 1}},
{Path: "fuzz/fuzz-body-xml-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 1}},
{Path: "fuzz/fuzz-body-generic-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 4}},
{Path: "fuzz/fuzz-raw-http-input.yaml", TestCase: &rawHttpInputFuzz{}},
}

// rawHttpInputFuzz seeds the base request from a raw HTTP request file, the
// shape users have on hand when there is no spec or captured traffic to feed in.
type rawHttpInputFuzz struct{}

func (h *rawHttpInputFuzz) Execute(filePath string) error {
router := httprouter.New()
router.POST("/api/login", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html")
var payload map[string]string
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// stand in for a backend that interpolates the value into a query
if strings.Contains(payload["user"], "'") {
_, _ = fmt.Fprint(w, "unrecognized token:")
return
}
_, _ = fmt.Fprintf(w, "welcome %s", payload["user"])
})
ts := httptest.NewServer(router)
defer ts.Close()

target, err := url.Parse(ts.URL)
if err != nil {
return err
}
rawRequest := fmt.Sprintf("POST /api/login HTTP/1.1\r\nHost: %s\r\nContent-Type: application/json\r\n\r\n{\"user\":\"admin\"}", target.Host)
targetPath := filepath.Join(os.TempDir(), "nuclei-raw-http-input.http")
if err := os.WriteFile(targetPath, []byte(rawRequest), 0644); err != nil {
return err
}
defer func() {
_ = os.Remove(targetPath)
}()

results, err := testutils.RunNucleiWithArgsAndGetResults(debug, "-t", filePath, "-l", targetPath, "-im", "http", "-dast")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}

type genericFuzzTestCase struct {
Expand Down
34 changes: 34 additions & 0 deletions internal/tests/integration/testdata/fuzz/fuzz-raw-http-input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
id: raw-http-input-body-fuzz

info:
name: fuzzing json body seeded from a raw http request file
author: pdteam
severity: info
description: |
This template fuzzes the json body of a base request seeded from a raw HTTP
request file (-im http), covering the shortest path from a single request to
a DAST scan.

http:
- pre-condition:
- type: dsl
dsl:
- method == "POST"
- contains(content_type, "application/json")
condition: and

payloads:
injection:
- "'"

fuzzing:
- part: body
type: postfix
mode: single
fuzz:
- '{{injection}}'

matchers:
- type: word
words:
- "unrecognized token:"
37 changes: 37 additions & 0 deletions pkg/input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,40 @@ func NewInputProvider(opts InputOptions) (InputProvider, error)

This function returns a InputProvider based by appropriately selecting input provider based on the input format (i.e. either list or http) and returns the provider that can handle that input format.


## Formats

Fuzzing and DAST take the request shape (method, body, params) from the input, not from CLI flags, so a bare URL is always fuzzed as a `GET`. To fuzz anything else, feed a request-shaped input with `-im`:

| `-im` | Input |
| --- | --- |
| `list` | urls, domains, ips, cidrs (default) |
| `http` | raw HTTP requests |
| `burp` | Burp Suite xml export |
| `jsonl` | proxify jsonl output |
| `yaml` | proxify yaml multidoc output |
| `openapi` | OpenAPI 3 spec |
| `swagger` | Swagger 2 spec |

### Raw HTTP requests (`-im http`)

The shortest path from a single endpoint to a DAST scan, for targets with no spec or captured traffic. Save the request as it appears on the wire, which is what "copy as raw request" in Burp and browser devtools produce:

```
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/json

{"user":"admin","pass":"secret"}
```

```console
nuclei -l login.http -im http -dast -t fuzzing-templates/
```

Notes:

- Separate multiple requests with a line starting with `###`, the `.http` file convention.
- The scheme is not part of the request, so it is inferred from the authority. Use an absolute request target (`POST https://example.com/api/login HTTP/1.1`) to state it explicitly.
- The target comes from the `Host` header or from an absolute request target; a request carrying neither is skipped.

99 changes: 99 additions & 0 deletions pkg/input/formats/raw/raw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Package raw parses input files holding raw HTTP requests, the shape produced
// by "copy as raw request" in Burp and browser devtools and stored in .http
// files. It is the shortest path from a single request to a DAST scan, for
// targets that have no OpenAPI spec or captured traffic to feed in.
package raw

import (
"io"
"strings"

"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
)

// requestSeparator delimits requests when a file carries more than one,
// following the .http file convention
const requestSeparator = "###"

// RawFormat is a parser for files containing one or more raw HTTP requests
type RawFormat struct {
opts formats.InputFormatOptions
}

// New creates a new raw HTTP request parser
func New() *RawFormat {
return &RawFormat{}
}

var _ formats.Format = &RawFormat{}

// Name returns the name of the format
func (r *RawFormat) Name() string {
return "http"
}

func (r *RawFormat) SetOptions(options formats.InputFormatOptions) {
r.opts = options
}

// Parse parses the input and calls the provided callback
// function for each RawRequest it discovers.
func (r *RawFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCallback, filePath string) error {
data, err := io.ReadAll(input)
if err != nil {
return errors.Wrap(err, "could not read raw request file")
}
for _, request := range splitRequests(string(data)) {
reqResp, err := types.ParseRawRequest(terminateHeaders(request))
if err != nil {
gologger.Warning().Msgf("http: could not parse raw request in %s: %s\n", filePath, err)
continue
}
if reqResp.URL.Host == "" {
gologger.Warning().Msgf("http: skipped raw request in %s: no target, add a Host header or an absolute request target\n", filePath)
continue
}
if reqResp.URL.Scheme == "" {
// nothing in the request states the scheme, so settle it the same
// way nuclei settles it for scheme-less list inputs
reqResp.URL.Scheme = utils.DetermineSchemeOrder(reqResp.URL.Host)[0]
}
resultsCb(reqResp)
}
return nil
}

// splitRequests splits the file contents into individual raw requests
func splitRequests(data string) []string {
var requests []string
var current []string
flush := func() {
if request := strings.Join(current, "\n"); strings.TrimSpace(request) != "" {
requests = append(requests, strings.TrimLeft(request, "\r\n"))
}
current = nil
}
for _, line := range strings.Split(data, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), requestSeparator) {
flush()
continue
}
current = append(current, line)
}
flush()
return requests
}

// terminateHeaders appends the blank line that separates headers from the body.
// Requests copied out of a browser or Burp routinely end at the last header,
// and the parser needs that terminator to know where the headers stop.
func terminateHeaders(request string) string {
if strings.Contains(request, "\n\r\n") || strings.Contains(request, "\n\n") {
return request
}
return request + "\r\n\r\n"
}
Loading
Loading