diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1338d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..c556fff --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,20 @@ +# .goreleaser.yml +# Build customization +builds: + - binary: nextnet + goos: + - linux + - windows + - darwin + - freebsd + goarch: + - amd64 + - 386 + - mips + - mipsle + - mips64 + - mips64le + - arm + - arm64 + - ppc + - ppc64 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7e5b577 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2018, HD Moore +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dc2a5d4 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +ALL: + @go get github.com/mitchellh/gox && \ + go get -u ./... && \ + go fmt ./... && \ + go vet ./... && \ + go build ./... && \ + go install ./... + + +.PHONY: ALL diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcaee14 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +nextnet +=== + +[![GoDoc](https://godoc.org/github.com/hdm/nextnet?status.svg)](https://godoc.org/github.com/hdm/nextnet) + +nextnet is a pivot point discovery tool written in Go. + +## Download + +Binary packages are available [here](https://github.com/hdm/nextnet/releases/latest). + +## Install + +Most folks should download a compiled binary from the releases page. If you have Go installed, you can build your own binary by running: +``` +$ go get github.com/hdm/nextnet +``` + +## Usage + +``` +Usage: ./nextnet [options] ... + +Probes a list of networks for potential pivot points. + +Options: + + -h, --help Show Usage and exit. + + -rate int + Set the maximum packets per second rate (default 1000) + + -version + Show the application version +``` + +### Local Network Discovery +Identify a multi-homed Windows desktop on the local network. + +``` +$ nextnet 192.168.0.0/24 + +{"host":"192.168.0.112","port":"137","proto":"udp","probe":"netbios","name":"DESKTOP-H14GTIO","nets":["192.168.10.12","192.168.20.12"],"info":{"domain":"WORKGROUP","hwaddr":"14:dd:a9:e4:10:a0"}} + +``` + +### Fast External Network Scans +Quickly identify multi-homed hosts running Netbios on the internet + +``` +$ nextnet -rate 10000 114.80.0.0/16 | grep nets + +{"host":"114.80.62.194","port":"137","proto":"udp","probe":"netbios","name":"WIN-6F47E00F5JS","nets":["192.168.80.2","192.168.90.8"],"info":{"domain":"WORKGROUP","hwaddr":"b8:2a:72:d6:e6:b7"}} +{"host":"114.80.60.40","port":"137","proto":"udp","probe":"netbios","name":"ESX140-2008G","nets":["192.168.11.40","114.80.60.40"],"info":{"domain":"WORKGROUP","hwaddr":"00:0c:29:03:df:5a"}} +{"host":"114.80.86.222","port":"137","proto":"udp","probe":"netbios","name":"SHOFFICE-ISA","nets":["114.80.86.222"],"info":{"domain":"\u0001\u0002__MSBROWSE__\u0002","hwaddr":"00:0c:29:5f:ba:d1"}} +{"host":"114.80.153.49","port":"137","proto":"udp","probe":"netbios","name":"WIN-0GRAGSOGFGS","nets":["114.80.153.49","172.16.0.157"],"info":{"domain":"WORKGROUP","hwaddr":"90:b1:1c:40:72:31"}} +{"host":"114.80.156.143","port":"137","proto":"udp","probe":"netbios","name":"WIN-E1GEEJQBT45","nets":["114.80.156.143","192.168.60.1"],"info":{"domain":"WORKGROUP","hwaddr":"b0:83:fe:e9:3b:d0"}} +{"host":"114.80.157.110","port":"137","proto":"udp","probe":"netbios","name":"SHWGQ-DBWEB","nets":["112.65.248.9"],"info":{"domain":"WORKGROUP","hwaddr":"00:26:55:1e:8c:04"}} +{"host":"114.80.157.108","port":"137","proto":"udp","probe":"netbios","name":"DATA-FCMB","nets":["112.65.248.8"],"info":{"domain":"WORKGROUP","hwaddr":"00:1f:29:64:e5:f4"}} +{"host":"114.80.157.170","port":"137","proto":"udp","probe":"netbios","name":"DZH-TRS6","nets":["10.10.2.13"],"info":{"domain":"WORKGROUP","hwaddr":"ac:16:2d:7a:ff:f0"}} +{"host":"114.80.157.44","port":"137","proto":"udp","probe":"netbios","name":"WINAD2","nets":["169.254.35.57","114.80.157.44"],"info":{"domain":"WINAD02","hwaddr":"00:50:56:9d:4f:e1"}} +{"host":"114.80.156.99","port":"137","proto":"udp","probe":"netbios","name":"WUHAN","nets":["10.1.1.2","169.254.95.120"],"info":{"domain":"WORKGROUP","hwaddr":"34:40:b5:9e:cf:28"}} +{"host":"114.80.166.28","port":"137","proto":"udp","probe":"netbios","name":"KEDE-MOBILE-131","nets":["114.80.166.28"],"info":{"domain":"WORKGROUP","hwaddr":"00:50:56:94:54:5c"}} +{"host":"114.80.167.219","port":"137","proto":"udp","probe":"netbios","name":"WIN-DB2BC7UU0CM","nets":["192.168.100.191"],"info":{"domain":"WORKGROUP","hwaddr":"00:50:56:94:49:7c"}} +{"host":"114.80.157.135","port":"137","proto":"udp","probe":"netbios","name":"WIN-L3DFEEB","nets":["169.254.54.216"],"info":{"domain":"WORKGROUP","hwaddr":"90:b1:1c:09:61:cc"}} +{"host":"114.80.207.27","port":"137","proto":"udp","probe":"netbios","name":"R420","nets":["192.168.126.1","192.168.72.1","169.254.73.205"],"info":{"domain":"WORKGROUP","hwaddr":"44:a8:42:3f:8a:23"}} +{"host":"114.80.215.81","port":"137","proto":"udp","probe":"netbios","name":"WINDOWS-7III9JS","nets":["114.80.215.81","192.168.108.1","192.168.233.1"],"info":{"domain":"WORKGROUP","hwaddr":"6c:ae:8b:38:51:f3"}} +{"host":"114.80.215.90","port":"137","proto":"udp","probe":"netbios","name":"HYDROGEN","nets":["114.80.215.90","2.0.1.1","1.1.1.1","192.168.118.1"],"info":{"domain":"WORKGROUP","hwaddr":"e4:1f:13:95:ee:c2"}} +{"host":"114.80.222.219","port":"137","proto":"udp","probe":"netbios","name":"WIN-VINFEGJ7HP8","nets":["114.80.222.219"],"info":{"domain":"WORKGROUP","hwaddr":"14:18:77:41:17:25"}} +{"host":"114.80.245.193","port":"137","proto":"udp","probe":"netbios","name":"WINDOWS-OT2WS9T","nets":["114.80.245.193"],"info":{"domain":"WORKGROUP","hwaddr":"a0:d3:c1:f2:3e:26"}} +``` diff --git a/ip.go b/ip.go new file mode 100644 index 0000000..e402185 --- /dev/null +++ b/ip.go @@ -0,0 +1,212 @@ +package main + +import ( + "encoding/binary" + "errors" + "fmt" + "math" + "net" + "os" + "regexp" + "strings" +) + +var Match_IPv6 = regexp.MustCompile(`^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$`) + +var Match_IPv4 = regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))$`) + +var IPv4_Masks = map[uint32]uint32{ + 1: 32, + 2: 31, + 4: 30, + 8: 29, + 16: 28, + 32: 27, + 64: 26, + 128: 25, + 256: 24, + 512: 23, + 1024: 22, + 2048: 21, + 4096: 20, + 8192: 19, + 16384: 18, + 32768: 17, + 65536: 16, + 131072: 15, + 262144: 14, + 524288: 13, + 1048576: 12, + 2097152: 11, + 4194304: 10, + 8388608: 9, + 16777216: 8, + 33554432: 7, + 67108864: 6, + 134217728: 5, + 268435456: 4, + 536870912: 3, + 1073741824: 2, + 2147483648: 1, +} + +var IPv4_Mask_Sizes = []uint32{ + 2147483648, + 1073741824, + 536870912, + 268435456, + 134217728, + 67108864, + 33554432, + 16777216, + 8388608, + 4194304, + 2097152, + 1048576, + 524288, + 262144, + 131072, + 65536, + 32768, + 16384, + 8192, + 4096, + 2048, + 1024, + 512, + 256, + 128, + 64, + 32, + 16, + 8, + 4, + 2, + 1, +} + +func IPv4_to_UInt(ips string) (uint32, error) { + ip := net.ParseIP(ips) + if ip == nil { + return 0, errors.New("Invalid IPv4 address") + } + ip = ip.To4() + return binary.BigEndian.Uint32(ip), nil +} + +func UInt_to_IPv4(ipi uint32) string { + ipb := make([]byte, 4) + binary.BigEndian.PutUint32(ipb, ipi) + ip := net.IP(ipb) + return ip.String() +} + +func IPv4Range2CIDRs(s_ip string, e_ip string) ([]string, error) { + + s_i, s_e := IPv4_to_UInt(s_ip) + if s_e != nil { + return []string{}, s_e + } + + e_i, e_e := IPv4_to_UInt(e_ip) + if e_e != nil { + return []string{}, e_e + } + + if s_i > e_i { + return []string{}, errors.New("Start address is bigger than end address") + } + + return IPv4UIntRange2CIDRs(s_i, e_i), nil +} + +func IPv4UIntRange2CIDRs(s_i uint32, e_i uint32) []string { + cidrs := []string{} + + // Ranges are inclusive + size := e_i - s_i + 1 + + if size == 0 { + return cidrs + } + + for i := range IPv4_Mask_Sizes { + + mask_size := IPv4_Mask_Sizes[i] + + if mask_size > size { + continue + } + + // Exact match of the block size + if mask_size == size { + cidrs = append(cidrs, fmt.Sprintf("%s/%d", UInt_to_IPv4(s_i), IPv4_Masks[mask_size])) + break + } + + // Chop off the biggest block that fits + cidrs = append(cidrs, fmt.Sprintf("%s/%d", UInt_to_IPv4(s_i), IPv4_Masks[mask_size])) + s_i = s_i + mask_size + + // Look for additional blocks + new_cidrs := IPv4UIntRange2CIDRs(s_i, e_i) + + // Merge those blocks into out results + for x := range new_cidrs { + cidrs = append(cidrs, new_cidrs[x]) + } + break + + } + return cidrs +} + +func AddressesFromCIDR(cidr string, o chan<- string) { + if len(cidr) == 0 { + return + } + + // We may receive bare IP addresses, not CIDRs sometimes + if !strings.Contains(cidr, "/") { + if strings.Contains(cidr, ":") { + cidr = cidr + "/128" + } else { + cidr = cidr + "/32" + } + } + + // Parse CIDR into base address + mask + ip, net, err := net.ParseCIDR(cidr) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid CIDR %s: %s\n", cidr, err.Error()) + return + } + + // Verify IPv4 for now + ip4 := net.IP.To4() + if ip4 == nil { + fmt.Fprintf(os.Stderr, "Invalid IPv4 CIDR %s\n", cidr) + return + } + + net_base, err := IPv4_to_UInt(net.IP.String()) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid IPv4 Address %s: %s\n", ip.String(), err.Error()) + return + } + + mask_ones, mask_total := net.Mask.Size() + + // Does not work for IPv6 due to cast to uint32 + net_size := uint32(math.Pow(2, float64(mask_total-mask_ones))) + + cur_base := net_base + end_base := net_base + net_size + cur_addr := cur_base + + for cur_addr = cur_base; cur_addr < end_base; cur_addr++ { + o <- UInt_to_IPv4(cur_addr) + } + + return +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..88420aa --- /dev/null +++ b/main.go @@ -0,0 +1,200 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "golang.org/x/time/rate" + "os" + "runtime" + "sync" + "time" +) + +type ScanResult struct { + Host string `json:"host"` + Port string `json:"port,omitempty"` + Proto string `json:"proto,omitempty"` + Probe string `json:"probe,omitempty"` + Name string `json:"name,omitempty"` + Nets []string `json:"nets,omitempty"` + Info map[string]string `json:"info"` +} + +type Prober interface { + Setup() + Initialize() + Wait() + AddTarget(string) + CloseInput() + SetOutput(chan<- ScanResult) + CheckRateLimit() + SetLimiter(*rate.Limiter) +} + +type Probe struct { + name string + options map[string]string + waiter sync.WaitGroup + input chan string + output chan<- ScanResult + limiter *rate.Limiter +} + +func (this *Probe) String() string { + return fmt.Sprintf("%s", this.name) +} + +func (this *Probe) Wait() { + this.waiter.Wait() + return +} + +func (this *Probe) Setup() { + this.name = "generic" + this.input = make(chan string) + return +} + +func (this *Probe) Initialize() { + this.Setup() + this.name = "generic" + return +} + +func (this *Probe) SetOutput(c_out chan<- ScanResult) { + this.output = c_out + return +} + +func (this *Probe) AddTarget(t string) { + this.input <- t + return +} + +func (this *Probe) CloseInput() { + close(this.input) + return +} + +func (this *Probe) SetLimiter(limiter *rate.Limiter) { + this.limiter = limiter + return +} + +func (this *Probe) CheckRateLimit() { + for this.limiter.Allow() == false { + time.Sleep(10 * time.Millisecond) + } +} + +var limiter *rate.Limiter +var ppsrate *int +var probes []Prober +var wi sync.WaitGroup +var wo sync.WaitGroup + +func usage() { + fmt.Println("Usage: " + os.Args[0] + " [cidr] ... [cidr]") + fmt.Println("") + fmt.Println("Probes a list of networks for potential pivot points.") + fmt.Println("") + fmt.Println("Options:") + flag.PrintDefaults() +} + +func outputWriter(o <-chan ScanResult) { + + for found := range o { + j, err := json.Marshal(found) + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshaling result: '%v' : %s\n", found, err) + continue + } + os.Stdout.Write(j) + os.Stdout.Write([]byte("\n")) + } + wo.Done() +} + +func initializeProbes(c_out chan<- ScanResult) { + for _, probe := range probes { + probe.Initialize() + probe.SetOutput(c_out) + probe.SetLimiter(limiter) + } +} + +func waitProbes() { + for _, probe := range probes { + probe.Wait() + } +} + +func processAddress(i <-chan string, o chan<- ScanResult) { + for addr := range i { + for _, probe := range probes { + probe.AddTarget(addr) + } + } + + for _, probe := range probes { + probe.CloseInput() + } + wi.Done() +} + +func main() { + + runtime.GOMAXPROCS(runtime.NumCPU()) + + flag.Usage = func() { usage() } + version := flag.Bool("version", false, "Show the application version") + ppsrate = flag.Int("rate", 1000, "Set the maximum packets per second rate") + + flag.Parse() + + if *version { + PrintVersion("nextnet") + os.Exit(0) + } + + limiter = rate.NewLimiter(rate.Limit(*ppsrate), *ppsrate*3) + + // Input addresses + c_addr := make(chan string) + + // Output structs + c_out := make(chan ScanResult) + + // Configure the probes + initializeProbes(c_out) + + // Launch a single input address processor + wi.Add(1) + go processAddress(c_addr, c_out) + + // Launch a single output writer + wo.Add(1) + go outputWriter(c_out) + + // Parse CIDRs and feed IPs to the input channel + for _, cidr := range flag.Args() { + AddressesFromCIDR(cidr, c_addr) + } + + // Close the cidr input channel + close(c_addr) + + // Wait for the input feed to complete + wi.Wait() + + // Wait for pending probes + waitProbes() + + // Close the output handle + close(c_out) + + // Wait for the output goroutine + wo.Wait() +} diff --git a/probe_netbios.go b/probe_netbios.go new file mode 100644 index 0000000..361776e --- /dev/null +++ b/probe_netbios.go @@ -0,0 +1,385 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + "math/rand" + "net" + "strings" + "time" +) + +const MaxPendingReplies int = 256 +const MaxProbeResponseTime time.Duration = time.Second * 2 + +type NetbiosInfo struct { + statusRecv time.Time + nameSent time.Time + nameRecv time.Time + statusReply NetbiosReplyStatus + nameReply NetbiosReplyStatus +} + +type ProbeNetbios struct { + Probe + socket net.PacketConn + replies map[string]*NetbiosInfo +} + +type NetbiosReplyHeader struct { + XID uint16 + Flags uint16 + QuestionCount uint16 + AnswerCount uint16 + AuthCount uint16 + AdditionalCount uint16 + QuestionName [34]byte + RecordType uint16 + RecordClass uint16 + RecordTTL uint32 + RecordLength uint16 +} + +type NetbiosReplyName struct { + Name [15]byte + Type uint8 + Flag uint16 +} + +type NetbiosReplyAddress struct { + Flag uint16 + Address [4]uint8 +} + +type NetbiosReplyStatus struct { + Header NetbiosReplyHeader + HostName [15]byte + UserName [15]byte + Names []NetbiosReplyName + Addresses []NetbiosReplyAddress + HWAddr string +} + +func (this *ProbeNetbios) ProcessReplies() { + buff := make([]byte, 1500) + + this.replies = make(map[string]*NetbiosInfo) + + for { + rlen, raddr, rerr := this.socket.ReadFrom(buff) + if rerr != nil { + if nerr, ok := rerr.(net.Error); ok && nerr.Timeout() { + log.Printf("probe %s receiver timed out: %s", this, rerr) + continue + } + + // Complain about other error types + log.Printf("probe %s receiver returned error: %s", this, rerr) + return + } + + ip := raddr.(*net.UDPAddr).IP.String() + + reply := this.ParseReply(buff[0 : rlen]) + if len(reply.Names) == 0 && len(reply.Addresses) == 0 { + continue + } + + _, found := this.replies[ip] + if !found { + nbinfo := new(NetbiosInfo) + this.replies[ip] = nbinfo + } + + // Handle status replies by sending a name request + if reply.Header.RecordType == 0x21 { + // log.Printf("probe %s received a status reply of %d bytes from %s", this, rlen, raddr) + this.replies[ip].statusReply = reply + this.replies[ip].statusRecv = time.Now() + + ntime := time.Time{} + if this.replies[ip].nameSent == ntime { + this.replies[ip].nameSent = time.Now() + this.SendNameRequest(ip) + } + } + + // Handle name replies by reporting the result + if reply.Header.RecordType == 0x20 { + // log.Printf("probe %s received a name reply of %d bytes from %s", this, rlen, raddr) + this.replies[ip].nameReply = reply + this.replies[ip].nameRecv = time.Now() + this.ReportResult(ip) + } + } +} + +func (this *ProbeNetbios) SendRequest(ip string, req []byte) { + addr, aerr := net.ResolveUDPAddr("udp", ip+":137") + if aerr != nil { + log.Printf("probe %s failed to resolve %s (%s)", this, ip, aerr) + return + } + + // Retry in case of network buffer congestion + wcnt := 0 + for wcnt = 0; wcnt < 5; wcnt++ { + + this.CheckRateLimit() + + _, werr := this.socket.WriteTo(req, addr) + if werr != nil { + log.Printf("probe %s [%d/%d] failed to send to %s (%s)", this, wcnt+1, 5, ip, werr) + time.Sleep(100 * time.Millisecond) + continue + } + break + } + + // Were we able to send it eventually? + if wcnt == 5 { + log.Printf("probe %s [%d/%d] gave up sending to %s", this, wcnt, 5, ip) + } +} + +func (this *ProbeNetbios) SendStatusRequest(ip string) { + // log.Printf("probe %s is sending a status request to %s", this, ip) + this.SendRequest(ip, this.CreateStatusRequest()) +} + +func (this *ProbeNetbios) SendNameRequest(ip string) { + sreply := this.replies[ip].statusReply + name := TrimName(string(sreply.HostName[:])) + this.SendRequest(ip, this.CreateNameRequest(name)) +} + +func (this *ProbeNetbios) ResultFromIP(ip string) ScanResult { + sreply := this.replies[ip].statusReply + nreply := this.replies[ip].nameReply + + res := ScanResult{ + Host: ip, + Port: "137", + Proto: "udp", + Probe: this.String(), + } + + res.Info = make(map[string]string) + + res.Name = TrimName(string(sreply.HostName[:])) + + if nreply.Header.RecordType == 0x20 { + for _, ainfo := range nreply.Addresses { + + net := fmt.Sprintf("%d.%d.%d.%d", ainfo.Address[0], ainfo.Address[1], ainfo.Address[2], ainfo.Address[3]) + if net == "0.0.0.0" { + continue + } + + res.Nets = append(res.Nets, net) + } + } + + if sreply.HWAddr != "00:00:00:00:00:00" { + res.Info["hwaddr"] = sreply.HWAddr + } + + username := TrimName(string(sreply.UserName[:])) + if len(username) > 0 && username != res.Name { + res.Info["username"] = username + } + + for _, rname := range sreply.Names { + + tname := TrimName(string(rname.Name[:])) + if tname == res.Name { + continue + } + + if rname.Flag&0x0800 != 0 { + continue + } + + res.Info["domain"] = tname + } + + return res +} + +func (this *ProbeNetbios) ReportResult(ip string) { + this.output <- this.ResultFromIP(ip) + delete(this.replies, ip) +} + +func (this *ProbeNetbios) ReportIncompleteResults() { + for ip, _ := range this.replies { + this.ReportResult(ip) + } +} + +func (this *ProbeNetbios) EncodeNetbiosName(name [16]byte) [32]byte { + encoded := [32]byte{} + + for i := 0; i < 16; i++ { + if name[i] == 0 { + encoded[(i*2)+0] = 'C' + encoded[(i*2)+1] = 'A' + } else { + encoded[(i*2)+0] = byte((name[i] / 16) + 0x41) + encoded[(i*2)+1] = byte((name[i] % 16) + 0x41) + } + } + + return encoded +} + +func (this *ProbeNetbios) DecodeNetbiosName(name [32]byte) [16]byte { + decoded := [16]byte{} + + for i := 0; i < 16; i++ { + if name[(i*2)+0] == 'C' && name[(i*2)+1] == 'A' { + decoded[i] = 0 + } else { + decoded[i] = ((name[(i*2)+0] * 16) - 0x41) + (name[(i*2)+1] - 0x41) + } + } + return decoded +} + +func (this *ProbeNetbios) ParseReply(buff []byte) NetbiosReplyStatus { + + resp := NetbiosReplyStatus{} + temp := bytes.NewBuffer(buff) + + binary.Read(temp, binary.BigEndian, &resp.Header) + + if resp.Header.QuestionCount != 0 { + return resp + } + + if resp.Header.AnswerCount == 0 { + return resp + } + + // Names + if resp.Header.RecordType == 0x21 { + var rcnt uint8 + var ridx uint8 + binary.Read(temp, binary.BigEndian, &rcnt) + + for ridx = 0; ridx < rcnt; ridx++ { + name := NetbiosReplyName{} + binary.Read(temp, binary.BigEndian, &name) + resp.Names = append(resp.Names, name) + + if name.Type == 0x20 { + resp.HostName = name.Name + } + + if name.Type == 0x03 { + resp.UserName = name.Name + } + } + + var hwbytes [6]uint8 + binary.Read(temp, binary.BigEndian, &hwbytes) + resp.HWAddr = fmt.Sprintf("%.2x:%.2x:%.2x:%.2x:%.2x:%.2x", + hwbytes[0], hwbytes[1], hwbytes[2], hwbytes[3], hwbytes[4], hwbytes[5], + ) + return resp + } + + // Addresses + if resp.Header.RecordType == 0x20 { + var ridx uint16 + for ridx = 0; ridx < (resp.Header.RecordLength / 6); ridx++ { + addr := NetbiosReplyAddress{} + binary.Read(temp, binary.BigEndian, &addr) + resp.Addresses = append(resp.Addresses, addr) + } + } + + return resp +} + +func (this *ProbeNetbios) CreateStatusRequest() []byte { + return []byte{ + byte(rand.Intn(256)), byte(rand.Intn(256)), + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x43, 0x4b, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01, + } +} + +func (this *ProbeNetbios) CreateNameRequest(name string) []byte { + nbytes := [16]byte{} + copy(nbytes[0:15], []byte(strings.ToUpper(name)[:])) + + req := []byte{ + byte(rand.Intn(256)), byte(rand.Intn(256)), + 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x20, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x00, 0x00, 0x20, 0x00, 0x01, + } + + encoded := this.EncodeNetbiosName(nbytes) + copy(req[13:45], encoded[0:32]) + return req +} + +func (this *ProbeNetbios) Initialize() { + this.Setup() + this.name = "netbios" + this.waiter.Add(1) + + // Open socket + this.socket, _ = net.ListenPacket("udp", "") + + go func() { + go this.ProcessReplies() + + for dip := range this.input { + this.SendStatusRequest(dip) + + // If our pending replies gets > MAX, stop, process, report, clear, resume + if len(this.replies) > MaxPendingReplies { + log.Printf("probe %s is flushing due to maximum replies hit (%d)", this, len(this.replies)) + time.Sleep(MaxProbeResponseTime) + this.ReportIncompleteResults() + } + } + + // Sleep for packet timeout of initial probe + log.Printf("probe %s is waiting for final replies to status probe", this) + time.Sleep(MaxProbeResponseTime) + + // The receiver is sending interface probes in response to status probes + + log.Printf("probe %s is waiting for final replies to interface probe", this) + time.Sleep(MaxProbeResponseTime) + + // Shut down receiver + this.socket.Close() + + // Report any incomplete results (status reply but no name replies) + this.ReportIncompleteResults() + + // Complete + this.waiter.Done() + }() + + return +} + +func init() { + probes = append(probes, new(ProbeNetbios)) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..29e91df --- /dev/null +++ b/utils.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +func PrintVersion(app string) { + var version = "master" + fmt.Fprintf(os.Stderr, "%s v%s\n", app, version) +} + +func TrimName(name string) string { + return strings.TrimSpace(strings.Replace(name, "\x00", "", -1)) +}