diff --git a/integration_tests/http/get-headers.yaml b/integration_tests/http/get-headers.yaml new file mode 100644 index 00000000..bae36705 --- /dev/null +++ b/integration_tests/http/get-headers.yaml @@ -0,0 +1,17 @@ +id: basic-get-headers + +info: + name: Basic GET Headers Request + author: pdteam + severity: info + +requests: + - method: GET + path: + - "{{BaseURL}}" + headers: + test: nuclei + matchers: + - type: word + words: + - "This is test headers matcher text" \ No newline at end of file diff --git a/integration_tests/http/get-query-string.yaml b/integration_tests/http/get-query-string.yaml new file mode 100644 index 00000000..129c080d --- /dev/null +++ b/integration_tests/http/get-query-string.yaml @@ -0,0 +1,15 @@ +id: basic-get-querystring + +info: + name: Basic GET QueryString Request + author: pdteam + severity: info + +requests: + - method: GET + path: + - "{{BaseURL}}?test=nuclei" + matchers: + - type: word + words: + - "This is test querystring matcher text" \ No newline at end of file diff --git a/integration_tests/http/get-redirects.yaml b/integration_tests/http/get-redirects.yaml new file mode 100644 index 00000000..e6bf0c44 --- /dev/null +++ b/integration_tests/http/get-redirects.yaml @@ -0,0 +1,17 @@ +id: basic-get-redirects + +info: + name: Basic GET Redirects Request + author: pdteam + severity: info + +requests: + - method: GET + path: + - "{{BaseURL}}" + redirects: true + max-redirects: 3 + matchers: + - type: word + words: + - "This is test redirects matcher text" \ No newline at end of file diff --git a/integration_tests/http/get.yaml b/integration_tests/http/get.yaml new file mode 100644 index 00000000..c7e07e8c --- /dev/null +++ b/integration_tests/http/get.yaml @@ -0,0 +1,15 @@ +id: basic-get + +info: + name: Basic GET Request + author: pdteam + severity: info + +requests: + - method: GET + path: + - "{{BaseURL}}" + matchers: + - type: word + words: + - "This is test matcher text" \ No newline at end of file diff --git a/integration_tests/http/post-body.yaml b/integration_tests/http/post-body.yaml new file mode 100644 index 00000000..7eb36ca4 --- /dev/null +++ b/integration_tests/http/post-body.yaml @@ -0,0 +1,19 @@ +id: basic-post-body + +info: + name: Basic POST Body Request + author: pdteam + severity: info + +requests: + - method: POST + path: + - "{{BaseURL}}" + headers: + Content-Type: application/x-www-form-urlencoded + Content-Length: 1 # as long as there is a value, nuclei will auto-recalculate it. + body: username=test&password=nuclei + matchers: + - type: word + words: + - "This is test post-body matcher text" \ No newline at end of file diff --git a/integration_tests/http/post-json-body.yaml b/integration_tests/http/post-json-body.yaml new file mode 100644 index 00000000..b4c6c317 --- /dev/null +++ b/integration_tests/http/post-json-body.yaml @@ -0,0 +1,19 @@ +id: basic-post-json-body + +info: + name: Basic POST JSON Body Request + author: pdteam + severity: info + +requests: + - method: POST + path: + - "{{BaseURL}}" + headers: + Content-Type: application/json + Content-Length: 1 + body: '{"username":"test","password":"nuclei"}' + matchers: + - type: word + words: + - "This is test post-json-body matcher text" \ No newline at end of file diff --git a/integration_tests/http/post-multipart-body.yaml b/integration_tests/http/post-multipart-body.yaml new file mode 100644 index 00000000..73bd5131 --- /dev/null +++ b/integration_tests/http/post-multipart-body.yaml @@ -0,0 +1,29 @@ +id: basic-post-multipart-body + +info: + name: Basic POST Multipart Request + author: pdteam + severity: info + +requests: + - method: POST + path: + - "{{BaseURL}}" + headers: + Content-Type: multipart/form-data; boundary=d64a5c6be2120f494d87b096fff6efe6d3248474d4de2debb1d387b3d8e8 + Content-Length: 1 + body: | + --d64a5c6be2120f494d87b096fff6efe6d3248474d4de2debb1d387b3d8e8 + Content-Disposition: form-data; name="username"; filename="username" + Content-Type: application/octet-stream + + test + --d64a5c6be2120f494d87b096fff6efe6d3248474d4de2debb1d387b3d8e8 + Content-Disposition: form-data; name="password" + + nuclei + --d64a5c6be2120f494d87b096fff6efe6d3248474d4de2debb1d387b3d8e8-- + matchers: + - type: word + words: + - "This is test post-multipart matcher text" \ No newline at end of file diff --git a/integration_tests/http/raw-cookie-reuse.yaml b/integration_tests/http/raw-cookie-reuse.yaml new file mode 100644 index 00000000..d680ad29 --- /dev/null +++ b/integration_tests/http/raw-cookie-reuse.yaml @@ -0,0 +1,34 @@ +id: cookiereuse-raw-example +info: + name: Test CookieReuse RAW Template + author: pdteam + severity: info + +requests: + - raw: + - | + POST / HTTP/1.1 + Host: {{Hostname}} + Origin: {{BaseURL}} + Connection: close + Content-Type: application/x-www-form-urlencoded + Content-Length: 1 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 + Accept-Language: en-US,en;q=0.9 + + testing=parameter + - | + GET / HTTP/1.1 + Host: {{Hostname}} + Origin: {{BaseURL}} + Connection: close + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 + Accept-Language: en-US,en;q=0.9 + + cookie-reuse: true + matchers: + - type: word + words: + - "Test is test-cookie-reuse matcher text" \ No newline at end of file diff --git a/integration_tests/http/raw-dynamic-extractor.yaml b/integration_tests/http/raw-dynamic-extractor.yaml new file mode 100644 index 00000000..d576be45 --- /dev/null +++ b/integration_tests/http/raw-dynamic-extractor.yaml @@ -0,0 +1,43 @@ +id: dynamic-extractor-raw-example + +info: + name: Test Dynamic Extractor RAW Template + author: pdteam + severity: info + +requests: + - raw: + - | + POST / HTTP/1.1 + Host: {{Hostname}} + Origin: {{BaseURL}} + Connection: close + Content-Type: application/x-www-form-urlencoded + Content-Length: 1 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 + Accept-Language: en-US,en;q=0.9 + + testing=parameter + - | + GET /?username={{randkey}} HTTP/1.1 + Host: {{Hostname}} + Origin: {{BaseURL}} + Connection: close + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 + Accept-Language: en-US,en;q=0.9 + extractors: + - type: regex + name: randkey + part: body + group: 1 + internal: true + regex: + - "Token: '([A-Za-z0-9]+)'" + + cookie-reuse: true + matchers: + - type: word + words: + - "Test is test-dynamic-extractor-raw matcher text" \ No newline at end of file diff --git a/integration_tests/http/raw-get-query.yaml b/integration_tests/http/raw-get-query.yaml new file mode 100644 index 00000000..71ff50c2 --- /dev/null +++ b/integration_tests/http/raw-get-query.yaml @@ -0,0 +1,18 @@ +id: basic-raw-query-example + +info: + name: Test RAW GET Query Template + author: pdteam + severity: info + +requests: + - raw: + - | + GET ?test=nuclei HTTP/1.1 + Host: {{Hostname}} + Origin: {{BaseURL}} + + matchers: + - type: word + words: + - "Test is test raw-get-query-matcher text" \ No newline at end of file diff --git a/integration_tests/http/raw-get.yaml b/integration_tests/http/raw-get.yaml new file mode 100644 index 00000000..572a6ef3 --- /dev/null +++ b/integration_tests/http/raw-get.yaml @@ -0,0 +1,18 @@ +id: basic-raw-http-example + +info: + name: Test RAW GET Template + author: pdteam + severity: info + +requests: + - raw: + - | + GET / HTTP/1.1 + Host: {{Hostname}} + Origin: {{BaseURL}} + + matchers: + - type: word + words: + - "Test is test raw-get-matcher text" \ No newline at end of file diff --git a/integration_tests/http/raw-payload.yaml b/integration_tests/http/raw-payload.yaml new file mode 100644 index 00000000..77b20b89 --- /dev/null +++ b/integration_tests/http/raw-payload.yaml @@ -0,0 +1,29 @@ +id: payload-raw-example +info: + name: Test RAW With Payload Template + author: pdteam + severity: info + +requests: + - payloads: + username: + - test + password: + - nuclei + - guest + attack: clusterbomb + raw: + - | + POST / HTTP/1.1 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) + Host: {{Hostname}} + Content-Type: application/x-www-form-urlencoded + Content-Length: 1 + another_header: {{base64('§password§')}} + Accept: */* + + username=§username§&password={{password}} + matchers: + - type: word + words: + - "Test is raw-payload matcher text" \ No newline at end of file diff --git a/integration_tests/http/raw-post-body.yaml b/integration_tests/http/raw-post-body.yaml new file mode 100644 index 00000000..68bc8651 --- /dev/null +++ b/integration_tests/http/raw-post-body.yaml @@ -0,0 +1,21 @@ +id: basic-raw-http-body-example + +info: + name: Test RAW POST Template + author: pdteam + severity: info + +requests: + - raw: + - | + POST / HTTP/1.1 + Host: {{Hostname}} + Origin: {{BaseURL}} + Content-Type: application/x-www-form-urlencoded + Content-Length: 1 + + username=test&password=nuclei + matchers: + - type: word + words: + - "Test is test raw-post-body-matcher text" \ No newline at end of file diff --git a/integration_tests/http/raw-unsafe-request.yaml b/integration_tests/http/raw-unsafe-request.yaml new file mode 100644 index 00000000..8a573984 --- /dev/null +++ b/integration_tests/http/raw-unsafe-request.yaml @@ -0,0 +1,20 @@ +id: basic-raw-unsafe-request-example + +info: + name: Test RAW Unsafe Request Template + author: pd-team + severity: info + +requests: + - raw: + - | + GET / HTTP/1.1 + Host: + Content-Length: 4 + + unsafe: true + matchers-condition: and + matchers: + - type: word + words: + - "This is test-raw-unsafe request matcher." \ No newline at end of file diff --git a/integration_tests/run.sh b/integration_tests/run.sh new file mode 100644 index 00000000..96d7cc0a --- /dev/null +++ b/integration_tests/run.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +cd ../v2/cmd/nuclei +go build +cp nuclei ../../../integration_tests/nuclei +cd ../integration-test +go build +cp integration-test ../../../integration_tests/integration-test +cd ../../../integration_tests +./integration-test +# Build and run nuclei. \ No newline at end of file diff --git a/v2/cmd/integration-test/http.go b/v2/cmd/integration-test/http.go new file mode 100644 index 00000000..5cc3e813 --- /dev/null +++ b/v2/cmd/integration-test/http.go @@ -0,0 +1,491 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/http/httputil" + "strings" + + "github.com/julienschmidt/httprouter" + "github.com/projectdiscovery/nuclei/v2/internal/testutils" +) + +var httpTestcases = map[string]testutils.TestCase{ + "http/get-headers.yaml": &httpGetHeaders{}, + "http/get-query-string.yaml": &httpGetQueryString{}, + "http/get-redirects.yaml": &httpGetRedirects{}, + "http/get.yaml": &httpGet{}, + "http/post-body.yaml": &httpPostBody{}, + "http/post-json-body.yaml": &httpPostJSONBody{}, + "http/post-multipart-body.yaml": &httpPostMultipartBody{}, + "http/raw-cookie-reuse.yaml": &httpRawCookieReuse{}, + "http/raw-dynamic-extractor.yaml": &httpRawDynamicExtractor{}, + "http/raw-get-query.yaml": &httpRawGetQuery{}, + "http/raw-get.yaml": &httpRawGet{}, + "http/raw-payload.yaml": &httpRawPayload{}, + "http/raw-post-body.yaml": &httpRawPostBody{}, + "http/raw-unsafe-request.yaml": &httpRawUnsafeRequest{}, +} + +func httpDebugRequestDump(r *http.Request) { + if debug { + if dump, err := httputil.DumpRequest(r, true); err == nil { + fmt.Printf("\nRequest dump: \n%s\n\n", string(dump)) + } + } +} + +type httpGetHeaders struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpGetHeaders) Execute(filePath string) error { + router := httprouter.New() + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if strings.EqualFold(r.Header.Get("test"), "nuclei") { + fmt.Fprintf(w, "This is test headers matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpGetQueryString struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpGetQueryString) Execute(filePath string) error { + router := httprouter.New() + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if strings.EqualFold(r.URL.Query().Get("test"), "nuclei") { + fmt.Fprintf(w, "This is test querystring matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpGetRedirects struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpGetRedirects) Execute(filePath string) error { + router := httprouter.New() + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + http.Redirect(w, r, "/redirected", 302) + })) + router.GET("/redirected", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + fmt.Fprintf(w, "This is test redirects matcher text") + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpGet struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpGet) Execute(filePath string) error { + router := httprouter.New() + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + fmt.Fprintf(w, "This is test matcher text") + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpPostBody struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpPostBody) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.POST("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if err := r.ParseForm(); err != nil { + routerErr = err + return + } + if strings.EqualFold(r.Form.Get("username"), "test") && strings.EqualFold(r.Form.Get("password"), "nuclei") { + fmt.Fprintf(w, "This is test post-body matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpPostJSONBody struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpPostJSONBody) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.POST("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + + type doc struct { + Username string `json:"username"` + Password string `json:"password"` + } + obj := &doc{} + if err := json.NewDecoder(r.Body).Decode(obj); err != nil { + routerErr = err + return + } + if strings.EqualFold(obj.Username, "test") && strings.EqualFold(obj.Password, "nuclei") { + fmt.Fprintf(w, "This is test post-json-body matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpPostMultipartBody struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpPostMultipartBody) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.POST("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if err := r.ParseMultipartForm(1 * 1024); err != nil { + routerErr = err + return + } + password, ok := r.MultipartForm.Value["password"] + if !ok || len(password) != 1 { + routerErr = errors.New("no password in request") + return + } + file := r.MultipartForm.File["username"] + if len(file) != 1 { + routerErr = errors.New("no file in request") + return + } + if strings.EqualFold(password[0], "nuclei") && strings.EqualFold(file[0].Filename, "username") { + fmt.Fprintf(w, "This is test post-multipart matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpRawDynamicExtractor struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpRawDynamicExtractor) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.POST("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if err := r.ParseForm(); err != nil { + routerErr = err + return + } + if strings.EqualFold(r.Form.Get("testing"), "parameter") { + fmt.Fprintf(w, "Token: 'nuclei'") + } + })) + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if strings.EqualFold(r.URL.Query().Get("username"), "nuclei") { + fmt.Fprintf(w, "Test is test-dynamic-extractor-raw matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpRawGetQuery struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpRawGetQuery) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if strings.EqualFold(r.URL.Query().Get("test"), "nuclei") { + fmt.Fprintf(w, "Test is test raw-get-query-matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpRawGet struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpRawGet) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + + fmt.Fprintf(w, "Test is test raw-get-matcher text") + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpRawPayload struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpRawPayload) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.POST("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if err := r.ParseForm(); err != nil { + routerErr = err + return + } + if !(strings.EqualFold(r.Header.Get("another_header"), "bnVjbGVp") || strings.EqualFold(r.Header.Get("another_header"), "Z3Vlc3Q=")) { + return + } + if strings.EqualFold(r.Form.Get("username"), "test") && (strings.EqualFold(r.Form.Get("password"), "nuclei") || strings.EqualFold(r.Form.Get("password"), "guest")) { + fmt.Fprintf(w, "Test is raw-payload matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 2 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpRawPostBody struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpRawPostBody) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.POST("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if err := r.ParseForm(); err != nil { + routerErr = err + return + } + if strings.EqualFold(r.Form.Get("username"), "test") && strings.EqualFold(r.Form.Get("password"), "nuclei") { + fmt.Fprintf(w, "Test is test raw-post-body-matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpRawCookieReuse struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpRawCookieReuse) Execute(filePath string) error { + router := httprouter.New() + var routerErr error + + router.POST("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if err := r.ParseForm(); err != nil { + routerErr = err + return + } + if strings.EqualFold(r.Form.Get("testing"), "parameter") { + http.SetCookie(w, &http.Cookie{Name: "nuclei", Value: "test"}) + } + })) + router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + httpDebugRequestDump(r) + if err := r.ParseForm(); err != nil { + routerErr = err + return + } + cookie, err := r.Cookie("nuclei") + if err != nil { + routerErr = err + return + } + + if strings.EqualFold(cookie.Value, "test") { + fmt.Fprintf(w, "Test is test-cookie-reuse matcher text") + } + })) + ts := httptest.NewServer(router) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} + +type httpRawUnsafeRequest struct{} + +// Executes executes a test case and returns an error if occured +func (h *httpRawUnsafeRequest) Execute(filePath string) error { + var routerErr error + + ts := testutils.NewTCPServer(func(conn net.Conn) { + defer conn.Close() + + conn.Write([]byte("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 40\r\nContent-Type: text/plain; charset=utf-8\r\nDate: Thu, 25 Feb 2021 17:17:28 GMT\r\n\r\nThis is test-raw-unsafe request matcher.\r\n")) + }) + defer ts.Close() + + results, err := testutils.RunNucleiAndGetResults(filePath, "http://"+ts.URL, debug) + if err != nil { + return err + } + if routerErr != nil { + return routerErr + } + if len(results) != 1 { + return errIncorrectResultsCount(results) + } + return nil +} diff --git a/v2/cmd/integration-test/integration-test.go b/v2/cmd/integration-test/integration-test.go new file mode 100644 index 00000000..8fa92a66 --- /dev/null +++ b/v2/cmd/integration-test/integration-test.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/logrusorgru/aurora" +) + +var ( + debug = os.Getenv("DEBUG") == "true" + customTest = os.Getenv("TEST") +) + +func main() { + success := aurora.Green("[✓]").String() + failed := aurora.Red("[✘]").String() + + for file, test := range httpTestcases { + if customTest != "" && !strings.Contains(file, customTest) { + continue // only run tests user asked + } + err := test.Execute(file) + if err != nil { + fmt.Fprintf(os.Stderr, "%s Test \"%s\" failed: %s\n", failed, file, err) + } else { + fmt.Printf("%s Test \"%s\" passed!\n", success, file) + } + } +} + +func errIncorrectResultsCount(results []string) error { + return fmt.Errorf("incorrect number of results %s", strings.Join(results, "\n\t")) +} diff --git a/v2/go.mod b/v2/go.mod index 0436910a..4aed85aa 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -16,6 +16,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 // indirect github.com/json-iterator/go v1.1.10 + github.com/julienschmidt/httprouter v1.3.0 github.com/karrick/godirwalk v1.16.1 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mattn/go-runewidth v0.0.10 // indirect diff --git a/v2/go.sum b/v2/go.sum index 138eddaf..39d502a9 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -154,6 +154,8 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= diff --git a/v2/internal/testutils/integration.go b/v2/internal/testutils/integration.go new file mode 100644 index 00000000..5ef7091b --- /dev/null +++ b/v2/internal/testutils/integration.go @@ -0,0 +1,71 @@ +package testutils + +import ( + "net" + "os" + "os/exec" + "strings" +) + +// RunNucleiAndGetResults returns a list of results for a template +func RunNucleiAndGetResults(template string, URL string, debug bool) ([]string, error) { + cmd := exec.Command("./nuclei", "-t", template, "-target", URL) + if debug { + cmd = exec.Command("./nuclei", "-t", template, "-target", URL, "-debug") + cmd.Stderr = os.Stderr + } + data, err := cmd.Output() + if err != nil { + return nil, err + } + parts := []string{} + items := strings.Split(string(data), "\n") + for _, i := range items { + if i != "" { + parts = append(parts, i) + } + } + return parts, nil +} + +// TestCase is a single integration test case +type TestCase interface { + // Execute executes a test case and returns any errors if occured + Execute(filePath string) error +} + +// TCPServer creates a new tcp server that returns a response +type TCPServer struct { + URL string + listener net.Listener +} + +// NewTCPServer creates a new TCP server from a handler +func NewTCPServer(handler func(conn net.Conn)) *TCPServer { + server := &TCPServer{} + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + server.URL = l.Addr().String() + server.listener = l + + go func() { + for { + // Listen for an incoming connection. + conn, err := l.Accept() + if err != nil { + continue + } + // Handle connections in a new goroutine. + go handler(conn) + } + }() + return server +} + +// Close closes the TCP server +func (s *TCPServer) Close() { + s.listener.Close() +}