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  `