zgrab2/modules/http/scanner.go
Benjamin VanderSloot 93f30ef698 Use Target Domain name in SNI when using HTTP
Previous addition of GetTLSConfigForTarget (811eb38) did not modify
HTTP module to use SNI. This let to the very cryptic unknown-error:
remote error: internal error. Some servers give Fatal alerts when
they don't get an SNI extension. Discovered on a `Pagely-ARES/1.3.21`
Server
2019-03-01 21:29:22 -05:00

373 lines
11 KiB
Go

// Package http contains the zgrab2 Module implementation for HTTP(S).
//
// The Flags can be configured to perform a specific Method (e.g. "GET") on the
// specified Path (e.g. "/"). If UseHTTPS is true, the scanner uses TLS for the
// initial request. The Result contains the final HTTP response following each
// response in the redirect chain.
package http
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"io"
"net"
"net/url"
"strconv"
"time"
log "github.com/sirupsen/logrus"
"github.com/zmap/zgrab2"
"github.com/zmap/zgrab2/lib/http"
)
var (
// ErrRedirLocalhost is returned when an HTTP redirect points to localhost,
// unless FollowLocalhostRedirects is set.
ErrRedirLocalhost = errors.New("Redirecting to localhost")
// ErrTooManyRedirects is returned when the number of HTTP redirects exceeds
// MaxRedirects.
ErrTooManyRedirects = errors.New("Too many redirects")
)
// Flags holds the command-line configuration for the HTTP scan module.
// Populated by the framework.
//
// TODO: Custom headers?
type Flags struct {
zgrab2.BaseFlags
zgrab2.TLSFlags
Method string `long:"method" default:"GET" description:"Set HTTP request method type"`
Endpoint string `long:"endpoint" default:"/" description:"Send an HTTP request to an endpoint"`
UserAgent string `long:"user-agent" default:"Mozilla/5.0 zgrab/0.x" description:"Set a custom user agent"`
RetryHTTPS bool `long:"retry-https" description:"If the initial request fails, reconnect and try with HTTPS."`
MaxSize int `long:"max-size" default:"256" description:"Max kilobytes to read in response to an HTTP request"`
MaxRedirects int `long:"max-redirects" default:"0" description:"Max number of redirects to follow"`
// FollowLocalhostRedirects overrides the default behavior to return
// ErrRedirLocalhost whenever a redirect points to localhost.
FollowLocalhostRedirects bool `long:"follow-localhost-redirects" description:"Follow HTTP redirects to localhost"`
// UseHTTPS causes the first request to be over TLS, without requiring a
// redirect to HTTPS. It does not change the port used for the connection.
UseHTTPS bool `long:"use-https" description:"Perform an HTTPS connection on the initial host"`
}
// A Results object is returned by the HTTP module's Scanner.Scan()
// implementation.
type Results struct {
// Result is the final HTTP response in the RedirectResponseChain
Response *http.Response `json:"response,omitempty"`
// RedirectResponseChain is non-empty is the scanner follows a redirect.
// It contains all redirect response prior to the final response.
RedirectResponseChain []*http.Response `json:"redirect_response_chain,omitempty"`
}
// Module is an implementation of the zgrab2.Module interface.
type Module struct {
}
// Scanner is the implementation of the zgrab2.Scanner interface.
type Scanner struct {
config *Flags
}
// scan holds the state for a single scan. This may entail multiple connections.
// It is used to implement the zgrab2.Scanner interface.
type scan struct {
connections []net.Conn
scanner *Scanner
target *zgrab2.ScanTarget
transport *http.Transport
client *http.Client
results Results
url string
globalDeadline time.Time
}
// NewFlags returns an empty Flags object.
func (module *Module) NewFlags() interface{} {
return new(Flags)
}
// NewScanner returns a new instance Scanner instance.
func (module *Module) NewScanner() zgrab2.Scanner {
return new(Scanner)
}
// Validate performs any needed validation on the arguments
func (flags *Flags) Validate(args []string) error {
return nil
}
// Help returns module-specific help
func (flags *Flags) Help() string {
return ""
}
// Protocol returns the protocol identifer for the scanner.
func (s *Scanner) Protocol() string {
return "http"
}
// Init initializes the scanner with the given flags
func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error {
fl, _ := flags.(*Flags)
scanner.config = fl
return nil
}
// InitPerSender does nothing in this module.
func (scanner *Scanner) InitPerSender(senderID int) error {
return nil
}
// GetName returns the name defined in the Flags.
func (scanner *Scanner) GetName() string {
return scanner.config.Name
}
// GetTrigger returns the Trigger defined in the Flags.
func (scanner *Scanner) GetTrigger() string {
return scanner.config.Trigger
}
// Cleanup closes any connections that have been opened during the scan
func (scan *scan) Cleanup() {
if scan.connections != nil {
for _, conn := range scan.connections {
defer conn.Close()
}
scan.connections = nil
}
}
// Get a context whose deadline is the earliest of the context's deadline (if it has one) and the
// global scan deadline.
func (scan *scan) withDeadlineContext(ctx context.Context) context.Context {
ctxDeadline, ok := ctx.Deadline()
if !ok || scan.globalDeadline.Before(ctxDeadline) {
ret, _ := context.WithDeadline(ctx, scan.globalDeadline)
return ret
}
return ctx
}
// Dial a connection using the configured timeouts, as well as the global deadline, and on success,
// add the connection to the list of connections to be cleaned up.
func (scan *scan) dialContext(ctx context.Context, net string, addr string) (net.Conn, error) {
dialer := zgrab2.GetTimeoutConnectionDialer(scan.scanner.config.Timeout)
timeoutContext, _ := context.WithTimeout(context.Background(), scan.scanner.config.Timeout)
conn, err := dialer.DialContext(scan.withDeadlineContext(timeoutContext), net, addr)
if err != nil {
return nil, err
}
scan.connections = append(scan.connections, conn)
return conn, nil
}
// getTLSDialer returns a Dial function that connects using the
// zgrab2.GetTLSConnection()
func (scan *scan) getTLSDialer(t *zgrab2.ScanTarget) func(net, addr string) (net.Conn, error) {
return func(net, addr string) (net.Conn, error) {
outer, err := scan.dialContext(context.Background(), net, addr)
if err != nil {
return nil, err
}
tlsConn, err := scan.scanner.config.TLSFlags.GetTLSConnectionForTarget(outer, t)
if err != nil {
return nil, err
}
// lib/http/transport.go fills in the TLSLog in the http.Request instance(s)
err = tlsConn.Handshake()
return tlsConn, err
}
}
// Taken from zgrab/zlib/grabber.go -- check if the URL points to localhost
func redirectsToLocalhost(host string) bool {
if i := net.ParseIP(host); i != nil {
return i.IsLoopback() || i.Equal(net.IPv4zero)
}
if host == "localhost" {
return true
}
if addrs, err := net.LookupHost(host); err == nil {
for _, i := range addrs {
if ip := net.ParseIP(i); ip != nil {
if ip.IsLoopback() || ip.Equal(net.IPv4zero) {
return true
}
}
}
}
return false
}
// Taken from zgrab/zlib/grabber.go -- get a CheckRedirect callback that uses the redirectToLocalhost and MaxRedirects config
func (scan *scan) getCheckRedirect() func(*http.Request, *http.Response, []*http.Request) error {
return func(req *http.Request, res *http.Response, via []*http.Request) error {
if !scan.scanner.config.FollowLocalhostRedirects && redirectsToLocalhost(req.URL.Hostname()) {
return ErrRedirLocalhost
}
scan.results.RedirectResponseChain = append(scan.results.RedirectResponseChain, res)
b := new(bytes.Buffer)
maxReadLen := int64(scan.scanner.config.MaxSize) * 1024
readLen := maxReadLen
if res.ContentLength >= 0 && res.ContentLength < maxReadLen {
readLen = res.ContentLength
}
io.CopyN(b, res.Body, readLen)
res.BodyText = b.String()
if len(res.BodyText) > 0 {
m := sha256.New()
m.Write(b.Bytes())
res.BodySHA256 = m.Sum(nil)
}
if len(via) > scan.scanner.config.MaxRedirects {
return ErrTooManyRedirects
}
return nil
}
}
// Maps URL protocol to the default port for that protocol
var protoToPort = map[string]uint16{
"http": 80,
"https": 443,
}
// getHTTPURL gets the HTTP URL (sans default port) for the given protocol/host/port/endpoint.
func getHTTPURL(https bool, host string, port uint16, endpoint string) string {
var proto string
if https {
proto = "https"
} else {
proto = "http"
}
if protoToPort[proto] == port {
return proto + "://" + host + endpoint
}
return proto + "://" + net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)) + endpoint
}
// NewHTTPScan gets a new Scan instance for the given target
func (scanner *Scanner) newHTTPScan(t *zgrab2.ScanTarget) *scan {
ret := scan{
scanner: scanner,
target: t,
transport: &http.Transport{
Proxy: nil, // TODO: implement proxying
DisableKeepAlives: false,
DisableCompression: false,
MaxIdleConnsPerHost: scanner.config.MaxRedirects,
},
client: http.MakeNewClient(),
globalDeadline: time.Now().Add(scanner.config.Timeout),
}
ret.transport.DialTLS = ret.getTLSDialer(t)
ret.transport.DialContext = ret.dialContext
ret.client.UserAgent = scanner.config.UserAgent
ret.client.CheckRedirect = ret.getCheckRedirect()
ret.client.Transport = ret.transport
ret.client.Jar = nil // Don't send or receive cookies (otherwise use CookieJar)
ret.client.Timeout = scanner.config.Timeout
host := t.Domain
if host == "" {
host = t.IP.String()
}
ret.url = getHTTPURL(scanner.config.UseHTTPS, host, uint16(scanner.config.BaseFlags.Port), scanner.config.Endpoint)
return &ret
}
// Grab performs the HTTP scan -- implementation taken from zgrab/zlib/grabber.go
func (scan *scan) Grab() *zgrab2.ScanError {
// TODO: Allow body?
request, err := http.NewRequest(scan.scanner.config.Method, scan.url, nil)
if err != nil {
return zgrab2.NewScanError(zgrab2.SCAN_UNKNOWN_ERROR, err)
}
// TODO: Headers from input?
request.Header.Set("Accept", "*/*")
resp, err := scan.client.Do(request)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
scan.results.Response = resp
if err != nil {
if urlError, ok := err.(*url.Error); ok {
err = urlError.Err
}
}
if err != nil {
switch err {
case ErrRedirLocalhost:
break
case ErrTooManyRedirects:
return zgrab2.NewScanError(zgrab2.SCAN_APPLICATION_ERROR, err)
default:
return zgrab2.DetectScanError(err)
}
}
buf := new(bytes.Buffer)
maxReadLen := int64(scan.scanner.config.MaxSize) * 1024
readLen := maxReadLen
if resp.ContentLength >= 0 && resp.ContentLength < maxReadLen {
readLen = resp.ContentLength
}
io.CopyN(buf, resp.Body, readLen)
scan.results.Response.BodyText = buf.String()
if len(scan.results.Response.BodyText) > 0 {
m := sha256.New()
m.Write(buf.Bytes())
scan.results.Response.BodySHA256 = m.Sum(nil)
}
return nil
}
// Scan implements the zgrab2.Scanner interface and performs the full scan of
// the target. If the scanner is configured to follow redirects, this may entail
// multiple TCP connections to hosts other than target.
func (scanner *Scanner) Scan(t zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) {
scan := scanner.newHTTPScan(&t)
defer scan.Cleanup()
err := scan.Grab()
if err != nil {
if scanner.config.RetryHTTPS && !scanner.config.UseHTTPS {
scan.Cleanup()
scanner.config.UseHTTPS = true
retry := scanner.newHTTPScan(&t)
defer retry.Cleanup()
retryError := retry.Grab()
if retryError != nil {
return retryError.Unpack(&retry.results)
}
return zgrab2.SCAN_SUCCESS, &retry.results, nil
}
return err.Unpack(&scan.results)
}
return zgrab2.SCAN_SUCCESS, &scan.results, nil
}
// RegisterModule is called by modules/http.go to register this module with the
// zgrab2 framework.
func RegisterModule() {
var module Module
_, err := zgrab2.AddCommand("http", "HTTP Banner Grab", "Grab a banner over HTTP", 80, &module)
if err != nil {
log.Fatal(err)
}
}