Skip to content

Commit ee8287a

Browse files
authored
fix(http): interactsh matching with payloads (#6778)
* fix(http): interactsh matching with `payloads` in parallel execution. Templates using `payloads` with Interactsh matchers failed to detect OAST interactions because the parallel HTTP execution path (used when `payloads` are present) did not register Interactsh request events, unlike the seq path. This caused incoming interactions to lack associated request context, preventing matchers from running and resulting in missed detections. Fix #5485 by wiring `(*interactsh.Client).RequestEvent` registration into the parallel worker goroutine, make sure both execution paths handle Interactsh correlation equally. Signed-off-by: Dwi Siswanto <[email protected]> * test: add interactsh with `payloads` integration Signed-off-by: Dwi Siswanto <[email protected]> * test: disable interactsh-with-payloads Signed-off-by: Dwi Siswanto <[email protected]> --------- Signed-off-by: Dwi Siswanto <[email protected]>
1 parent 9c951a2 commit ee8287a

File tree

5 files changed

+80
-4
lines changed

5 files changed

+80
-4
lines changed

cmd/integration-test/http.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,30 @@ func (h *httpInteractshRequest) Execute(filePath string) error {
163163
return expectResultsCount(results, 1, 2)
164164
}
165165

166+
type httpInteractshWithPayloadsRequest struct{}
167+
168+
// Execute executes a test case and returns an error if occurred
169+
func (h *httpInteractshWithPayloadsRequest) Execute(filePath string) error {
170+
router := httprouter.New()
171+
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
172+
value := r.Header.Get("url")
173+
if value != "" {
174+
if resp, _ := retryablehttp.DefaultClient().Get(value); resp != nil {
175+
_ = resp.Body.Close()
176+
}
177+
}
178+
})
179+
ts := httptest.NewServer(router)
180+
defer ts.Close()
181+
182+
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug)
183+
if err != nil {
184+
return err
185+
}
186+
187+
return expectResultsCount(results, 1, 3)
188+
}
189+
166190
type httpDefaultMatcherCondition struct{}
167191

168192
// Execute executes a test case and returns an error if occurred

cmd/integration-test/interactsh.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import osutils "github.com/projectdiscovery/utils/os"
55
// All Interactsh related testcases
66
var interactshTestCases = []TestCaseInfo{
77
{Path: "protocols/http/interactsh.yaml", TestCase: &httpInteractshRequest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
8+
{Path: "protocols/http/interactsh-with-payloads.yaml", TestCase: &httpInteractshWithPayloadsRequest{}, DisableOn: func() bool { return true }},
89
{Path: "protocols/http/interactsh-stop-at-first-match.yaml", TestCase: &httpInteractshStopAtFirstMatchRequest{}, DisableOn: func() bool { return true }}, // disable this test for now
910
{Path: "protocols/http/default-matcher-condition.yaml", TestCase: &httpDefaultMatcherCondition{}, DisableOn: func() bool { return true }},
1011
{Path: "protocols/http/interactsh-requests-mc-and.yaml", TestCase: &httpInteractshRequestsWithMCAnd{}},
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
id: interactsh-with-payloads
2+
3+
info:
4+
name: Interactsh With Payloads Integration Test
5+
author: dwisiswant0
6+
severity: info
7+
tags: test
8+
9+
http:
10+
- method: GET
11+
path:
12+
- "{{BaseURL}}/?p={{p}}"
13+
headers:
14+
url: 'http://{{interactsh-url}}'
15+
payloads:
16+
p:
17+
- a
18+
- b
19+
- c
20+
matchers:
21+
- type: word
22+
part: interactsh_protocol
23+
words:
24+
- "dns"

pkg/protocols/common/interactsh/interactsh.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,14 @@ func (c *Client) processInteractionForRequest(interaction *server.Interaction, d
200200
} else {
201201
data.Event.SetOperatorResult(result)
202202
}
203+
// ensure payload values are preserved for interactsh-only matches
204+
data.Event.Lock()
205+
if data.Event.OperatorsResult != nil && len(data.Event.OperatorsResult.PayloadValues) == 0 {
206+
if payloads, ok := data.Event.InternalEvent["payloads"].(map[string]interface{}); ok {
207+
data.Event.OperatorsResult.PayloadValues = payloads
208+
}
209+
}
210+
data.Event.Unlock()
203211

204212
data.Event.Lock()
205213
data.Event.Results = data.MakeResultFunc(data.Event)

pkg/protocols/http/request.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,9 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
242242

243243
// bounded worker-pool to avoid spawning one goroutine per payload
244244
type task struct {
245-
req *generatedRequest
246-
updatedInput *contextargs.Context
245+
req *generatedRequest
246+
updatedInput *contextargs.Context
247+
hasInteractMarkers bool
247248
}
248249

249250
var workersWg sync.WaitGroup
@@ -268,11 +269,27 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
268269
continue
269270
}
270271
request.options.RateLimitTake()
272+
hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators)
273+
needsRequestEvent := hasInteractMatchers && request.NeedsRequestCondition()
271274
select {
272275
case <-spmHandler.Done():
273276
spmHandler.Release()
274277
continue
275-
case spmHandler.ResultChan <- request.executeRequest(t.updatedInput, t.req, make(map[string]interface{}), false, wrappedCallback, 0):
278+
case spmHandler.ResultChan <- request.executeRequest(t.updatedInput, t.req, make(map[string]interface{}), hasInteractMatchers, func(event *output.InternalWrappedEvent) {
279+
if (t.hasInteractMarkers || needsRequestEvent) && request.options.Interactsh != nil {
280+
requestData := &interactsh.RequestData{
281+
MakeResultFunc: request.MakeResultEvent,
282+
Event: event,
283+
Operators: request.CompiledOperators,
284+
MatchFunc: request.Match,
285+
ExtractFunc: request.Extract,
286+
}
287+
allOASTUrls := httputils.GetInteractshURLSFromEvent(event.InternalEvent)
288+
allOASTUrls = append(allOASTUrls, t.req.interactshURLs...)
289+
request.options.Interactsh.RequestEvent(sliceutil.Dedupe(allOASTUrls), requestData)
290+
}
291+
wrappedCallback(event)
292+
}, 0):
276293
spmHandler.Release()
277294
}
278295
}
@@ -330,6 +347,7 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
330347
workersWg.Wait()
331348
return err
332349
}
350+
hasInteractMarkers := interactsh.HasMarkers(inputData) || len(generatedHttpRequest.interactshURLs) > 0
333351
if input.MetaInput.Input == "" {
334352
input.MetaInput.Input = generatedHttpRequest.URL()
335353
}
@@ -350,7 +368,7 @@ func (request *Request) executeParallelHTTP(input *contextargs.Context, dynamicV
350368
return nil
351369
}
352370
return multierr.Combine(spmHandler.CombinedResults()...)
353-
case tasks <- task{req: generatedHttpRequest, updatedInput: updatedInput}:
371+
case tasks <- task{req: generatedHttpRequest, updatedInput: updatedInput, hasInteractMarkers: hasInteractMarkers}:
354372
}
355373
request.options.Progress.IncrementRequests()
356374
}
@@ -1047,6 +1065,7 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
10471065
request.pruneSignatureInternalValues(generatedRequest.meta)
10481066

10491067
interimEvent := generators.MergeMaps(generatedRequest.dynamicValues, finalEvent)
1068+
interimEvent["payloads"] = generatedRequest.meta
10501069
// add the request URL pattern to the event BEFORE operators execute
10511070
// so that interactsh events etc can also access it
10521071
if request.options.ExportReqURLPattern {

0 commit comments

Comments
 (0)