github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/http/operators_test.go (about) 1 package http 2 3 import ( 4 "net/http" 5 "testing" 6 "time" 7 8 "github.com/stretchr/testify/require" 9 10 "github.com/projectdiscovery/nuclei/v2/pkg/model" 11 "github.com/projectdiscovery/nuclei/v2/pkg/model/types/severity" 12 "github.com/projectdiscovery/nuclei/v2/pkg/operators" 13 "github.com/projectdiscovery/nuclei/v2/pkg/operators/extractors" 14 "github.com/projectdiscovery/nuclei/v2/pkg/operators/matchers" 15 "github.com/projectdiscovery/nuclei/v2/pkg/output" 16 "github.com/projectdiscovery/nuclei/v2/pkg/testutils" 17 ) 18 19 func TestResponseToDSLMap(t *testing.T) { 20 options := testutils.DefaultOptions 21 22 testutils.Init(options) 23 templateID := "testing-http" 24 request := &Request{ 25 ID: templateID, 26 Name: "testing", 27 Path: []string{"{{BaseURL}}?test=1"}, 28 Method: HTTPMethodTypeHolder{MethodType: HTTPGet}, 29 } 30 executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ 31 ID: templateID, 32 Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"}, 33 }) 34 err := request.Compile(executerOpts) 35 require.Nil(t, err, "could not compile file request") 36 37 resp := &http.Response{} 38 resp.Header = make(http.Header) 39 resp.Header.Set("Test", "Test-Response") 40 host := "http://example.com/test/" 41 matched := "http://example.com/test/?test=1" 42 43 event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{}) 44 require.Len(t, event, 15, "could not get correct number of items in dsl map") 45 require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp") 46 require.Equal(t, "Test-Response", event["test"], "could not get correct resp for header") 47 } 48 49 func TestHTTPOperatorMatch(t *testing.T) { 50 options := testutils.DefaultOptions 51 52 testutils.Init(options) 53 templateID := "testing-http" 54 request := &Request{ 55 ID: templateID, 56 Name: "testing", 57 Path: []string{"{{BaseURL}}?test=1"}, 58 Method: HTTPMethodTypeHolder{MethodType: HTTPGet}, 59 } 60 executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ 61 ID: templateID, 62 Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"}, 63 }) 64 err := request.Compile(executerOpts) 65 require.Nil(t, err, "could not compile file request") 66 67 resp := &http.Response{} 68 resp.Header = make(http.Header) 69 resp.Header.Set("Test", "Test-Response") 70 host := "http://example.com/test/" 71 matched := "http://example.com/test/?test=1" 72 73 event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{}) 74 require.Len(t, event, 15, "could not get correct number of items in dsl map") 75 require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp") 76 require.Equal(t, "Test-Response", event["test"], "could not get correct resp for header") 77 78 t.Run("valid", func(t *testing.T) { 79 matcher := &matchers.Matcher{ 80 Part: "body", 81 Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, 82 Words: []string{"1.1.1.1"}, 83 } 84 err = matcher.CompileMatchers() 85 require.Nil(t, err, "could not compile matcher") 86 87 isMatched, matched := request.Match(event, matcher) 88 require.True(t, isMatched, "could not match valid response") 89 require.Equal(t, matcher.Words, matched) 90 }) 91 92 t.Run("negative", func(t *testing.T) { 93 matcher := &matchers.Matcher{ 94 Part: "body", 95 Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, 96 Negative: true, 97 Words: []string{"random"}, 98 } 99 err := matcher.CompileMatchers() 100 require.Nil(t, err, "could not compile negative matcher") 101 102 isMatched, matched := request.Match(event, matcher) 103 require.True(t, isMatched, "could not match valid negative response matcher") 104 require.Equal(t, []string{}, matched) 105 }) 106 107 t.Run("invalid", func(t *testing.T) { 108 matcher := &matchers.Matcher{ 109 Part: "body", 110 Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, 111 Words: []string{"random"}, 112 } 113 err := matcher.CompileMatchers() 114 require.Nil(t, err, "could not compile matcher") 115 116 isMatched, matched := request.Match(event, matcher) 117 require.False(t, isMatched, "could match invalid response matcher") 118 require.Equal(t, []string{}, matched) 119 }) 120 121 t.Run("caseInsensitive", func(t *testing.T) { 122 matcher := &matchers.Matcher{ 123 Part: "body", 124 Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, // only applies to word 125 Words: []string{"EXAMPLE DOMAIN"}, 126 CaseInsensitive: true, 127 } 128 err = matcher.CompileMatchers() 129 require.Nil(t, err, "could not compile matcher") 130 131 isMatched, matched := request.Match(event, matcher) 132 require.True(t, isMatched, "could not match valid response") 133 require.Equal(t, []string{"example domain"}, matched) 134 }) 135 } 136 137 func TestHTTPOperatorExtract(t *testing.T) { 138 options := testutils.DefaultOptions 139 140 testutils.Init(options) 141 templateID := "testing-http" 142 request := &Request{ 143 ID: templateID, 144 Name: "testing", 145 Path: []string{"{{BaseURL}}?test=1"}, 146 Method: HTTPMethodTypeHolder{MethodType: HTTPGet}, 147 } 148 executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ 149 ID: templateID, 150 Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"}, 151 }) 152 err := request.Compile(executerOpts) 153 require.Nil(t, err, "could not compile file request") 154 155 resp := &http.Response{} 156 resp.Header = make(http.Header) 157 resp.Header.Set("Test-Header", "Test-Response") 158 host := "http://example.com/test/" 159 matched := "http://example.com/test/?test=1" 160 161 event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{}) 162 require.Len(t, event, 15, "could not get correct number of items in dsl map") 163 require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp") 164 require.Equal(t, "Test-Response", event["test_header"], "could not get correct resp for header") 165 166 t.Run("extract", func(t *testing.T) { 167 extractor := &extractors.Extractor{ 168 Part: "body", 169 Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.RegexExtractor}, 170 Regex: []string{"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+"}, 171 } 172 err = extractor.CompileExtractors() 173 require.Nil(t, err, "could not compile extractor") 174 175 data := request.Extract(event, extractor) 176 require.Greater(t, len(data), 0, "could not extractor valid response") 177 require.Equal(t, map[string]struct{}{"1.1.1.1": {}}, data, "could not extract correct data") 178 }) 179 180 t.Run("kval", func(t *testing.T) { 181 extractor := &extractors.Extractor{ 182 Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.KValExtractor}, 183 KVal: []string{"test_header"}, 184 } 185 err = extractor.CompileExtractors() 186 require.Nil(t, err, "could not compile kval extractor") 187 188 data := request.Extract(event, extractor) 189 require.Greater(t, len(data), 0, "could not extractor kval valid response") 190 require.Equal(t, map[string]struct{}{"Test-Response": {}}, data, "could not extract correct kval data") 191 }) 192 193 t.Run("json", func(t *testing.T) { 194 event["body"] = exampleJSONResponseBody 195 196 t.Run("jq-simple", func(t *testing.T) { 197 extractor := &extractors.Extractor{ 198 Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.JSONExtractor}, 199 JSON: []string{".batters | .batter | .[] | .id"}, 200 } 201 err = extractor.CompileExtractors() 202 require.Nil(t, err, "could not compile json extractor") 203 204 data := request.Extract(event, extractor) 205 require.Greater(t, len(data), 0, "could not extractor json valid response") 206 require.Equal(t, map[string]struct{}{"1001": {}, "1002": {}, "1003": {}, "1004": {}}, data, "could not extract correct json data") 207 }) 208 t.Run("jq-array", func(t *testing.T) { 209 extractor := &extractors.Extractor{ 210 Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.JSONExtractor}, 211 JSON: []string{".array"}, 212 } 213 err = extractor.CompileExtractors() 214 require.Nil(t, err, "could not compile json extractor") 215 216 data := request.Extract(event, extractor) 217 require.Greater(t, len(data), 0, "could not extractor json valid response") 218 require.Equal(t, map[string]struct{}{"[\"hello\",\"world\"]": {}}, data, "could not extract correct json data") 219 }) 220 t.Run("jq-object", func(t *testing.T) { 221 extractor := &extractors.Extractor{ 222 Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.JSONExtractor}, 223 JSON: []string{".batters"}, 224 } 225 err = extractor.CompileExtractors() 226 require.Nil(t, err, "could not compile json extractor") 227 228 data := request.Extract(event, extractor) 229 require.Greater(t, len(data), 0, "could not extractor json valid response") 230 require.Equal(t, map[string]struct{}{"{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"},{\"id\":\"1003\",\"type\":\"Blueberry\"},{\"id\":\"1004\",\"type\":\"Devil's Food\"}]}": {}}, data, "could not extract correct json data") 231 }) 232 }) 233 234 t.Run("caseInsensitive", func(t *testing.T) { 235 event["body"] = exampleResponseBody 236 237 extractor := &extractors.Extractor{ 238 Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.KValExtractor}, 239 KVal: []string{"TEST_HEADER"}, // only applies to KVal 240 CaseInsensitive: true, 241 } 242 err = extractor.CompileExtractors() 243 require.Nil(t, err, "could not compile kval extractor") 244 245 data := request.Extract(event, extractor) 246 require.Greater(t, len(data), 0, "could not extractor kval valid response") 247 require.Equal(t, map[string]struct{}{"test-response": {}}, data, "could not extract correct kval data") 248 }) 249 } 250 251 func TestHTTPMakeResult(t *testing.T) { 252 options := testutils.DefaultOptions 253 254 testutils.Init(options) 255 templateID := "testing-http" 256 request := &Request{ 257 ID: templateID, 258 Name: "testing", 259 Path: []string{"{{BaseURL}}?test=1"}, 260 Method: HTTPMethodTypeHolder{MethodType: HTTPGet}, 261 Operators: operators.Operators{ 262 Matchers: []*matchers.Matcher{{ 263 Name: "test", 264 Part: "body", 265 Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher}, 266 Words: []string{"1.1.1.1"}, 267 }}, 268 Extractors: []*extractors.Extractor{{ 269 Part: "body", 270 Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.RegexExtractor}, 271 Regex: []string{"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+"}, 272 }}, 273 }, 274 } 275 executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ 276 ID: templateID, 277 Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"}, 278 }) 279 err := request.Compile(executerOpts) 280 require.Nil(t, err, "could not compile file request") 281 282 resp := &http.Response{} 283 resp.Header = make(http.Header) 284 resp.Header.Set("Test", "Test-Response") 285 host := "http://example.com/test/" 286 matched := "http://example.com/test/?test=1" 287 288 event := request.responseToDSLMap(resp, host, matched, exampleRawRequest, exampleRawResponse, exampleResponseBody, exampleResponseHeader, 1*time.Second, map[string]interface{}{}) 289 require.Len(t, event, 15, "could not get correct number of items in dsl map") 290 require.Equal(t, exampleRawResponse, event["response"], "could not get correct resp") 291 require.Equal(t, "Test-Response", event["test"], "could not get correct resp for header") 292 293 event["ip"] = "192.169.1.1" 294 finalEvent := &output.InternalWrappedEvent{InternalEvent: event} 295 if request.CompiledOperators != nil { 296 result, ok := request.CompiledOperators.Execute(event, request.Match, request.Extract, false) 297 if ok && result != nil { 298 finalEvent.OperatorsResult = result 299 finalEvent.Results = request.MakeResultEvent(finalEvent) 300 } 301 } 302 require.Equal(t, 1, len(finalEvent.Results), "could not get correct number of results") 303 require.Equal(t, "test", finalEvent.Results[0].MatcherName, "could not get correct matcher name of results") 304 require.Equal(t, "1.1.1.1", finalEvent.Results[0].ExtractedResults[0], "could not get correct extracted results") 305 } 306 307 const exampleRawRequest = `GET / HTTP/1.1 308 Host: example.com 309 Upgrade-Insecure-Requests: 1 310 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36 311 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 312 Accept-Encoding: gzip, deflate 313 Accept-Language: en-US,en;q=0.9,hi;q=0.8 314 If-None-Match: "3147526947+gzip" 315 If-Modified-Since: Thu, 17 Oct 2019 07:18:26 GMT 316 Connection: close 317 318 ` 319 320 const exampleRawResponse = exampleResponseHeader + exampleResponseBody 321 const exampleResponseHeader = ` 322 HTTP/1.1 200 OK 323 Accept-Ranges: bytes 324 Age: 493322 325 Cache-Control: max-age=604800 326 Content-Type: text/html; charset=UTF-8 327 Date: Thu, 04 Feb 2021 12:15:51 GMT 328 Etag: "3147526947+ident" 329 Expires: Thu, 11 Feb 2021 12:15:51 GMT 330 Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT 331 Server: ECS (nyb/1D1C) 332 Vary: Accept-Encoding 333 X-Cache: HIT 334 Content-Length: 1256 335 Connection: close 336 ` 337 338 const exampleResponseBody = ` 339 <!doctype html> 340 <html> 341 <head> 342 <title>Example Domain</title> 343 344 <meta charset="utf-8" /> 345 <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> 346 <meta name="viewport" content="width=device-width, initial-scale=1" /> 347 <style type="text/css"> 348 body { 349 background-color: #f0f0f2; 350 margin: 0; 351 padding: 0; 352 font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 353 354 } 355 div { 356 width: 600px; 357 margin: 5em auto; 358 padding: 2em; 359 background-color: #fdfdff; 360 border-radius: 0.5em; 361 box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02); 362 } 363 a:link, a:visited { 364 color: #38488f; 365 text-decoration: none; 366 } 367 @media (max-width: 700px) { 368 div { 369 margin: 0 auto; 370 width: auto; 371 } 372 } 373 </style> 374 </head> 375 <a>1.1.1.1</a> 376 <body> 377 <div> 378 <h1>Example Domain</h1> 379 <p>This domain is for use in illustrative examples in documents. You may use this 380 domain in literature without prior coordination or asking for permission.</p> 381 <p><a href="https://www.iana.org/domains/example">More information...</a></p> 382 </div> 383 </body> 384 </html> 385 ` 386 387 const exampleJSONResponseBody = ` 388 { 389 "id": "0001", 390 "type": "donut", 391 "name": "Cake", 392 "ppu": 0.55, 393 "array": ["hello", "world"], 394 "batters": { 395 "batter": [ 396 { 397 "id": "1001", 398 "type": "Regular" 399 }, 400 { 401 "id": "1002", 402 "type": "Chocolate" 403 }, 404 { 405 "id": "1003", 406 "type": "Blueberry" 407 }, 408 { 409 "id": "1004", 410 "type": "Devil's Food" 411 } 412 ] 413 }, 414 "topping": [ 415 { 416 "id": "5001", 417 "type": "None" 418 }, 419 { 420 "id": "5002", 421 "type": "Glazed" 422 }, 423 { 424 "id": "5005", 425 "type": "Sugar" 426 }, 427 { 428 "id": "5007", 429 "type": "Powdered Sugar" 430 }, 431 { 432 "id": "5006", 433 "type": "Chocolate with Sprinkles" 434 }, 435 { 436 "id": "5003", 437 "type": "Chocolate" 438 }, 439 { 440 "id": "5004", 441 "type": "Maple" 442 } 443 ] 444 } 445 `