Merge branch 'dev' of https://github.com/projectdiscovery/nuclei into more-protocols

This commit is contained in:
Ice3man543 2021-11-11 17:21:25 +05:30
commit c1a35b3ff9
19 changed files with 405 additions and 228 deletions

View File

@ -162,16 +162,16 @@ HEADLESS:
-sc, -system-chrome Use local installed chrome browser instead of nuclei installed -sc, -system-chrome Use local installed chrome browser instead of nuclei installed
DEBUG: DEBUG:
-debug show all requests and responses -debug show all requests and responses
-debug-req show all sent requests -debug-req show all sent requests
-debug-resp show all received responses -debug-resp show all received responses
-proxy, -proxy-url string URL of the HTTP proxy server -p, -proxy string[] List of HTTP(s)/SOCKS5 proxy to use (comma separated or file input)
-proxy-socks-url string URL of the SOCKS proxy server -tlog, -trace-log string file to write sent requests trace log
-tlog, -trace-log string file to write sent requests trace log -elog, -error-log string file to write sent requests error log
-version show nuclei version -version show nuclei version
-v, -verbose show verbose output -v, -verbose show verbose output
-vv display extra verbose information -vv display templates loaded for scan
-tv, -templates-version shows the version of the installed nuclei-templates -tv, -templates-version shows the version of the installed nuclei-templates
UPDATE: UPDATE:
-update update nuclei engine to the latest released version -update update nuclei engine to the latest released version

View File

@ -121,8 +121,7 @@ nuclei -h
|templates-version|显示已安装的模板版本|nuclei -templates-version| |templates-version|显示已安装的模板版本|nuclei -templates-version|
|v|显示发送请求的详细信息|nuclei -v| |v|显示发送请求的详细信息|nuclei -v|
|version|显示nuclei的版本号|nuclei -version| |version|显示nuclei的版本号|nuclei -version|
|proxy-url|输入代理地址|nuclei -proxy-url hxxp://127.0.0.1:8080| |proxy|输入代理地址|nuclei -proxy ./proxy.txt|
|proxy-socks-url|输入socks代理地址|nuclei -proxy-socks-url socks5://127.0.0.1:8080|
|random-agent|使用随机的UA|nuclei -random-agent| |random-agent|使用随机的UA|nuclei -random-agent|
|H|自定义请求头|nuclei -H “x-bug-bounty:hacker”| |H|自定义请求头|nuclei -H “x-bug-bounty:hacker”|

View 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

View File

@ -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
}

View File

@ -52,5 +52,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"))
} }

View File

@ -143,11 +143,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVar(&options.Debug, "debug", false, "show all requests and responses"), flagSet.BoolVar(&options.Debug, "debug", false, "show all requests and responses"),
flagSet.BoolVar(&options.DebugRequests, "debug-req", false, "show all sent requests"), flagSet.BoolVar(&options.DebugRequests, "debug-req", false, "show all sent requests"),
flagSet.BoolVar(&options.DebugResponse, "debug-resp", false, "show all received responses"), flagSet.BoolVar(&options.DebugResponse, "debug-resp", false, "show all received responses"),
flagSet.NormalizedStringSliceVarP(&options.Proxy, "proxy", "p", []string{}, "List of HTTP(s)/SOCKS5 proxy to use (comma separated or file input)"),
/* TODO why the separation? http://proxy:port vs socks5://proxy:port etc
TODO should auto-set the HTTP_PROXY variable for the process? */
flagSet.StringVarP(&options.ProxyURL, "proxy-url", "proxy", "", "URL of the HTTP proxy server"),
flagSet.StringVar(&options.ProxySocksURL, "proxy-socks-url", "", "URL of the SOCKS proxy server"),
flagSet.StringVarP(&options.TraceLogFile, "trace-log", "tlog", "", "file to write sent requests trace log"), flagSet.StringVarP(&options.TraceLogFile, "trace-log", "tlog", "", "file to write sent requests trace log"),
flagSet.StringVarP(&options.ErrorLogFile, "error-log", "elog", "", "file to write sent requests error log"), flagSet.StringVarP(&options.ErrorLogFile, "error-log", "elog", "", "file to write sent requests error log"),
flagSet.BoolVar(&options.Version, "version", false, "show nuclei version"), flagSet.BoolVar(&options.Version, "version", false, "show nuclei version"),

