Specify arbitrary HTTP headers (#284)

* Add support for specifying arbitrary HTTP headers

* * (Minor, Comment) Fix incorrect comment, replace with more helpful (and accurate) comment
* (Minor, Linting) Rename raw_hash => rawHash, 4 occurences (linter)
* (Minor, Linting) Rename s -> scanner, 1 occurence (linter)
* (Sanity Checking) Prevent duplicate custom headers
* (Sanity Checking) Prevent attempts to set known immutable headers (host, content-length)

* Add --custom-header-delimeter for convenience, in practice, quoting the header values that contain comma can be problematic

* Make the separator consistent for both custom-headers-names and custom-headers-values. It's just weird having them be different :>

* Spelling delimiter correctly would probably help...

* Update modules/http/scanner.go

Co-authored-by: engn33r <engn33r@users.noreply.github.com>

Co-authored-by: Adam Greene <copyright@mzpqnxow.com>
Co-authored-by: Zakir Durumeric <zakird@gmail.com>
Co-authored-by: engn33r <engn33r@users.noreply.github.com>
This commit is contained in:
AG 2021-06-06 14:17:33 -04:00 зафіксовано GitHub
джерело 5e9507cacf
коміт 3c55bbe861
Не вдалося знайти GPG ключ що відповідає даному підпису
Ідентифікатор GPG ключа: 4AEE18F83AFDEB23

@ -11,6 +11,7 @@ import (
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/csv"
"encoding/hex"
"errors"
"fmt"
@ -18,6 +19,7 @@ import (
"net"
"net/url"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
@ -62,6 +64,11 @@ type Flags struct {
// RedirectsSucceed causes the ErrTooManRedirects error to be suppressed
RedirectsSucceed bool `long:"redirects-succeed" description:"Redirects are always a success, even if max-redirects is exceeded"`
// Set arbitrary HTTP headers
CustomHeadersNames string `long:"custom-headers-names" description:"CSV of custom HTTP headers to send to server"`
CustomHeadersValues string `long:"custom-headers-values" description:"CSV of custom HTTP header values to send to server. Should match order of custom-headers-names."`
CustomHeadersDelimiter string `long:"custom-headers-delimiter" description:"Delimiter for customer header name/value CSVs"`
OverrideSH bool `long:"override-sig-hash" description:"Override the default SignatureAndHashes TLS option with more expansive default"`
// ComputeDecodedBodyHashAlgorithm enables computing the body hash later than the default,
@ -90,6 +97,7 @@ type Module struct {
// Scanner is the implementation of the zgrab2.Scanner interface.
type Scanner struct {
config *Flags
customHeaders map[string]string
decodedHashFn func([]byte) string
}
@ -141,6 +149,65 @@ func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error {
fl, _ := flags.(*Flags)
scanner.config = fl
// parse out custom headers at initialization so that they can be easily
// iterated over when constructing individual scanners
if len(fl.CustomHeadersNames) > 0 || len(fl.CustomHeadersValues) > 0 {
if len(fl.CustomHeadersNames) == 0 {
log.Panicf("custom-headers-names must be specified if custom-headers-values is provided")
}
if len(fl.CustomHeadersValues) == 0 {
log.Panicf("custom-headers-values must be specified if custom-headers-names is provided")
}
namesReader := csv.NewReader(strings.NewReader(fl.CustomHeadersNames))
if namesReader == nil {
log.Panicf("unable to read custom-headers-names in CSV reader")
}
valuesReader := csv.NewReader(strings.NewReader(fl.CustomHeadersValues))
if valuesReader == nil {
log.Panicf("unable to read custom-headers-values in CSV reader")
}
// By default, the CSV delimiter will remain a comma unless explicitly specified
if len(fl.CustomHeadersDelimiter) > 1 {
log.Panicf("Invalid delimiter custom-header delimiter, must be a single character")
} else if fl.CustomHeadersDelimiter != "" {
valuesReader.Comma = rune(fl.CustomHeadersDelimiter[0])
namesReader.Comma = rune(fl.CustomHeadersDelimiter[0])
}
headerNames, err := namesReader.Read()
if err != nil {
return err
}
headerValues, err := valuesReader.Read()
if err != nil {
return err
}
if len(headerNames) != len(headerValues) {
log.Panicf("inconsistent number of HTTP header names and values")
}
scanner.customHeaders = make(map[string]string)
for i := 0; i < len(headerNames); i++ {
// The case of header names is normalized to title case later by HTTP library
// explicitly ToLower() to catch duplicates more easily
hName := strings.ToLower(headerNames[i])
switch {
case hName == "host":
log.Panicf("Attempt to set immutable header 'Host', specify this in targets file")
case hName == "user-agent":
log.Panicf("Attempt to set special header 'User-Agent', use --user-agent instead")
case hName == "content-length":
log.Panicf("Attempt to set immutable header 'Content-Length'")
}
// Disallow duplicate headers
_, ok := scanner.customHeaders[hName]
if ok {
log.Panicf("Attempt to set same custom header twice")
}
scanner.customHeaders[hName] = headerValues[i]
}
}
if fl.ComputeDecodedBodyHashAlgorithm == "sha1" {
scanner.decodedHashFn = func(body []byte) string {
rawHash := sha1.Sum(body)
@ -409,8 +476,20 @@ func (scan *scan) Grab() *zgrab2.ScanError {
if err != nil {
return zgrab2.NewScanError(zgrab2.SCAN_UNKNOWN_ERROR, err)
}
// TODO: Headers from input?
request.Header.Set("Accept", "*/*")
// By default, the following headers are *always* set:
// Host, User-Agent, Accept, Accept-Encoding
if scan.scanner.customHeaders != nil {
request.Header.Set("Accept", "*/*")
for k, v := range scan.scanner.customHeaders {
request.Header.Set(k, v)
}
} else {
// If user did not specify custom headers, legacy behavior has always been
// to set the Accept header
request.Header.Set("Accept", "*/*")
}
resp, err := scan.client.Do(request)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()