Skip to content

Commit

Permalink
Merge pull request #240 from zmap/jcody/http-custom-resolver
Browse files Browse the repository at this point in the history
Add the ability to use a custom (fake) DNS resolver
  • Loading branch information
codyprime authored Feb 3, 2020
2 parents d12c70e + 0b6845e commit d9885ed
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 3 deletions.
165 changes: 165 additions & 0 deletions fake_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package zgrab2

import (
"context"
"errors"
"fmt"
"golang.org/x/net/dns/dnsmessage"
"net"
"time"
)

// Fake DNS Resolver, to force a DNS lookup to return a pinned address
// Inspired by the golang net/dnsclient_unix_test.go code
//
// For a given IP, create a new Resolver that wraps a fake
// DNS server. This resolver will always return an IP that
// is represented by "ipstr", for DNS queries of the same
// IP type. Otherwise, it will return a DNS lookup error.
func NewFakeResolver(ipstr string) (*net.Resolver, error) {
ip := net.ParseIP(ipstr)
if len(ip) < 4 {
return nil, fmt.Errorf("Fake resolver can't use non-IP '%s'", ipstr)
}
fDNS := FakeDNSServer{
IP: ip,
}
return &net.Resolver{
PreferGo: true, // Needed to force the use of the Go internal resolver
Dial: fDNS.DialContext,
}, nil
}

type FakeDNSServer struct {
// Any domain name will resolve to this IP. It can be either ipv4 or ipv6
IP net.IP
}

// For a given DNS query, return the hard-coded IP that is part of
// FakeDNSServer.
//
// It will work with either ipv4 or ipv6 addresses; if a TypeA question
// is received, we will only return the IP if what we have to return is
// ipv4. The same for TypeAAAA and ipv6.
func (f *FakeDNSServer) fakeDNS(s string, dmsg dnsmessage.Message) (r dnsmessage.Message, err error) {

r = dnsmessage.Message{
Header: dnsmessage.Header{
ID: dmsg.ID,
Response: true,
},
Questions: dmsg.Questions,
}
ipv6 := f.IP.To16()
ipv4 := f.IP.To4()
switch t := dmsg.Questions[0].Type; {
case t == dnsmessage.TypeA && ipv4 != nil:
var ip [4]byte
copy(ip[:], []byte(ipv4))
r.Answers = []dnsmessage.Resource{
{
Header: dnsmessage.ResourceHeader{
Name: dmsg.Questions[0].Name,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
Length: 4,
},
Body: &dnsmessage.AResource{
A: ip,
},
},
}
case t == dnsmessage.TypeAAAA && ipv4 == nil:
var ip [16]byte
copy(ip[:], []byte(ipv6))
r.Answers = []dnsmessage.Resource{
{
Header: dnsmessage.ResourceHeader{
Name: dmsg.Questions[0].Name,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
Length: 16,
},
Body: &dnsmessage.AAAAResource{
AAAA: ip,
},
},
}
default:
r.Header.RCode = dnsmessage.RCodeNameError
}

return r, nil
}

// This merely wraps a custom net.Conn, that is only good for DNS
// messages
func (f *FakeDNSServer) DialContext(ctx context.Context, network,
address string) (net.Conn, error) {

conn := &fakeDNSPacketConn{
fakeDNSConn: fakeDNSConn{
server: f,
network: network,
address: address,
},
}
return conn, nil
}

type fakeDNSConn struct {
net.Conn
server *FakeDNSServer
network string
address string
dmsg dnsmessage.Message
}

func (fc *fakeDNSConn) Read(b []byte) (int, error) {
resp, err := fc.server.fakeDNS(fc.address, fc.dmsg)
if err != nil {
return 0, err
}

bb := make([]byte, 2, 514)
bb, err = resp.AppendPack(bb)
if err != nil {
return 0, fmt.Errorf("cannot marshal DNS message: %v", err)
}

bb = bb[2:]
if len(b) < len(bb) {
return 0, errors.New("read would fragment DNS message")
}

copy(b, bb)
return len(bb), nil
}

func (fc *fakeDNSConn) Write(b []byte) (int, error) {
if fc.dmsg.Unpack(b) != nil {
return 0, fmt.Errorf("cannot unmarshal DNS message fake %s (%d)", fc.network, len(b))
}
return len(b), nil
}

func (fc *fakeDNSConn) SetDeadline(deadline time.Time) error {
return nil
}

func (fc *fakeDNSConn) Close() error {
return nil
}

type fakeDNSPacketConn struct {
net.PacketConn
fakeDNSConn
}

func (f *fakeDNSPacketConn) SetDeadline(deadline time.Time) error {
return nil
}

func (f *fakeDNSPacketConn) Close() error {
return f.fakeDNSConn.Close()
}
35 changes: 32 additions & 3 deletions modules/http/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,40 @@ func (scan *scan) withDeadlineContext(ctx context.Context) context.Context {

// 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) {
func (scan *scan) dialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
dialer := zgrab2.GetTimeoutConnectionDialer(scan.scanner.config.Timeout)

switch network {
case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
// If the scan is for a specific IP, and a domain name is provided, we
// don't want to just let the http library resolve the domain. Create
// a fake resolver that we will use, that always returns the IP we are
// given to scan.
if scan.target.IP != nil && scan.target.Domain != "" {
host, _, err := net.SplitHostPort(addr)
if err != nil {
log.Errorf("http/scanner.go dialContext: unable to split host:port '%s'", addr)
log.Errorf("No fake resolver, IP address may be incorrect: %s", err)
} else {
// In the case of redirects, we don't want to blindly use the
// IP we were given to scan, however. Only use the fake
// resolver if the domain originally specified for the scan
// target matches the current address being looked up in this
// DialContext.
if host == scan.target.Domain {
resolver, err := zgrab2.NewFakeResolver(scan.target.IP.String())
if err != nil {
return nil, err
}
dialer.Dialer.Resolver = resolver
}
}
}
}

timeoutContext, _ := context.WithTimeout(context.Background(), scan.scanner.config.Timeout)

conn, err := dialer.DialContext(scan.withDeadlineContext(timeoutContext), net, addr)
conn, err := dialer.DialContext(scan.withDeadlineContext(timeoutContext), network, addr)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -210,7 +238,8 @@ func redirectsToLocalhost(host string) bool {
return false
}

// Taken from zgrab/zlib/grabber.go -- get a CheckRedirect callback that uses the redirectToLocalhost and MaxRedirects config
// 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()) {
Expand Down

0 comments on commit d9885ed

Please sign in to comment.