github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/headless/request.go (about) 1 package headless 2 3 import ( 4 "fmt" 5 "net/url" 6 "strings" 7 "time" 8 9 "github.com/pkg/errors" 10 "golang.org/x/exp/maps" 11 12 "github.com/projectdiscovery/gologger" 13 "github.com/projectdiscovery/nuclei/v2/pkg/output" 14 "github.com/projectdiscovery/nuclei/v2/pkg/protocols" 15 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs" 16 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/fuzz" 17 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" 18 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/eventcreator" 19 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/responsehighlighter" 20 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh" 21 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump" 22 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/headless/engine" 23 protocolutils "github.com/projectdiscovery/nuclei/v2/pkg/protocols/utils" 24 templateTypes "github.com/projectdiscovery/nuclei/v2/pkg/templates/types" 25 "github.com/projectdiscovery/nuclei/v2/pkg/types" 26 urlutil "github.com/projectdiscovery/utils/url" 27 ) 28 29 var _ protocols.Request = &Request{} 30 31 const errCouldGetHtmlElement = "could get html element" 32 33 // Type returns the type of the protocol request 34 func (request *Request) Type() templateTypes.ProtocolType { 35 return templateTypes.HeadlessProtocol 36 } 37 38 // ExecuteWithResults executes the protocol requests and returns results instead of writing them. 39 func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, previous output.InternalEvent, callback protocols.OutputEventCallback) error { 40 if request.options.Browser.UserAgent() == "" { 41 request.options.Browser.SetUserAgent(request.compiledUserAgent) 42 } 43 44 vars := protocolutils.GenerateVariablesWithContextArgs(input, false) 45 payloads := generators.BuildPayloadFromOptions(request.options.Options) 46 values := generators.MergeMaps(vars, metadata, payloads) 47 variablesMap := request.options.Variables.Evaluate(values) 48 payloads = generators.MergeMaps(variablesMap, payloads, request.options.Constants) 49 50 // check for operator matches by wrapping callback 51 gotmatches := false 52 wrappedCallback := func(results *output.InternalWrappedEvent) { 53 callback(results) 54 if results != nil && results.OperatorsResult != nil { 55 gotmatches = results.OperatorsResult.Matched 56 } 57 } 58 // verify if fuzz elaboration was requested 59 if len(request.Fuzzing) > 0 { 60 return request.executeFuzzingRule(input, payloads, previous, wrappedCallback) 61 } 62 if request.generator != nil { 63 iterator := request.generator.NewIterator() 64 for { 65 value, ok := iterator.Value() 66 if !ok { 67 break 68 } 69 if gotmatches && (request.StopAtFirstMatch || request.options.Options.StopAtFirstMatch || request.options.StopAtFirstMatch) { 70 return nil 71 } 72 value = generators.MergeMaps(value, payloads) 73 if err := request.executeRequestWithPayloads(input, value, previous, wrappedCallback); err != nil { 74 return err 75 } 76 } 77 } else { 78 value := maps.Clone(payloads) 79 if err := request.executeRequestWithPayloads(input, value, previous, wrappedCallback); err != nil { 80 return err 81 } 82 } 83 return nil 84 } 85 86 func (request *Request) executeRequestWithPayloads(input *contextargs.Context, payloads map[string]interface{}, previous output.InternalEvent, callback protocols.OutputEventCallback) error { 87 instance, err := request.options.Browser.NewInstance() 88 if err != nil { 89 request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err) 90 request.options.Progress.IncrementFailedRequestsBy(1) 91 return errors.Wrap(err, errCouldGetHtmlElement) 92 } 93 defer instance.Close() 94 95 if vardump.EnableVarDump { 96 gologger.Debug().Msgf("Protocol request variables: \n%s\n", vardump.DumpVariables(payloads)) 97 } 98 99 instance.SetInteractsh(request.options.Interactsh) 100 101 if _, err := url.Parse(input.MetaInput.Input); err != nil { 102 request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err) 103 request.options.Progress.IncrementFailedRequestsBy(1) 104 return errors.Wrap(err, errCouldGetHtmlElement) 105 } 106 options := &engine.Options{ 107 Timeout: time.Duration(request.options.Options.PageTimeout) * time.Second, 108 CookieReuse: request.CookieReuse, 109 Options: request.options.Options, 110 } 111 112 if options.CookieReuse && input.CookieJar == nil { 113 return errors.New("cookie-reuse set but cookie-jar is nil") 114 } 115 116 out, page, err := instance.Run(input, request.Steps, payloads, options) 117 if err != nil { 118 request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err) 119 request.options.Progress.IncrementFailedRequestsBy(1) 120 return errors.Wrap(err, errCouldGetHtmlElement) 121 } 122 defer page.Close() 123 124 reqLog := instance.GetRequestLog() 125 navigatedURL := request.getLastNavigationURLWithLog(reqLog) // also known as matchedURL if there is a match 126 127 request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), nil) 128 request.options.Progress.IncrementRequests() 129 gologger.Verbose().Msgf("Sent Headless request to %s", navigatedURL) 130 131 reqBuilder := &strings.Builder{} 132 if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.DebugResponse { 133 gologger.Info().Msgf("[%s] Dumped Headless request for %s", request.options.TemplateID, navigatedURL) 134 135 for _, act := range request.Steps { 136 if act.ActionType.ActionType == engine.ActionNavigate { 137 value := act.GetArg("url") 138 if reqLog[value] != "" { 139 reqBuilder.WriteString(fmt.Sprintf("\tnavigate => %v\n", reqLog[value])) 140 } else { 141 reqBuilder.WriteString(fmt.Sprintf("%v not found in %v\n", value, reqLog)) 142 } 143 } else { 144 actStepStr := act.String() 145 reqBuilder.WriteString("\t" + actStepStr + "\n") 146 } 147 } 148 gologger.Debug().Msgf(reqBuilder.String()) 149 } 150 151 var responseBody string 152 html, err := page.Page().Element("html") 153 if err == nil { 154 responseBody, _ = html.HTML() 155 } 156 157 outputEvent := request.responseToDSLMap(responseBody, out["header"], out["status_code"], reqBuilder.String(), input.MetaInput.Input, navigatedURL, page.DumpHistory()) 158 for k, v := range out { 159 outputEvent[k] = v 160 } 161 for k, v := range payloads { 162 outputEvent[k] = v 163 } 164 165 var event *output.InternalWrappedEvent 166 if len(page.InteractshURLs) == 0 { 167 event = eventcreator.CreateEvent(request, outputEvent, request.options.Options.Debug || request.options.Options.DebugResponse) 168 callback(event) 169 } else if request.options.Interactsh != nil { 170 event = &output.InternalWrappedEvent{InternalEvent: outputEvent} 171 request.options.Interactsh.RequestEvent(page.InteractshURLs, &interactsh.RequestData{ 172 MakeResultFunc: request.MakeResultEvent, 173 Event: event, 174 Operators: request.CompiledOperators, 175 MatchFunc: request.Match, 176 ExtractFunc: request.Extract, 177 }) 178 } 179 if len(page.InteractshURLs) > 0 { 180 event.UsesInteractsh = true 181 } 182 183 dumpResponse(event, request.options, responseBody, input.MetaInput.Input) 184 return nil 185 } 186 187 func dumpResponse(event *output.InternalWrappedEvent, requestOptions *protocols.ExecutorOptions, responseBody string, input string) { 188 cliOptions := requestOptions.Options 189 if cliOptions.Debug || cliOptions.DebugResponse { 190 highlightedResponse := responsehighlighter.Highlight(event.OperatorsResult, responseBody, cliOptions.NoColor, false) 191 gologger.Debug().Msgf("[%s] Dumped Headless response for %s\n\n%s", requestOptions.TemplateID, input, highlightedResponse) 192 } 193 } 194 195 // executeFuzzingRule executes a fuzzing rule in the template request 196 func (request *Request) executeFuzzingRule(input *contextargs.Context, payloads map[string]interface{}, previous output.InternalEvent, callback protocols.OutputEventCallback) error { 197 // check for operator matches by wrapping callback 198 gotmatches := false 199 fuzzRequestCallback := func(gr fuzz.GeneratedRequest) bool { 200 if gotmatches && (request.StopAtFirstMatch || request.options.Options.StopAtFirstMatch || request.options.StopAtFirstMatch) { 201 return true 202 } 203 newInput := input.Clone() 204 newInput.MetaInput.Input = gr.Request.URL.String() 205 if err := request.executeRequestWithPayloads(newInput, gr.DynamicValues, previous, callback); err != nil { 206 return false 207 } 208 return true 209 } 210 211 if _, err := urlutil.Parse(input.MetaInput.Input); err != nil { 212 return errors.Wrap(err, "could not parse url") 213 } 214 for _, rule := range request.Fuzzing { 215 err := rule.Execute(&fuzz.ExecuteRuleInput{ 216 Input: input, 217 Callback: fuzzRequestCallback, 218 Values: payloads, 219 BaseRequest: nil, 220 }) 221 if err == types.ErrNoMoreRequests { 222 return nil 223 } 224 if err != nil { 225 return errors.Wrap(err, "could not execute rule") 226 } 227 } 228 return nil 229 } 230 231 // getLastNavigationURL returns last successfully navigated URL 232 func (request *Request) getLastNavigationURLWithLog(reqLog map[string]string) string { 233 for i := len(request.Steps) - 1; i >= 0; i-- { 234 if request.Steps[i].ActionType.ActionType == engine.ActionNavigate { 235 templateURL := request.Steps[i].GetArg("url") 236 if reqLog[templateURL] != "" { 237 return reqLog[templateURL] 238 } 239 } 240 } 241 return "" 242 }