View File

@ -3,7 +3,6 @@ package runner
import ( import (
"bufio" "bufio"
"errors" "errors"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -24,7 +23,6 @@ func ParseOptions(options *types.Options) {
// Read the inputs and configure the logging // Read the inputs and configure the logging
configureOutput(options) configureOutput(options)
// Show the user the banner // Show the user the banner
showBanner() showBanner()
@ -82,15 +80,10 @@ func validateOptions(options *types.Options) error {
if options.Verbose && options.Silent { if options.Verbose && options.Silent {
return errors.New("both verbose and silent mode specified") return errors.New("both verbose and silent mode specified")
} }
//loading the proxy server list from file or cli and test the connectivity
if err := validateProxyURL(options.ProxyURL, "invalid http proxy format (It should be http://username:password@host:port)"); err != nil { if err := loadProxyServers(options); err != nil {
return err return err
} }
if err := validateProxyURL(options.ProxySocksURL, "invalid socks proxy format (It should be socks5://username:password@host:port)"); err != nil {
return err
}
if options.Validate { if options.Validate {
options.Headless = true // required for correct validation of headless templates options.Headless = true // required for correct validation of headless templates
validateTemplatePaths(options.TemplatesDirectory, options.Templates, options.Workflows) validateTemplatePaths(options.TemplatesDirectory, options.Templates, options.Workflows)
@ -107,19 +100,6 @@ func validateOptions(options *types.Options) error {
return nil return nil
} }
func validateProxyURL(proxyURL, message string) error {
if proxyURL != "" && !isValidURL(proxyURL) {
return errors.New(message)
}
return nil
}
func isValidURL(urlString string) bool {
_, err := url.Parse(urlString)
return err == nil
}
// configureOutput configures the output logging levels to be displayed on the screen // configureOutput configures the output logging levels to be displayed on the screen
func configureOutput(options *types.Options) { func configureOutput(options *types.Options) {
// If the user desires verbose output, show verbose output // If the user desires verbose output, show verbose output
@ -165,7 +145,6 @@ func loadResolvers(options *types.Options) {
func validateTemplatePaths(templatesDirectory string, templatePaths, workflowPaths []string) { func validateTemplatePaths(templatesDirectory string, templatePaths, workflowPaths []string) {
allGivenTemplatePaths := append(templatePaths, workflowPaths...) allGivenTemplatePaths := append(templatePaths, workflowPaths...)
for _, templatePath := range allGivenTemplatePaths { for _, templatePath := range allGivenTemplatePaths {
if templatesDirectory != templatePath && filepath.IsAbs(templatePath) { if templatesDirectory != templatePath && filepath.IsAbs(templatePath) {
fileInfo, err := os.Stat(templatePath) fileInfo, err := os.Stat(templatePath)

123
v2/internal/runner/proxy.go Normal file
View File

@ -0,0 +1,123 @@
package runner
import (
"bufio"
"errors"
"fmt"
"net"
"net/url"
"os"
"strings"
"time"
"github.com/projectdiscovery/fileutil"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v2/pkg/types"
)
var proxyURLList []url.URL
// loadProxyServers load list of proxy servers from file or comma seperated
func loadProxyServers(options *types.Options) error {
if len(options.Proxy) == 0 {
return nil
}
for _, p := range options.Proxy {
if proxyURL, err := validateProxyURL(p); err == nil {
proxyURLList = append(proxyURLList, proxyURL)
} else if fileutil.FileExists(p) {
file, err := os.Open(p)
if err != nil {
return fmt.Errorf("could not open proxy file: %s", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
proxy := scanner.Text()
if strings.TrimSpace(proxy) == "" {
continue
}
if proxyURL, err := validateProxyURL(proxy); err != nil {
return err
} else {
proxyURLList = append(proxyURLList, proxyURL)
}
}
} else {
return fmt.Errorf("invalid proxy file or URL provided for %s", p)
}
}
return processProxyList(options)
}
func processProxyList(options *types.Options) error {
if len(proxyURLList) == 0 {
return fmt.Errorf("could not find any valid proxy")
} else {
done := make(chan bool)
exitCounter := make(chan bool)
counter := 0
for _, url := range proxyURLList {
go runProxyConnectivity(url, options, done, exitCounter)
}
for {
select {
case <-done:
{
close(done)
return nil
}
case <-exitCounter:
{
if counter += 1; counter == len(proxyURLList) {
return errors.New("no reachable proxy found")
}
}
}
}
}
}
func runProxyConnectivity(proxyURL url.URL, options *types.Options, done chan bool, exitCounter chan bool) {
if err := testProxyConnection(proxyURL, options.Timeout); err == nil {
if types.ProxyURL == "" && types.ProxySocksURL == "" {
assignProxyURL(proxyURL, options)
done <- true
}
}
exitCounter <- true
}
func testProxyConnection(proxyURL url.URL, timeoutDelay int) error {
timeout := time.Duration(timeoutDelay) * time.Second
_, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", proxyURL.Hostname(), proxyURL.Port()), timeout)
if err != nil {
return err
}
return nil
}
func assignProxyURL(proxyURL url.URL, options *types.Options) {
os.Setenv(types.HTTP_PROXY_ENV, proxyURL.String())
if proxyURL.Scheme == types.HTTP || proxyURL.Scheme == types.HTTPS {
types.ProxyURL = proxyURL.String()
types.ProxySocksURL = ""
gologger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
} else if proxyURL.Scheme == types.SOCKS5 {
types.ProxyURL = ""
types.ProxySocksURL = proxyURL.String()
gologger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
}
}
func validateProxyURL(proxy string) (url.URL, error) {
if url, err := url.Parse(proxy); err == nil && isSupportedProtocol(url.Scheme) {
return *url, nil
}
return url.URL{}, errors.New("invalid proxy format (It should be http[s]/socks5://[username:password@]host:port)")
}
//isSupportedProtocol checks given protocols are supported
func isSupportedProtocol(value string) bool {
return value == types.HTTP || value == types.HTTPS || value == types.SOCKS5
}

View File

@ -14,7 +14,7 @@ import (
"github.com/projectdiscovery/nuclei/v2/pkg/types" "github.com/projectdiscovery/nuclei/v2/pkg/types"
) )
// Match matches a generic data response again a given matcher // Match matches a generic data response against a given matcher
func (request *Request) Match(data map[string]interface{}, matcher *matchers.Matcher) (bool, []string) { func (request *Request) Match(data map[string]interface{}, matcher *matchers.Matcher) (bool, []string) {
item, ok := request.getMatchPart(matcher.Part, data) item, ok := request.getMatchPart(matcher.Part, data)
if !ok { if !ok {

View File

@ -63,8 +63,8 @@ func New(options *types.Options) (*Browser, error) {
} else { } else {
chromeLauncher = chromeLauncher.Headless(true) chromeLauncher = chromeLauncher.Headless(true)
} }
if options.ProxyURL != "" { if types.ProxyURL != "" {
chromeLauncher = chromeLauncher.Proxy(options.ProxyURL) chromeLauncher = chromeLauncher.Proxy(types.ProxyURL)
} }
launcherURL, err := chromeLauncher.Launch() launcherURL, err := chromeLauncher.Launch()
if err != nil { if err != nil {
@ -88,7 +88,12 @@ func New(options *types.Options) (*Browser, error) {
if customAgent == "" { if customAgent == "" {
customAgent = uarand.GetRandom() customAgent = uarand.GetRandom()
} }
httpclient := newhttpClient(options)
httpclient, err := newhttpClient(options)
if err != nil {
return nil, err
}
engine := &Browser{ engine := &Browser{
tempDir: dataStore, tempDir: dataStore,
customAgent: customAgent, customAgent: customAgent,

View File

@ -4,20 +4,21 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"time" "time"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v2/pkg/types" "github.com/projectdiscovery/nuclei/v2/pkg/types"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
) )
// newhttpClient creates a new http client for headless communication with a timeout // newhttpClient creates a new http client for headless communication with a timeout
func newhttpClient(options *types.Options) *http.Client { func newhttpClient(options *types.Options) (*http.Client, error) {
dialer := protocolstate.Dialer dialer := protocolstate.Dialer
// Set the base TLS configuration definition // Set the base TLS configuration definition
@ -27,7 +28,11 @@ func newhttpClient(options *types.Options) *http.Client {
} }
// Add the client certificate authentication to the request if it's configured // Add the client certificate authentication to the request if it's configured
tlsConfig = utils.AddConfiguredClientCertToRequest(tlsConfig, options) var err error
tlsConfig, err = utils.AddConfiguredClientCertToRequest(tlsConfig, options)
if err != nil {
return nil, err
}
transport := &http.Transport{ transport := &http.Transport{
DialContext: dialer.Dial, DialContext: dialer.Dial,
@ -36,15 +41,13 @@ func newhttpClient(options *types.Options) *http.Client {
MaxConnsPerHost: 500, MaxConnsPerHost: 500,
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
} }
if types.ProxyURL != "" {
if options.ProxyURL != "" { if proxyURL, err := url.Parse(types.ProxyURL); err == nil {
if proxyURL, err := url.Parse(options.ProxyURL); err == nil {
transport.Proxy = http.ProxyURL(proxyURL) transport.Proxy = http.ProxyURL(proxyURL)
} }
} else if options.ProxySocksURL != "" { } else if types.ProxySocksURL != "" {
var proxyAuth *proxy.Auth var proxyAuth *proxy.Auth
socksURL, proxyErr := url.Parse(types.ProxySocksURL)
socksURL, proxyErr := url.Parse(options.ProxySocksURL)
if proxyErr == nil { if proxyErr == nil {
proxyAuth = &proxy.Auth{} proxyAuth = &proxy.Auth{}
proxyAuth.User = socksURL.User.Username() proxyAuth.User = socksURL.User.Username()
@ -71,5 +74,5 @@ func newhttpClient(options *types.Options) *http.Client {
}, },
} }
return httpclient return httpclient, nil
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
@ -14,6 +13,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
@ -129,9 +130,8 @@ func wrappedGet(options *types.Options, configuration *Configuration) (*retryabl
return client, nil return client, nil
} }
poolMutex.RUnlock() poolMutex.RUnlock()
if types.ProxyURL != "" {
if options.ProxyURL != "" { proxyURL, err = url.Parse(types.ProxyURL)
proxyURL, err = url.Parse(options.ProxyURL)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -169,7 +169,10 @@ func wrappedGet(options *types.Options, configuration *Configuration) (*retryabl
} }
// Add the client certificate authentication to the request if it's configured // Add the client certificate authentication to the request if it's configured
tlsConfig = utils.AddConfiguredClientCertToRequest(tlsConfig, options) tlsConfig, err = utils.AddConfiguredClientCertToRequest(tlsConfig, options)
if err != nil {
return nil, errors.Wrap(err, "could not create client certificate")
}
transport := &http.Transport{ transport := &http.Transport{
DialContext: Dialer.Dial, DialContext: Dialer.Dial,
@ -179,27 +182,24 @@ func wrappedGet(options *types.Options, configuration *Configuration) (*retryabl
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
DisableKeepAlives: disableKeepAlives, DisableKeepAlives: disableKeepAlives,
} }
// Attempts to overwrite the dial function with the socks proxied version
if options.ProxySocksURL != "" {
var proxyAuth *proxy.Auth
socksURL, proxyErr := url.Parse(options.ProxySocksURL)
if proxyErr == nil {
proxyAuth = &proxy.Auth{}
proxyAuth.User = socksURL.User.Username()
proxyAuth.Password, _ = socksURL.User.Password()
}
dialer, proxyErr := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", socksURL.Hostname(), socksURL.Port()), proxyAuth, proxy.Direct)
dc := dialer.(interface {
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
})
if proxyErr == nil {
transport.DialContext = dc.DialContext
}
}
if proxyURL != nil { if proxyURL != nil {
transport.Proxy = http.ProxyURL(proxyURL) // Attempts to overwrite the dial function with the socks proxied version
if proxyURL.Scheme == types.SOCKS5 {
var proxyAuth *proxy.Auth = &proxy.Auth{}
proxyAuth.User = proxyURL.User.Username()
proxyAuth.Password, _ = proxyURL.User.Password()
dialer, proxyErr := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%s", proxyURL.Hostname(), proxyURL.Port()), proxyAuth, proxy.Direct)
dc := dialer.(interface {
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
})
if proxyErr == nil {
transport.DialContext = dc.DialContext
}
} else {
transport.Proxy = http.ProxyURL(proxyURL)
}
} }
var jar *cookiejar.Jar var jar *cookiejar.Jar

View File

@ -105,16 +105,16 @@ func (request *Request) executeParallelHTTP(reqURL string, dynamicValues output.
mutex := &sync.Mutex{} mutex := &sync.Mutex{}
for { for {
generatedHttpRequest, err := generator.Make(reqURL, dynamicValues) generatedHttpRequest, err := generator.Make(reqURL, dynamicValues)
if err == io.EOF { if err != nil {
break if err == io.EOF {
break
}
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
return err
} }
if reqURL == "" { if reqURL == "" {
reqURL = generatedHttpRequest.URL() reqURL = generatedHttpRequest.URL()
} }
if err != nil {
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
return err
}
swg.Add() swg.Add()
go func(httpRequest *generatedRequest) { go func(httpRequest *generatedRequest) {
defer swg.Done() defer swg.Done()
@ -168,18 +168,17 @@ func (request *Request) executeTurboHTTP(reqURL string, dynamicValues, previous
mutex := &sync.Mutex{} mutex := &sync.Mutex{}
for { for {
generatedHttpRequest, err := generator.Make(reqURL, dynamicValues) generatedHttpRequest, err := generator.Make(reqURL, dynamicValues)
if err == io.EOF { if err != nil {
break if err == io.EOF {
break
}
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
return err
} }
if reqURL == "" { if reqURL == "" {
reqURL = generatedHttpRequest.URL() reqURL = generatedHttpRequest.URL()
} }
if err != nil {
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
return err
}
generatedHttpRequest.pipelinedClient = pipeClient generatedHttpRequest.pipelinedClient = pipeClient
swg.Add() swg.Add()
go func(httpRequest *generatedRequest) { go func(httpRequest *generatedRequest) {
defer swg.Done() defer swg.Done()
@ -222,17 +221,16 @@ func (request *Request) ExecuteWithResults(reqURL string, dynamicValues, previou
hasInteractMarkers := interactsh.HasMatchers(request.CompiledOperators) hasInteractMarkers := interactsh.HasMatchers(request.CompiledOperators)
generatedHttpRequest, err := generator.Make(reqURL, dynamicValues) generatedHttpRequest, err := generator.Make(reqURL, dynamicValues)
if err == io.EOF { if err != nil {
break if err == io.EOF {
break
}
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
return err
} }
if reqURL == "" { if reqURL == "" {
reqURL = generatedHttpRequest.URL() reqURL = generatedHttpRequest.URL()
} }
if err != nil {
request.options.Progress.IncrementFailedRequestsBy(int64(generator.Total()))
return err
}
request.dynamicValues = generatedHttpRequest.dynamicValues request.dynamicValues = generatedHttpRequest.dynamicValues
// Check if hosts just keep erroring // Check if hosts just keep erroring
if request.options.HostErrorsCache != nil && request.options.HostErrorsCache.Check(reqURL) { if request.options.HostErrorsCache != nil && request.options.HostErrorsCache.Check(reqURL) {
@ -406,7 +404,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
@ -415,7 +413,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") {
@ -426,96 +424,61 @@ 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 for _, response := range dumpedResponse {
// encoding has been specified by the user in the request so in case we have to // if nuclei-project is enabled store the response if not previously done
// manually do it. if request.options.ProjectFile != nil && !fromCache {
dataOrig := data if err := request.options.ProjectFile.Set(dumpedRequest, resp, response.body); err != nil {
data, err = handleDecompression(resp, data) return errors.Wrap(err, "could not store in project file")
// 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 matchedURL := reqURL
data, err = decodegbk(data) if generatedRequest.rawRequest != nil && generatedRequest.rawRequest.FullURL != "" {
if err != nil { matchedURL = generatedRequest.rawRequest.FullURL
return errors.Wrap(err, "could not gbk decode")
} }
} if generatedRequest.request != nil {
matchedURL = generatedRequest.request.URL.String()
// if nuclei-project is enabled store the response if not previously done
if request.options.ProjectFile != nil && !fromCache {
if err := request.options.ProjectFile.Set(dumpedRequest, resp, data); err != nil {
return errors.Wrap(err, "could not store in project file")
} }
} finalEvent := make(output.InternalEvent)
matchedURL := reqURL 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 generatedRequest.rawRequest != nil && generatedRequest.rawRequest.FullURL != "" { if i := strings.LastIndex(hostname, ":"); i != -1 {
matchedURL = generatedRequest.rawRequest.FullURL hostname = hostname[:i]
} }
if generatedRequest.request != nil { outputEvent["curl-command"] = curlCommand
matchedURL = generatedRequest.request.URL.String() outputEvent["ip"] = httpclientpool.Dialer.GetDialedIP(hostname)
} for k, v := range previousEvent {
finalEvent := make(output.InternalEvent) finalEvent[k] = v
}
outputEvent := request.responseToDSLMap(resp, reqURL, matchedURL, tostring.UnsafeToString(dumpedRequest), tostring.UnsafeToString(dumpedResponse), tostring.UnsafeToString(data), headersToString(resp.Header), duration, generatedRequest.meta)
if i := strings.LastIndex(hostname, ":"); i != -1 {
hostname = hostname[:i]
}
outputEvent["curl-command"] = curlCommand
outputEvent["ip"] = httpclientpool.Dialer.GetDialedIP(hostname)
outputEvent["redirect-chain"] = tostring.UnsafeToString(redirectedResponse)
for k, v := range previousEvent {
finalEvent[k] = v
}
for k, v := range outputEvent {
finalEvent[k] = v
}
// Add to history the current request number metadata if asked by the user.
if request.ReqCondition {
for k, v := range outputEvent { for k, v := range outputEvent {
key := fmt.Sprintf("%s_%d", k, requestCount) finalEvent[k] = v
previousEvent[key] = v
finalEvent[key] = v
} }
// Add to history the current request number metadata if asked by the user.
if request.ReqCondition {
for k, v := range outputEvent {
key := fmt.Sprintf("%s_%d", k, requestCount)
previousEvent[key] = v
finalEvent[key] = v
}
}
event := eventcreator.CreateEventWithAdditionalOptions(request, finalEvent, request.options.Options.Debug || request.options.Options.DebugResponse, func(internalWrappedEvent *output.InternalWrappedEvent) {
internalWrappedEvent.OperatorsResult.PayloadValues = generatedRequest.meta
})
responseContentType := resp.Header.Get("Content-Type")
dumpResponse(event, request.options, response.fullResponse, formedURL, responseContentType)
callback(event)
} }
event := eventcreator.CreateEventWithAdditionalOptions(request, finalEvent, request.options.Options.Debug || request.options.Options.DebugResponse, func(internalWrappedEvent *output.InternalWrappedEvent) {
internalWrappedEvent.OperatorsResult.PayloadValues = generatedRequest.meta
})
dumpResponse(event, request.options, redirectedResponse, formedURL, responseContentType)
callback(event)
return nil return nil
} }

View File

@ -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
for header, values := range headers { // encoding has been specified by the user in the request so in case we have to
builder.WriteString(header) // manually do it.
builder.WriteString(": ") dataOrig := response.body
response.body, err = handleDecompression(resp, response.body)
for i, value := range values { // in case of error use original data
builder.WriteString(value) if err != nil {
response.body = dataOrig
if i != len(values)-1 {
builder.WriteRune('\n')
builder.WriteString(header)
builder.WriteString(": ")
}
}
builder.WriteRune('\n')
} }
return builder.String() response.fullResponse = bytes.ReplaceAll(response.fullResponse, dataOrig, response.body)
// Decode gbk response content-types
// gb18030 supersedes gb2312
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")
}
// the uncompressed body needs to be decoded to standard utf8
response.body, err = decodegbk(response.body)
if err != nil {
return errors.Wrap(err, "could not gbk decode")
}
}
return nil
} }
// 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

View File

@ -1,12 +1,15 @@
package offlinehttp package offlinehttp
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest"
"net/http/httputil" "net/http/httputil"
"testing" "testing"
"time" "time"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -154,11 +157,30 @@ Server: Google Frontend
} }
t.Run("test-live-response-with-content-length", func(t *testing.T) { t.Run("test-live-response-with-content-length", func(t *testing.T) {
var ts *httptest.Server
router := httprouter.New()
router.GET("/", httprouter.Handle(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Add("Server", "Google Frontend")
fmt.Fprintf(w, "%s", `<!DOCTYPE html>
<html>
<head>
<title>Firing Range</title>
</head>
<body>
<h1>Version 0.48</h1>
<h1>What is the Firing Range?</h1>
<p>
</body>
</html>`)
}))
ts = httptest.NewServer(router)
defer ts.Close()
client := &http.Client{ client := &http.Client{
Timeout: 3 * time.Second, Timeout: 3 * time.Second,
} }
data, err := client.Get("https://golang.org/doc/install") data, err := client.Get(ts.URL)
require.Nil(t, err, "could not dial url") require.Nil(t, err, "could not dial url")
defer data.Body.Close() defer data.Body.Close()

View File

@ -3,13 +3,13 @@ package utils
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"github.com/projectdiscovery/nuclei/v2/pkg/types"
"io/ioutil" "io/ioutil"
"log"
"github.com/projectdiscovery/nuclei/v2/pkg/types"
) )
// AddConfiguredClientCertToRequest adds the client certificate authentication to the tls.Config object and returns it // AddConfiguredClientCertToRequest adds the client certificate authentication to the tls.Config object and returns it
func AddConfiguredClientCertToRequest(tlsConfig *tls.Config, options *types.Options) *tls.Config { func AddConfiguredClientCertToRequest(tlsConfig *tls.Config, options *types.Options) (*tls.Config, error) {
// Build the TLS config with the client certificate if it has been configured with the appropriate options. // Build the TLS config with the client certificate if it has been configured with the appropriate options.
// Only one of the options needs to be checked since the validation checks in main.go ensure that all three // Only one of the options needs to be checked since the validation checks in main.go ensure that all three
// files are set if any of the client certification configuration options are. // files are set if any of the client certification configuration options are.
@ -17,18 +17,18 @@ func AddConfiguredClientCertToRequest(tlsConfig *tls.Config, options *types.Opti
// Load the client certificate using the PEM encoded client certificate and the private key file // Load the client certificate using the PEM encoded client certificate and the private key file
cert, err := tls.LoadX509KeyPair(options.ClientCertFile, options.ClientKeyFile) cert, err := tls.LoadX509KeyPair(options.ClientCertFile, options.ClientKeyFile)
if err != nil { if err != nil {
log.Fatal(err) return nil, err
} }
tlsConfig.Certificates = []tls.Certificate{cert} tlsConfig.Certificates = []tls.Certificate{cert}
// Load the certificate authority PEM certificate into the TLS configuration // Load the certificate authority PEM certificate into the TLS configuration
caCert, err := ioutil.ReadFile(options.ClientCAFile) caCert, err := ioutil.ReadFile(options.ClientCAFile)
if err != nil { if err != nil {
log.Fatal(err) return nil, err
} }
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert) caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool tlsConfig.RootCAs = caCertPool
} }
return tlsConfig return tlsConfig, nil
} }

View File

@ -51,8 +51,7 @@ var DefaultOptions = &types.Options{
Targets: []string{}, Targets: []string{},
TargetsFilePath: "", TargetsFilePath: "",
Output: "", Output: "",
ProxyURL: "", Proxy: []string{},
ProxySocksURL: "",
TemplatesDirectory: "", TemplatesDirectory: "",
TraceLogFile: "", TraceLogFile: "",
Templates: []string{}, Templates: []string{},

15
v2/pkg/types/proxy.go Normal file
View File

@ -0,0 +1,15 @@
package types
const (
HTTP_PROXY_ENV = "HTTP_PROXY"
SOCKS5 = "socks5"
HTTP = "http"
HTTPS = "https"
)
var (
// ProxyURL is the URL for the proxy server
ProxyURL string
// ProxySocksURL is the URL for the proxy socks server
ProxySocksURL string
)

View File

@ -58,10 +58,8 @@ type Options struct {
TargetsFilePath string TargetsFilePath string
// Output is the file to write found results to. // Output is the file to write found results to.
Output string Output string
// ProxyURL is the URL for the proxy server // List of HTTP(s)/SOCKS5 proxy to use (comma separated or file input)
ProxyURL string Proxy goflags.NormalizedStringSlice
// ProxySocksURL is the URL for the proxy socks server
ProxySocksURL string
// TemplatesDirectory is the directory to use for storing templates // TemplatesDirectory is the directory to use for storing templates
TemplatesDirectory string TemplatesDirectory string
// TraceLogFile specifies a file to write with the trace of all requests // TraceLogFile specifies a file to write with the trace of all requests