Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/protocols/headless/request.go
2070 views
1
package headless
2
3
import (
4
"fmt"
5
"maps"
6
"net/url"
7
"strings"
8
"time"
9
10
"github.com/projectdiscovery/retryablehttp-go"
11
12
"github.com/pkg/errors"
13
14
"github.com/projectdiscovery/gologger"
15
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
16
"github.com/projectdiscovery/nuclei/v3/pkg/output"
17
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
18
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
19
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
20
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
23
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
24
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
25
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
26
"github.com/projectdiscovery/nuclei/v3/pkg/types"
27
urlutil "github.com/projectdiscovery/utils/url"
28
)
29
30
var _ protocols.Request = &Request{}
31
32
const errCouldNotGetHtmlElement = "could not get html element"
33
34
// Type returns the type of the protocol request
35
func (request *Request) Type() templateTypes.ProtocolType {
36
return templateTypes.HeadlessProtocol
37
}
38
39
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
40
func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
41
if request.SelfContained {
42
url, err := extractBaseURLFromActions(request.Steps)
43
if err != nil {
44
return err
45
}
46
input = contextargs.NewWithInput(input.Context(), url)
47
}
48
49
if request.options.Browser.UserAgent() == "" {
50
request.options.Browser.SetUserAgent(request.compiledUserAgent)
51
}
52
53
vars := protocolutils.GenerateVariablesWithContextArgs(input, false)
54
optionVars := generators.BuildPayloadFromOptions(request.options.Options)
55
// add templatecontext variables to varMap
56
if request.options.HasTemplateCtx(input.MetaInput) {
57
vars = generators.MergeMaps(vars, request.options.GetTemplateCtx(input.MetaInput).GetAll())
58
}
59
60
variablesMap := request.options.Variables.Evaluate(vars)
61
vars = generators.MergeMaps(vars, metadata, optionVars, variablesMap, request.options.Constants)
62
63
// check for operator matches by wrapping callback
64
gotmatches := false
65
wrappedCallback := func(results *output.InternalWrappedEvent) {
66
callback(results)
67
if results != nil && results.OperatorsResult != nil {
68
gotmatches = results.OperatorsResult.Matched
69
}
70
}
71
// verify if fuzz elaboration was requested
72
if len(request.Fuzzing) > 0 {
73
return request.executeFuzzingRule(input, vars, previous, wrappedCallback)
74
}
75
if request.generator != nil {
76
iterator := request.generator.NewIterator()
77
for {
78
value, ok := iterator.Value()
79
if !ok {
80
break
81
}
82
if gotmatches && (request.StopAtFirstMatch || request.options.Options.StopAtFirstMatch || request.options.StopAtFirstMatch) {
83
return nil
84
}
85
value = generators.MergeMaps(value, vars)
86
if err := request.executeRequestWithPayloads(input, value, previous, wrappedCallback); err != nil {
87
return err
88
}
89
}
90
} else {
91
value := maps.Clone(vars)
92
if err := request.executeRequestWithPayloads(input, value, previous, wrappedCallback); err != nil {
93
return err
94
}
95
}
96
return nil
97
}
98
99
// This function extracts the base URL from actions.
100
func extractBaseURLFromActions(steps []*engine.Action) (string, error) {
101
for _, action := range steps {
102
if action.ActionType.ActionType == engine.ActionNavigate {
103
navigateURL := action.GetArg("url")
104
url, err := urlutil.Parse(navigateURL)
105
if err != nil {
106
return "", errors.Errorf("could not parse URL '%s': %s", navigateURL, err.Error())
107
}
108
return fmt.Sprintf("%s://%s", url.Scheme, url.Host), nil
109
}
110
}
111
return "", errors.New("no navigation action found")
112
}
113
114
func (request *Request) executeRequestWithPayloads(input *contextargs.Context, payloads map[string]interface{}, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
115
instance, err := request.options.Browser.NewInstance()
116
if err != nil {
117
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
118
request.options.Progress.IncrementFailedRequestsBy(1)
119
return errors.Wrap(err, errCouldNotGetHtmlElement)
120
}
121
defer func() {
122
_ = instance.Close()
123
}()
124
125
instance.SetInteractsh(request.options.Interactsh)
126
127
if _, err := url.Parse(input.MetaInput.Input); err != nil {
128
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
129
request.options.Progress.IncrementFailedRequestsBy(1)
130
return errors.Wrap(err, errCouldNotGetHtmlElement)
131
}
132
options := &engine.Options{
133
Timeout: time.Duration(request.options.Options.PageTimeout) * time.Second,
134
DisableCookie: request.DisableCookie,
135
Options: request.options.Options,
136
}
137
138
if !options.DisableCookie && input.CookieJar == nil {
139
return errors.New("cookie reuse enabled but cookie-jar is nil")
140
}
141
142
out, page, err := instance.Run(input, request.Steps, payloads, options)
143
if err != nil {
144
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), err)
145
request.options.Progress.IncrementFailedRequestsBy(1)
146
return errors.Wrap(err, errCouldNotGetHtmlElement)
147
}
148
defer page.Close()
149
150
reqLog := instance.GetRequestLog()
151
navigatedURL := request.getLastNavigationURLWithLog(reqLog) // also known as matchedURL if there is a match
152
153
request.options.Output.Request(request.options.TemplatePath, input.MetaInput.Input, request.Type().String(), nil)
154
request.options.Progress.IncrementRequests()
155
gologger.Verbose().Msgf("Sent Headless request to %s", navigatedURL)
156
157
reqBuilder := &strings.Builder{}
158
if request.options.Options.Debug || request.options.Options.DebugRequests || request.options.Options.DebugResponse {
159
gologger.Info().Msgf("[%s] Dumped Headless request for %s", request.options.TemplateID, navigatedURL)
160
161
for _, act := range request.Steps {
162
if act.ActionType.ActionType == engine.ActionNavigate {
163
value := act.GetArg("url")
164
if reqLog[value] != "" {
165
_, _ = fmt.Fprintf(reqBuilder, "\tnavigate => %v\n", reqLog[value])
166
} else {
167
_, _ = fmt.Fprintf(reqBuilder, "%v not found in %v\n", value, reqLog)
168
}
169
} else {
170
actStepStr := act.String()
171
_, _ = fmt.Fprintf(reqBuilder, "\t%s\n", actStepStr)
172
}
173
}
174
gologger.Debug().Msg(reqBuilder.String())
175
}
176
177
var responseBody string
178
html, err := page.Page().Element("html")
179
if err == nil {
180
responseBody, _ = html.HTML()
181
}
182
183
header := out.GetOrDefault("header", "").(string)
184
185
// NOTE(dwisiswant0): `status_code` key should be an integer type.
186
// Ref: https://github.com/projectdiscovery/nuclei/pull/5545#discussion_r1721291013
187
statusCode := out.GetOrDefault("status_code", "").(string)
188
189
outputEvent := request.responseToDSLMap(responseBody, header, statusCode, reqBuilder.String(), input.MetaInput.Input, navigatedURL, page.DumpHistory())
190
// add response fields to template context and merge templatectx variables to output event
191
request.options.AddTemplateVars(input.MetaInput, request.Type(), request.ID, outputEvent)
192
if request.options.HasTemplateCtx(input.MetaInput) {
193
outputEvent = generators.MergeMaps(outputEvent, request.options.GetTemplateCtx(input.MetaInput).GetAll())
194
}
195
196
maps.Copy(outputEvent, out)
197
maps.Copy(outputEvent, payloads)
198
199
var event *output.InternalWrappedEvent
200
if len(page.InteractshURLs) == 0 {
201
event = eventcreator.CreateEvent(request, outputEvent, request.options.Options.Debug || request.options.Options.DebugResponse)
202
callback(event)
203
} else if request.options.Interactsh != nil {
204
event = &output.InternalWrappedEvent{InternalEvent: outputEvent}
205
request.options.Interactsh.RequestEvent(page.InteractshURLs, &interactsh.RequestData{
206
MakeResultFunc: request.MakeResultEvent,
207
Event: event,
208
Operators: request.CompiledOperators,
209
MatchFunc: request.Match,
210
ExtractFunc: request.Extract,
211
})
212
}
213
if len(page.InteractshURLs) > 0 {
214
event.UsesInteractsh = true
215
}
216
217
dumpResponse(event, request.options, responseBody, input.MetaInput.Input)
218
shouldStopAtFirstMatch := request.StopAtFirstMatch || request.options.StopAtFirstMatch || request.options.Options.StopAtFirstMatch
219
if shouldStopAtFirstMatch && event.HasOperatorResult() {
220
return types.ErrNoMoreRequests
221
}
222
return nil
223
}
224
225
func dumpResponse(event *output.InternalWrappedEvent, requestOptions *protocols.ExecutorOptions, responseBody string, input string) {
226
if requestOptions.Options.Debug || requestOptions.Options.DebugResponse || requestOptions.Options.StoreResponse {
227
msg := fmt.Sprintf("[%s] Dumped Headless response for %s\n\n", requestOptions.TemplateID, input)
228
if requestOptions.Options.Debug || requestOptions.Options.DebugResponse {
229
resp := responsehighlighter.Highlight(event.OperatorsResult, responseBody, requestOptions.Options.NoColor, false)
230
gologger.Debug().Msgf("%s%s", msg, resp)
231
}
232
if requestOptions.Options.StoreResponse {
233
requestOptions.Output.WriteStoreDebugData(input, requestOptions.TemplateID, "headless", fmt.Sprintf("%s%s", msg, responseBody))
234
}
235
}
236
}
237
238
// executeFuzzingRule executes a fuzzing rule in the template request
239
func (request *Request) executeFuzzingRule(input *contextargs.Context, payloads map[string]interface{}, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
240
// check for operator matches by wrapping callback
241
gotmatches := false
242
fuzzRequestCallback := func(gr fuzz.GeneratedRequest) bool {
243
if gotmatches && (request.StopAtFirstMatch || request.options.Options.StopAtFirstMatch || request.options.StopAtFirstMatch) {
244
return true
245
}
246
newInput := input.Clone()
247
newInput.MetaInput.Input = gr.Request.String()
248
if err := request.executeRequestWithPayloads(newInput, gr.DynamicValues, previous, callback); err != nil {
249
return false
250
}
251
return true
252
}
253
254
if _, err := urlutil.Parse(input.MetaInput.Input); err != nil {
255
return errors.Wrap(err, "could not parse url")
256
}
257
baseRequest, err := retryablehttp.NewRequest("GET", input.MetaInput.Input, nil)
258
if err != nil {
259
return errors.Wrap(err, "could not create base request")
260
}
261
for _, rule := range request.Fuzzing {
262
err := rule.Execute(&fuzz.ExecuteRuleInput{
263
Input: input,
264
Callback: fuzzRequestCallback,
265
Values: payloads,
266
BaseRequest: baseRequest,
267
})
268
if err == types.ErrNoMoreRequests {
269
return nil
270
}
271
if err != nil {
272
return errors.Wrap(err, "could not execute rule")
273
}
274
}
275
return nil
276
}
277
278
// getLastNavigationURL returns last successfully navigated URL
279
func (request *Request) getLastNavigationURLWithLog(reqLog map[string]string) string {
280
for i := len(request.Steps) - 1; i >= 0; i-- {
281
if request.Steps[i].ActionType.ActionType == engine.ActionNavigate {
282
templateURL := request.Steps[i].GetArg("url")
283
if reqLog[templateURL] != "" {
284
return reqLog[templateURL]
285
}
286
}
287
}
288
return ""
289
}
290
291