mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-23 15:05:24 +00:00
Merge pull request #1232 from projectdiscovery/fix-redirect-response-bug
fix #1173: perform matching on all redirect responses instead of final
This commit is contained in:
commit
66074a1842
23
integration_tests/http/get-redirects-chain-headers.yaml
Normal file
23
integration_tests/http/get-redirects-chain-headers.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
id: basic-get-redirects-chain-headers
|
||||||
|
|
||||||
|
info:
|
||||||
|
name: Basic GET Redirects Request With Chain header
|
||||||
|
author: pdteam
|
||||||
|
severity: info
|
||||||
|
|
||||||
|
requests:
|
||||||
|
- method: GET
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}"
|
||||||
|
redirects: true
|
||||||
|
max-redirects: 3
|
||||||
|
matchers-condition: and
|
||||||
|
matchers:
|
||||||
|
- type: word
|
||||||
|
part: header
|
||||||
|
words:
|
||||||
|
- "TestRedirectHeaderMatch"
|
||||||
|
|
||||||
|
- type: status
|
||||||
|
status:
|
||||||
|
- 302
|
||||||
@ -35,6 +35,7 @@ var httpTestcases = map[string]testutils.TestCase{
|
|||||||
"http/self-contained.yaml": &httpRequestSelContained{},
|
"http/self-contained.yaml": &httpRequestSelContained{},
|
||||||
"http/get-case-insensitive.yaml": &httpGetCaseInsensitive{},
|
"http/get-case-insensitive.yaml": &httpGetCaseInsensitive{},
|
||||||
"http/get.yaml,http/get-case-insensitive.yaml": &httpGetCaseInsensitiveCluster{},
|
"http/get.yaml,http/get-case-insensitive.yaml": &httpGetCaseInsensitiveCluster{},
|
||||||
|
"http/get-redirects-chain-headers.yaml": &httpGetRedirectsChainHeaders{},
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpInteractshRequest struct{}
|
type httpInteractshRequest struct{}
|
||||||
@ -599,3 +600,31 @@ func (h *httpGetCaseInsensitiveCluster) Execute(filesPath string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type httpGetRedirectsChainHeaders struct{}
|
||||||
|
|
||||||
|
// Execute executes a test case and returns an error if occurred
|
||||||
|
func (h *httpGetRedirectsChainHeaders) Execute(filePath string) error {
|
||||||
|
router := httprouter.New()
|
||||||
|
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
http.Redirect(w, r, "/redirected", http.StatusFound)
|
||||||
|
})
|
||||||
|
router.GET("/redirected", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
w.Header().Set("Secret", "TestRedirectHeaderMatch")
|
||||||
|
http.Redirect(w, r, "/final", http.StatusFound)
|
||||||
|
})
|
||||||
|
router.GET("/final", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
ts := httptest.NewServer(router)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(results) != 1 {
|
||||||
|
return errIncorrectResultsCount(results)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -51,5 +51,5 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func errIncorrectResultsCount(results []string) error {
|
func errIncorrectResultsCount(results []string) error {
|
||||||
return fmt.Errorf("incorrect number of results %s", strings.Join(results, "\n\t"))
|
return fmt.Errorf("incorrect number of results \n\t%s", strings.Join(results, "\n\t"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -400,7 +400,7 @@ func (request *Request) executeRequest(reqURL string, generatedRequest *generate
|
|||||||
return errors.Wrap(err, "could not dump http response")
|
return errors.Wrap(err, "could not dump http response")
|
||||||
}
|
}
|
||||||
|
|
||||||
var data, redirectedResponse []byte
|
var dumpedResponse []redirectedResponse
|
||||||
// If the status code is HTTP 101, we should not proceed with reading body.
|
// If the status code is HTTP 101, we should not proceed with reading body.
|
||||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||||
var bodyReader io.Reader
|
var bodyReader io.Reader
|
||||||
@ -409,7 +409,7 @@ func (request *Request) executeRequest(reqURL string, generatedRequest *generate
|
|||||||
} else {
|
} else {
|
||||||
bodyReader = resp.Body
|
bodyReader = resp.Body
|
||||||
}
|
}
|
||||||
data, err = ioutil.ReadAll(bodyReader)
|
data, err := ioutil.ReadAll(bodyReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Ignore body read due to server misconfiguration errors
|
// Ignore body read due to server misconfiguration errors
|
||||||
if stringsutil.ContainsAny(err.Error(), "gzip: invalid header") {
|
if stringsutil.ContainsAny(err.Error(), "gzip: invalid header") {
|
||||||
@ -420,54 +420,18 @@ func (request *Request) executeRequest(reqURL string, generatedRequest *generate
|
|||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
redirectedResponse, err = dumpResponseWithRedirectChain(resp, data)
|
dumpedResponse, err = dumpResponseWithRedirectChain(resp, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not read http response with redirect chain")
|
return errors.Wrap(err, "could not read http response with redirect chain")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
redirectedResponse = dumpedResponseHeaders
|
dumpedResponse = []redirectedResponse{{fullResponse: dumpedResponseHeaders, headers: dumpedResponseHeaders}}
|
||||||
}
|
|
||||||
|
|
||||||
// net/http doesn't automatically decompress the response body if an
|
|
||||||
// encoding has been specified by the user in the request so in case we have to
|
|
||||||
// manually do it.
|
|
||||||
dataOrig := data
|
|
||||||
data, err = handleDecompression(resp, data)
|
|
||||||
// in case of error use original data
|
|
||||||
if err != nil {
|
|
||||||
data = dataOrig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dump response - step 2 - replace gzip body with deflated one or with itself (NOP operation)
|
|
||||||
dumpedResponseBuilder := &bytes.Buffer{}
|
|
||||||
dumpedResponseBuilder.Write(dumpedResponseHeaders)
|
|
||||||
dumpedResponseBuilder.Write(data)
|
|
||||||
dumpedResponse := dumpedResponseBuilder.Bytes()
|
|
||||||
redirectedResponse = bytes.ReplaceAll(redirectedResponse, dataOrig, data)
|
|
||||||
|
|
||||||
// Decode gbk response content-types
|
|
||||||
// gb18030 supersedes gb2312
|
|
||||||
responseContentType := resp.Header.Get("Content-Type")
|
|
||||||
if isContentTypeGbk(responseContentType) {
|
|
||||||
dumpedResponse, err = decodegbk(dumpedResponse)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not gbk decode")
|
|
||||||
}
|
|
||||||
redirectedResponse, err = decodegbk(redirectedResponse)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not gbk decode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// the uncompressed body needs to be decoded to standard utf8
|
|
||||||
data, err = decodegbk(data)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "could not gbk decode")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, response := range dumpedResponse {
|
||||||
// if nuclei-project is enabled store the response if not previously done
|
// if nuclei-project is enabled store the response if not previously done
|
||||||
if request.options.ProjectFile != nil && !fromCache {
|
if request.options.ProjectFile != nil && !fromCache {
|
||||||
if err := request.options.ProjectFile.Set(dumpedRequest, resp, data); err != nil {
|
if err := request.options.ProjectFile.Set(dumpedRequest, resp, response.body); err != nil {
|
||||||
return errors.Wrap(err, "could not store in project file")
|
return errors.Wrap(err, "could not store in project file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -481,13 +445,12 @@ func (request *Request) executeRequest(reqURL string, generatedRequest *generate
|
|||||||
}
|
}
|
||||||
finalEvent := make(output.InternalEvent)
|
finalEvent := make(output.InternalEvent)
|
||||||
|
|
||||||
outputEvent := request.responseToDSLMap(resp, reqURL, matchedURL, tostring.UnsafeToString(dumpedRequest), tostring.UnsafeToString(dumpedResponse), tostring.UnsafeToString(data), headersToString(resp.Header), duration, generatedRequest.meta)
|
outputEvent := request.responseToDSLMap(response.resp, reqURL, matchedURL, tostring.UnsafeToString(dumpedRequest), tostring.UnsafeToString(response.fullResponse), tostring.UnsafeToString(response.body), tostring.UnsafeToString(response.headers), duration, generatedRequest.meta)
|
||||||
if i := strings.LastIndex(hostname, ":"); i != -1 {
|
if i := strings.LastIndex(hostname, ":"); i != -1 {
|
||||||
hostname = hostname[:i]
|
hostname = hostname[:i]
|
||||||
}
|
}
|
||||||
outputEvent["curl-command"] = curlCommand
|
outputEvent["curl-command"] = curlCommand
|
||||||
outputEvent["ip"] = httpclientpool.Dialer.GetDialedIP(hostname)
|
outputEvent["ip"] = httpclientpool.Dialer.GetDialedIP(hostname)
|
||||||
outputEvent["redirect-chain"] = tostring.UnsafeToString(redirectedResponse)
|
|
||||||
for k, v := range previousEvent {
|
for k, v := range previousEvent {
|
||||||
finalEvent[k] = v
|
finalEvent[k] = v
|
||||||
}
|
}
|
||||||
@ -507,9 +470,11 @@ func (request *Request) executeRequest(reqURL string, generatedRequest *generate
|
|||||||
internalWrappedEvent.OperatorsResult.PayloadValues = generatedRequest.meta
|
internalWrappedEvent.OperatorsResult.PayloadValues = generatedRequest.meta
|
||||||
})
|
})
|
||||||
|
|
||||||
dumpResponse(event, request.options, redirectedResponse, formedURL, responseContentType)
|
responseContentType := resp.Header.Get("Content-Type")
|
||||||
|
dumpResponse(event, request.options, response.fullResponse, formedURL, responseContentType)
|
||||||
|
|
||||||
callback(event)
|
callback(event)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,14 +10,21 @@ import (
|
|||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators"
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/tostring"
|
|
||||||
"github.com/projectdiscovery/rawhttp"
|
"github.com/projectdiscovery/rawhttp"
|
||||||
"github.com/projectdiscovery/stringsutil"
|
"github.com/projectdiscovery/stringsutil"
|
||||||
"golang.org/x/text/encoding/simplifiedchinese"
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type redirectedResponse struct {
|
||||||
|
headers []byte
|
||||||
|
body []byte
|
||||||
|
fullResponse []byte
|
||||||
|
resp *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
// dumpResponseWithRedirectChain dumps a http response with the
|
// dumpResponseWithRedirectChain dumps a http response with the
|
||||||
// complete http redirect chain.
|
// complete http redirect chain.
|
||||||
//
|
//
|
||||||
@ -25,18 +32,23 @@ import (
|
|||||||
// and returns the data to the user for matching and viewing in that order.
|
// and returns the data to the user for matching and viewing in that order.
|
||||||
//
|
//
|
||||||
// Inspired from - https://github.com/ffuf/ffuf/issues/324#issuecomment-719858923
|
// Inspired from - https://github.com/ffuf/ffuf/issues/324#issuecomment-719858923
|
||||||
func dumpResponseWithRedirectChain(resp *http.Response, body []byte) ([]byte, error) {
|
func dumpResponseWithRedirectChain(resp *http.Response, body []byte) ([]redirectedResponse, error) {
|
||||||
redirects := []string{}
|
var response []redirectedResponse
|
||||||
|
|
||||||
respData, err := httputil.DumpResponse(resp, false)
|
respData, err := httputil.DumpResponse(resp, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
redirectChain := &bytes.Buffer{}
|
respObj := redirectedResponse{
|
||||||
|
headers: respData,
|
||||||
redirectChain.WriteString(tostring.UnsafeToString(respData))
|
body: body,
|
||||||
redirectChain.Write(body)
|
resp: resp,
|
||||||
redirects = append(redirects, redirectChain.String())
|
fullResponse: bytes.Join([][]byte{respData, body}, []byte{}),
|
||||||
redirectChain.Reset()
|
}
|
||||||
|
if err := normalizeResponseBody(resp, &respObj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response = append(response, respObj)
|
||||||
|
|
||||||
var redirectResp *http.Response
|
var redirectResp *http.Response
|
||||||
if resp != nil && resp.Request != nil {
|
if resp != nil && resp.Request != nil {
|
||||||
@ -52,40 +64,51 @@ func dumpResponseWithRedirectChain(resp *http.Response, body []byte) ([]byte, er
|
|||||||
if redirectResp.Body != nil {
|
if redirectResp.Body != nil {
|
||||||
body, _ = ioutil.ReadAll(redirectResp.Body)
|
body, _ = ioutil.ReadAll(redirectResp.Body)
|
||||||
}
|
}
|
||||||
redirectChain.WriteString(tostring.UnsafeToString(respData))
|
respObj := redirectedResponse{
|
||||||
if len(body) > 0 {
|
headers: respData,
|
||||||
redirectChain.WriteString(tostring.UnsafeToString(body))
|
body: body,
|
||||||
|
resp: redirectResp,
|
||||||
|
fullResponse: bytes.Join([][]byte{respData, body}, []byte{}),
|
||||||
}
|
}
|
||||||
redirects = append(redirects, redirectChain.String())
|
if err := normalizeResponseBody(redirectResp, &respObj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response = append(response, respObj)
|
||||||
redirectResp = redirectResp.Request.Response
|
redirectResp = redirectResp.Request.Response
|
||||||
redirectChain.Reset()
|
|
||||||
}
|
}
|
||||||
for i := len(redirects) - 1; i >= 0; i-- {
|
return response, nil
|
||||||
redirectChain.WriteString(redirects[i])
|
|
||||||
}
|
|
||||||
return redirectChain.Bytes(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// headersToString converts http headers to string
|
// normalizeResponseBody performs normalization on the http response object.
|
||||||
func headersToString(headers http.Header) string {
|
func normalizeResponseBody(resp *http.Response, response *redirectedResponse) error {
|
||||||
builder := &strings.Builder{}
|
var err error
|
||||||
|
// net/http doesn't automatically decompress the response body if an
|
||||||
|
// encoding has been specified by the user in the request so in case we have to
|
||||||
|
// manually do it.
|
||||||
|
dataOrig := response.body
|
||||||
|
response.body, err = handleDecompression(resp, response.body)
|
||||||
|
// in case of error use original data
|
||||||
|
if err != nil {
|
||||||
|
response.body = dataOrig
|
||||||
|
}
|
||||||
|
response.fullResponse = bytes.ReplaceAll(response.fullResponse, dataOrig, response.body)
|
||||||
|
|
||||||
for header, values := range headers {
|
// Decode gbk response content-types
|
||||||
builder.WriteString(header)
|
// gb18030 supersedes gb2312
|
||||||
builder.WriteString(": ")
|
responseContentType := resp.Header.Get("Content-Type")
|
||||||
|
if isContentTypeGbk(responseContentType) {
|
||||||
|
response.fullResponse, err = decodegbk(response.fullResponse)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not gbk decode")
|
||||||
|
}
|
||||||
|
|
||||||
for i, value := range values {
|
// the uncompressed body needs to be decoded to standard utf8
|
||||||
builder.WriteString(value)
|
response.body, err = decodegbk(response.body)
|
||||||
|
if err != nil {
|
||||||
if i != len(values)-1 {
|
return errors.Wrap(err, "could not gbk decode")
|
||||||
builder.WriteRune('\n')
|
|
||||||
builder.WriteString(header)
|
|
||||||
builder.WriteString(": ")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.WriteRune('\n')
|
return nil
|
||||||
}
|
|
||||||
return builder.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dump creates a dump of the http request in form of a byte slice
|
// dump creates a dump of the http request in form of a byte slice
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user