diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30135dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cookiescan +*.json diff --git a/README.md b/README.md index 887ea02..e94844d 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,15 @@ $ ./cookiescan ##Usage## ``` $ cookiescan -h + Usage: cookiescan [options] cookiescan -h | --help cookiescan -v | --version Required Arguments: - target: IP Address or Hostname + target: IP Address, Hostname, or CIDR network. May also be a a newline separated + file containing targets. Options: -h --help Show this message. @@ -32,6 +34,6 @@ Options: -c Minimum confidence level to flag port as open. [default: 1] -i Network interface to listen on. -t Timeout in Milliseconds to wait for a connection. [default: 400] - -j Output JSON. + -j Output JSON to file. ``` diff --git a/explode.go b/cmd/explode.go similarity index 100% rename from explode.go rename to cmd/explode.go diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..cdcc8ad --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "log" + "net" + "time" + + "github.com/gosuri/uiprogress" + "github.com/miekg/pcap" + "github.com/tomsteele/cookiescan" +) + +type empty struct{} +type task struct { + ip string + port int +} + +func main() { + var ( + options = parse() + filter = "tcp[13] == 0x11 or tcp[13] == 0x10 or tcp[13] == 0x18" + ) + + h, err := pcap.OpenLive(options.device, int32(320), true, 500) + if err != nil { + log.Fatal(err.Error()) + } + if err = h.SetFilter(filter); err != nil { + log.Fatal(err.Error()) + } + db := cookiescan.NewStore(options.ips) + + var ( + track = make(chan empty) + tasks = make(chan task, options.minconcurrency) + ) + + go func() { + for pkt, r := h.NextEx(); r >= 0; pkt, r = h.NextEx() { + select { + case <-track: + break + default: + if r == 0 { + continue + } + pkt.Decode() + if len(pkt.Headers) < 2 { + continue + } + iphdr, ok := pkt.Headers[0].(*pcap.Iphdr) + if !ok { + continue + } + if len(iphdr.SrcIp) < 4 { + continue + } + ip := fmt.Sprintf("%v.%v.%v.%v", iphdr.SrcIp[0], iphdr.SrcIp[1], iphdr.SrcIp[2], iphdr.SrcIp[3]) + tcphdr, ok := pkt.Headers[1].(*pcap.Tcphdr) + if !ok { + continue + } + db.Add(ip, int(tcphdr.SrcPort), tcphdr.FlagsString()) + } + } + h.Close() + }() + + for i := 0; i < options.minconcurrency; i++ { + go func() { + for tsk := range tasks { + c, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", tsk.ip, tsk.port), options.timeout) + if err != nil { + continue + } + c.Close() + } + }() + } + + uiprogress.Start() + bar := uiprogress.AddBar(len(options.ips) * len(options.services)) + bar.AppendCompleted() + bar.PrependElapsed() + + for _, ip := range options.ips { + for _, p := range options.services { + tasks <- task{ip, p} + bar.Incr() + } + } + + close(tasks) + time.Sleep(time.Duration(2 * time.Second)) + track <- empty{} + close(track) + uiprogress.Stop() + + if options.jsonfile != "" { + db.JSON(options.minconfidence, options.jsonfile) + } + db.Tabbed(options.minconfidence) +} diff --git a/cmd/parse.go b/cmd/parse.go new file mode 100644 index 0000000..2bdd1eb --- /dev/null +++ b/cmd/parse.go @@ -0,0 +1,158 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "net" + "os" + "strconv" + "time" + + "github.com/docopt/docopt-go" + "github.com/miekg/pcap" +) + +const usage = ` + +Usage: + cookiescan [options] + cookiescan -h | --help + cookiescan -v | --version + +Required Arguments: + target: IP Address, Hostname, or CIDR network. May also be a a newline separated + file containing targets. + +Options: + -h --help Show this message. + -v --version Show version. + -p Ex: -p 22; -p 1-65535, -p 80,443. [default: 1-1024] + -g Amount of goroutines to spread connection attempts across. [default: 1000] + -c Minimum confidence level to flag port as open. [default: 1] + -i Network interface to listen on. + -t Timeout in Milliseconds to wait for a connection. [default: 400] + -j Output JSON to file. + +` + +type O struct { + services []int + minconfidence int + minconcurrency int + timeout time.Duration + device string + ips []string + jsonfile string +} + +func parse() *O { + args, err := docopt.Parse(usage, nil, true, "cookiescan 2.0.0", false) + if err != nil { + log.Fatalf("Error parsing usage. Error: %s\n", err.Error()) + } + if err != nil { + log.Fatalf("Error parsing usage. Error: %s\n", err.Error()) + } + o := &O{ + jsonfile: args["-j"].(string), + } + + var lines []string + hostorfile := args[""].(string) + if ok, err := os.Stat(hostorfile); err == nil && ok != nil { + if lines, err = readFileLines(hostorfile); err != nil { + log.Fatalf("Error parsing input file. Error: %s\n", err.Error()) + } + } else { + lines = append(lines, hostorfile) + } + + if o.ips, err = linesToIPList(lines); err != nil { + log.Fatalf("Error parsing targets. Error: %s\n", err.Error()) + } + + if o.services, err = explode(args["-p"].(string)); err != nil { + log.Fatalf("Error parsing port string. Error %s\n", err.Error()) + } + + if o.minconfidence, err = strconv.Atoi(args["-c"].(string)); err != nil { + log.Fatal("Invalid argument for -c.") + } + if o.minconcurrency, err = strconv.Atoi(args["-g"].(string)); err != nil { + log.Fatal("Invalid argument for -g.") + } + + ti, err := strconv.Atoi(args["-t"].(string)) + if err != nil { + log.Fatal("Invalid argument for -t.") + } + o.timeout = time.Duration(ti) * time.Millisecond + + if args["-i"] != nil { + o.device = args["-i"].(string) + } + if o.device == "" { + devs, err := pcap.FindAllDevs() + if err != nil { + log.Fatal("Error finding interfaces. Error: ", err) + } + if len(devs) == 0 { + log.Fatal("No interfaces found. Are you not running as root?") + } + o.device = devs[0].Name + } + + return o +} + +// linesToIPList processes a list of IP addresses or networks in CIDR format. +// Returning a list of all possible IP addresses. +func linesToIPList(lines []string) ([]string, error) { + ipList := []string{} + for _, line := range lines { + if net.ParseIP(line) != nil { + ipList = append(ipList, line) + } else if ip, network, err := net.ParseCIDR(line); err == nil { + for ip := ip.Mask(network.Mask); network.Contains(ip); increaseIP(ip) { + ipList = append(ipList, ip.String()) + } + } else { + return ipList, fmt.Errorf("%s is not an IP Address or CIDR Network", line) + ips, err := net.LookupIP(line) + if err != nil { + return ipList, fmt.Errorf("%s is not a valid hostname", line) + } + ipList = append(ipList, ips[0].String()) + } + } + return ipList, nil +} + +// increases an IP by a single address. +func increaseIP(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + +// readFileLines returns all the lines in a file. +func readFileLines(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + lines := []string{} + scanner := bufio.NewScanner(file) + for scanner.Scan() { + if scanner.Text() == "" { + continue + } + lines = append(lines, scanner.Text()) + } + return lines, scanner.Err() +} diff --git a/db.go b/db.go new file mode 100644 index 0000000..6151cad --- /dev/null +++ b/db.go @@ -0,0 +1,108 @@ +package cookiescan + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "sort" + "text/tabwriter" +) + +type Services []Service + +func (s Services) Len() int { return len(s) } +func (s Services) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s Services) Less(i, j int) bool { return s[i].Port < s[j].Port } + +type Service struct { + Port int `json:"port"` + Service string `json:"service"` + State string `json:"state"` + Confidence int `json:"confidence"` + Reason []string `json:"reason"` +} + +type Store struct { + Hosts map[string][]Service +} + +type Result struct { + Host string `json:"host"` + Services Services `json:"services"` +} + +func (s *Store) Add(ip string, port int, flags string) { + services := s.Hosts[ip] + if services == nil { + return + } + var found bool + for i, service := range services { + if service.Port == port { + services[i].Reason = append(services[i].Reason, flags) + services[i].Confidence = len(services[i].Reason) + found = true + } + } + if !found { + services = append(services, Service{ + Port: port, + Confidence: 1, + State: "open", + Service: "unknown", + Reason: []string{flags}, + }) + } + s.Hosts[ip] = services +} + +func (s *Store) build(confidence int) []Result { + sMap, _ := buildServices() + results := []Result{} + for host, services := range s.Hosts { + result := Result{Host: host} + for i := range services { + if def, ok := sMap[int(services[i].Port)]; ok { + services[i].Service = def + } + if services[i].Confidence >= confidence { + result.Services = append(result.Services, services[i]) + } + } + if len(result.Services) > 0 { + sort.Sort(result.Services) + results = append(results, result) + } + } + return results +} + +func (s *Store) JSON(confidence int, fname string) { + results := s.build(confidence) + j, _ := json.MarshalIndent(results, "", " ") + ioutil.WriteFile(fname, j, 0664) +} + +func (s *Store) Tabbed(confidence int) { + results := s.build(confidence) + w := tabwriter.NewWriter(os.Stdout, 0, 8, 4, ' ', 0) + for _, r := range results { + fmt.Fprintf(w, "\nHost: %s\n", r.Host) + fmt.Fprintln(w, "Port\tState\tService\tConfidence\tReason") + for _, p := range r.Services { + fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%s\n", p.Port, p.State, p.Service, p.Confidence, p.Reason) + } + w.Flush() + } +} + +func NewStore(ips []string) *Store { + db := &Store{ + Hosts: make(map[string][]Service), + } + for _, ip := range ips { + db.Hosts[ip] = []Service{} + } + return db +} diff --git a/main.go b/main.go deleted file mode 100644 index 1e73aee..0000000 --- a/main.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log" - "net" - "os" - "sort" - "strconv" - "text/tabwriter" - "time" - - "github.com/docopt/docopt-go" - "github.com/miekg/pcap" - "github.com/tomsteele/cookiescan/result" -) - -type empty struct{} - -func main() { - args, err := docopt.Parse(usage, nil, true, "cookiescan 1.0.0", false) - if err != nil { - log.Fatal("Error parsing usage. Error: ", err.Error()) - } - host := args[""].(string) - ports, err := explode(args["-p"].(string)) - if err != nil { - log.Fatal(err.Error()) - } - - var ip string - if net.ParseIP(host) == nil { - ips, err := net.LookupIP(host) - if err != nil { - log.Fatal("Could not resolve hostname. Error: ", err.Error()) - } - ip = ips[0].String() - } else { - ip = host - } - - minc, err := strconv.Atoi(args["-c"].(string)) - if err != nil { - log.Fatal("Invalid argument for -c.") - } - concurrency, err := strconv.Atoi(args["-g"].(string)) - if err != nil { - log.Fatal("Invalid argument for -g.") - } - ti, err := strconv.Atoi(args["-t"].(string)) - if err != nil { - log.Fatal("Invalid argument for -t.") - } - timeout := time.Duration(ti) * time.Millisecond - - filter := fmt.Sprintf("src %s and ((tcp[13] == 0x11) or (tcp[13] == 0x10) or (tcp[13] == 0x18))", ip) - var device string - if args["-i"] != nil { - device = args["-i"].(string) - } - if device == "" { - devs, err := pcap.FindAllDevs() - if err != nil { - log.Fatal("Error finding interfaces. Error: ", err) - } - if len(devs) == 0 { - log.Fatal("No interfaces found. Are you not running as root?") - } - device = devs[0].Name - } - - h, err := pcap.OpenLive(device, int32(320), true, 500) - if err != nil { - log.Fatal(err.Error()) - } - if err = h.SetFilter(filter); err != nil { - log.Fatal(err.Error()) - } - - res := make(map[uint16][]string) - tasks := make(chan int, concurrency) - track := make(chan empty) - - go func() { - for pkt, r := h.NextEx(); r >= 0; pkt, r = h.NextEx() { - select { - case <-track: - break - default: - if r == 0 { - continue - } - pkt.Decode() - if len(pkt.Headers) < 2 { - continue - } - t := pkt.Headers[1].(*pcap.Tcphdr) - f := t.FlagsString() - res[t.SrcPort] = append(res[t.SrcPort], f) - } - } - - h.Close() - }() - - for i := 0; i < concurrency; i++ { - go func() { - for p := range tasks { - c, err := net.DialTimeout("tcp", ip+":"+strconv.Itoa(p), timeout) - if err != nil { - continue - } - c.Close() - } - }() - } - - log.Printf("Starting scan of %s.\n", ip) - for _, p := range ports { - tasks <- p - } - close(tasks) - time.Sleep(time.Duration(2 * time.Second)) - track <- empty{} - close(track) - log.Println("Scan complete.") - - services, _ := buildServices() - results := cookiescan.Result{Host: ip} - for k, v := range res { - conf := len(v) - if conf < minc { - continue - } - service := "unknown" - if s, ok := services[int(k)]; ok { - service = s - } - p := cookiescan.Port{Port: int(k), Service: service, State: "open", Confidence: conf, Reason: v} - results.Ports = append(results.Ports, p) - } - sort.Sort(results.Ports) - - if args["-j"].(bool) { - j, _ := json.MarshalIndent(results, "", " ") - fmt.Println(string(j)) - } else { - w := tabwriter.NewWriter(os.Stdout, 0, 8, 4, ' ', 0) - fmt.Fprintln(w, "Port\tState\tService\tConfidence\tReason") - for _, p := range results.Ports { - fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%s\n", p.Port, p.State, p.Service, p.Confidence, p.Reason) - } - w.Flush() - } -} diff --git a/result/result.go b/result/result.go deleted file mode 100644 index b5436f7..0000000 --- a/result/result.go +++ /dev/null @@ -1,20 +0,0 @@ -package cookiescan - -type Result struct { - Host string `json:"host"` - Ports Ports `json:"ports"` -} - -type Port struct { - Port int `json:"port"` - Service string `json:"service"` - State string `json:"state"` - Confidence int `json:"confidence"` - Reason []string `json:"reason"` -} - -type Ports []Port - -func (p Ports) Len() int { return len(p) } -func (p Ports) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -func (p Ports) Less(i, j int) bool { return p[i].Port < p[j].Port } diff --git a/service.go b/services.go similarity index 97% rename from service.go rename to services.go index c2ebb2f..4c2c4ed 100644 --- a/service.go +++ b/services.go @@ -1,4 +1,4 @@ -package main +package cookiescan import ( "bufio" diff --git a/usage.go b/usage.go deleted file mode 100644 index d7e9b58..0000000 --- a/usage.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -const usage = ` - -Usage: - cookiescan [options] - cookiescan -h | --help - cookiescan -v | --version - -Required Arguments: - target: IP Address or Hostname - -Options: - -h --help Show this message. - -v --version Show version. - -p Ex: -p 22; -p 1-65535, -p 80,443. [default: 1-1024] - -g Amount of goroutines to spread connection attempts across. [default: 1000] - -c Minimum confidence level to flag port as open. [default: 1] - -i Network interface to listen on. - -t Timeout in Milliseconds to wait for a connection. [default: 400] - -j Output JSON. - -`