diff --git a/modules/events_stream/events_view_zeroconf.go b/modules/events_stream/events_view_zeroconf.go index 3141c94d..b8a5deb9 100644 --- a/modules/events_stream/events_view_zeroconf.go +++ b/modules/events_stream/events_view_zeroconf.go @@ -7,6 +7,7 @@ import ( "github.com/bettercap/bettercap/v2/modules/zerogod" "github.com/bettercap/bettercap/v2/session" + "github.com/evilsocket/islazy/ops" "github.com/evilsocket/islazy/tui" ) @@ -50,10 +51,11 @@ func (mod *EventsStream) viewZeroConfEvent(output io.Writer, e session.Event) { } */ - fmt.Fprintf(output, "[%s] [%s] %s is browsing for services %s\n", + fmt.Fprintf(output, "[%s] [%s] %s is browsing (%s) for services %s\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), source, + ops.Ternary(event.Query.QR, "RESPONSE", "QUERY"), strings.Join(services, ", "), ) } else { diff --git a/modules/zerogod/zeroconf/connection.go b/modules/zerogod/zeroconf/connection.go index daac9c12..5933da00 100644 --- a/modules/zerogod/zeroconf/connection.go +++ b/modules/zerogod/zeroconf/connection.go @@ -105,9 +105,46 @@ func listMulticastInterfaces() []net.Interface { return nil } for _, ifi := range ifaces { + // not up if (ifi.Flags & net.FlagUp) == 0 { continue } + // localhost + if (ifi.Flags & net.FlagLoopback) != 0 { + continue + } + // not running + if (ifi.Flags & net.FlagRunning) == 0 { + continue + } + // vpn and similar + if (ifi.Flags & net.FlagPointToPoint) != 0 { + continue + } + + // at least one ipv4 address assigned + hasIPv4 := false + if addresses, _ := ifi.Addrs(); addresses != nil { + for _, addr := range addresses { + // ipv4 or ipv4 CIDR + if ip, ipnet, err := net.ParseCIDR(addr.String()); err == nil { + if ip.To4() != nil || ipnet.IP.To4() != nil { + hasIPv4 = true + break + } + } else if ipAddr, ok := addr.(*net.IPAddr); ok { + if ipAddr.IP.To4() != nil { + hasIPv4 = true + break + } + } + } + } + + if !hasIPv4 { + continue + } + if (ifi.Flags & net.FlagMulticast) > 0 { interfaces = append(interfaces, ifi) } diff --git a/modules/zerogod/zeroconf/server.go b/modules/zerogod/zeroconf/server.go index 03cd05b7..433e1bab 100644 --- a/modules/zerogod/zeroconf/server.go +++ b/modules/zerogod/zeroconf/server.go @@ -48,9 +48,10 @@ func Register(instance, service, domain string, port int, text []string, ifaces if err != nil { return nil, fmt.Errorf("could not determine host") } + entry.HostName = strings.ReplaceAll(entry.HostName, ".local", "") } - if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) { + if !strings.HasSuffix(trimDot(entry.HostName), trimDot(entry.Domain)) { entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain)) } @@ -431,7 +432,7 @@ func (s *Server) composeBrowsingAnswers(resp *dns.Msg, ifIndex int) { Ttl: s.ttl, }, Priority: 0, - Weight: 0, + Weight: 0xffff, Port: uint16(s.service.Port), Target: s.service.HostName, } @@ -463,7 +464,7 @@ func (s *Server) composeLookupAnswers(resp *dns.Msg, ttl uint32, ifIndex int, fl Ttl: ttl, }, Priority: 0, - Weight: 0, + Weight: 0xffff, Port: uint16(s.service.Port), Target: s.service.HostName, } @@ -539,7 +540,7 @@ func (s *Server) probe() { Ttl: s.ttl, }, Priority: 0, - Weight: 0, + Weight: 0xffff, Port: uint16(s.service.Port), Target: s.service.HostName, } diff --git a/modules/zerogod/zerogod.go b/modules/zerogod/zerogod.go index 929371bb..ca3c7f7e 100644 --- a/modules/zerogod/zerogod.go +++ b/modules/zerogod/zerogod.go @@ -126,6 +126,10 @@ func NewZeroGod(s *session.Session) *ZeroGod { "", "If an IPP acceptor is started, this setting defines where to save documents received for printing.")) + mod.AddParam(session.NewBoolParameter("zerogod.verbose", + "false", + "Log every mDNS query.")) + return mod } diff --git a/modules/zerogod/zerogod_advertise.go b/modules/zerogod/zerogod_advertise.go index 31187d34..82eb9dd3 100644 --- a/modules/zerogod/zerogod_advertise.go +++ b/modules/zerogod/zerogod_advertise.go @@ -103,9 +103,7 @@ func (mod *ZeroGod) startAdvertiser(fileName string) error { if err != nil { return fmt.Errorf("could not get hostname: %v", err) } - if !strings.HasSuffix(hostName, ".") { - hostName += "." - } + hostName = strings.ReplaceAll(hostName, ".local", "") data, err := ioutil.ReadFile(fileName) if err != nil { diff --git a/modules/zerogod/zerogod_discovery.go b/modules/zerogod/zerogod_discovery.go index 2beefc98..989f51e7 100644 --- a/modules/zerogod/zerogod_discovery.go +++ b/modules/zerogod/zerogod_discovery.go @@ -1,6 +1,7 @@ package zerogod import ( + "fmt" "net" "strings" "time" @@ -8,6 +9,7 @@ import ( "github.com/bettercap/bettercap/v2/modules/zerogod/zeroconf" "github.com/bettercap/bettercap/v2/network" "github.com/bettercap/bettercap/v2/session" + "github.com/evilsocket/islazy/ops" "github.com/evilsocket/islazy/tui" "github.com/google/gopacket" "github.com/google/gopacket/layers" @@ -81,6 +83,119 @@ func (mod *ZeroGod) onServiceDiscovered(svc *zeroconf.ServiceEntry) { session.I.Refresh() } +func (mod *ZeroGod) DNSResourceRecord2String(rr *layers.DNSResourceRecord) string { + + if rr.Type == layers.DNSTypeOPT { + opts := make([]string, len(rr.OPT)) + for i, opt := range rr.OPT { + opts[i] = opt.String() + } + return "OPT " + strings.Join(opts, ",") + } + if rr.Type == layers.DNSTypeURI { + return fmt.Sprintf("URI %d %d %s", rr.URI.Priority, rr.URI.Weight, string(rr.URI.Target)) + } + /* + https://www.rfc-editor.org/rfc/rfc6762 + + Note that the cache-flush bit is NOT part of the resource record + class. The cache-flush bit is the most significant bit of the second + 16-bit word of a resource record in a Resource Record Section of a + Multicast DNS message (the field conventionally referred to as the + rrclass field), and the actual resource record class is the least + significant fifteen bits of this field. There is no Multicast DNS + resource record class 0x8001. The value 0x8001 in the rrclass field + of a resource record in a Multicast DNS response message indicates a + resource record with class 1, with the cache-flush bit set. When + receiving a resource record with the cache-flush bit set, + implementations should take care to mask off that bit before storing + the resource record in memory, or otherwise ensure that it is given + the correct semantic interpretation. + */ + + if rr.Class == layers.DNSClassIN || rr.Class == 0x8001 { + switch rr.Type { + case layers.DNSTypeA, layers.DNSTypeAAAA: + return rr.IP.String() + case layers.DNSTypeNS: + return "NS " + string(rr.NS) + case layers.DNSTypeCNAME: + return "CNAME " + string(rr.CNAME) + case layers.DNSTypePTR: + return "PTR " + string(rr.PTR) + case layers.DNSTypeTXT: + return "TXT \n" + Dump(rr.TXT) + case layers.DNSTypeSRV: + return fmt.Sprintf("SRV priority=%d weight=%d port=%d name=%s", + rr.SRV.Priority, + rr.SRV.Weight, + rr.SRV.Port, + string(rr.SRV.Name)) + case 47: // NSEC + return "NSEC" + } + } + + return fmt.Sprintf("<%v (%d), %v (%d)>", rr.Class, rr.Class, rr.Type, rr.Type) +} + +func (mod *ZeroGod) logDNS(src net.IP, dns layers.DNS, isLocal bool) { + source := tui.Yellow(src.String()) + if endpoint := mod.Session.Lan.GetByIp(src.String()); endpoint != nil { + if endpoint.Alias != "" { + source = tui.Bold(endpoint.Alias) + } else if endpoint.Hostname != "" { + source = tui.Bold(endpoint.Hostname) + } else if endpoint.Vendor != "" { + source = fmt.Sprintf("%s (%s)", tui.Bold(endpoint.IpAddress), tui.Dim(endpoint.Vendor)) + } + } + + desc := fmt.Sprintf("DNS op=%s %s from %s (r_code=%s)", + dns.OpCode.String(), + tui.Bold(ops.Ternary(dns.QR, "RESPONSE", "QUERY").(string)), + source, + dns.ResponseCode.String()) + + attrs := []string{} + if dns.AA { + attrs = append(attrs, "AA") + } + if dns.TC { + attrs = append(attrs, "TC") + } + if dns.RD { + attrs = append(attrs, "RD") + } + if dns.RA { + attrs = append(attrs, "RA") + } + if len(attrs) > 0 { + desc += " [" + strings.Join(attrs, ", ") + "]" + } + + desc += " :\n" + + for _, q := range dns.Questions { + desc += fmt.Sprintf(" Q: %s\n", q) + } + for _, a := range dns.Answers { + desc += fmt.Sprintf(" A: %s\n", mod.DNSResourceRecord2String(&a)) + } + for _, a := range dns.Authorities { + desc += fmt.Sprintf(" AU: %s\n", mod.DNSResourceRecord2String(&a)) + } + for _, a := range dns.Additionals { + desc += fmt.Sprintf(" AD: %s\n", mod.DNSResourceRecord2String(&a)) + } + + if isLocal { + desc = tui.Dim(desc) + } + + mod.Info("%s", desc) +} + func (mod *ZeroGod) onPacket(pkt gopacket.Packet) { mod.Debug("%++v", pkt) @@ -105,12 +220,6 @@ func (mod *ZeroGod) onPacket(pkt gopacket.Packet) { return } - // not interested in packet generated by us - if srcIP.Equal(mod.Session.Interface.IP) || srcIP.Equal(mod.Session.Interface.IPv6) { - mod.Debug("skipping local packet") - return - } - udp := pkt.Layer(layers.LayerTypeUDP) if udp == nil { mod.Warning("not udp layer in packet %+v", pkt) @@ -123,6 +232,18 @@ func (mod *ZeroGod) onPacket(pkt gopacket.Packet) { return } + isLocal := srcIP.Equal(mod.Session.Interface.IP) || srcIP.Equal(mod.Session.Interface.IPv6) + + if _, verbose := mod.BoolParam("zerogod.verbose"); verbose { + mod.logDNS(srcIP, dns, isLocal) + } + + // not interested in packet generated by us + if isLocal { + mod.Debug("skipping local packet") + return + } + // since the browser is already checking for these, we are only interested in queries numQs := len(dns.Questions) if numQs == 0 {