From cf4dd75b5c1c854d0e2f24fdcef01951a6341eb5 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Fri, 13 Jul 2018 20:17:36 -0400 Subject: [PATCH] use masscan for k8scan Signed-off-by: Jess Frazelle --- k8scan/main.go | 182 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 159 insertions(+), 23 deletions(-) diff --git a/k8scan/main.go b/k8scan/main.go index e74a6cf..3147163 100644 --- a/k8scan/main.go +++ b/k8scan/main.go @@ -11,7 +11,9 @@ import ( "net" "net/http" "os" + "os/exec" "os/signal" + "path/filepath" "strconv" "strings" "sync" @@ -36,8 +38,11 @@ var ( cidr string - defaultPorts = intSlice{80, 443, 8001, 9001} - ports intSlice + defaultPorts = intSlice{80, 443, 8001, 9001} + originalPorts string + ports intSlice + + useMasscan bool mailgunDomain string mailgunAPIKey string @@ -62,6 +67,8 @@ func (i *intSlice) String() (out string) { } func (i *intSlice) Set(value string) error { + originalPorts = value + // Set the default if nothing was given. if len(value) <= 0 { *i = defaultPorts @@ -110,6 +117,8 @@ func init() { flag.StringVar(&cidr, "cidr", defaultCIDR, "IP CIDR to scan") flag.Var(&ports, "ports", fmt.Sprintf("Ports to scan (ex. 80-443 or 80,443,8080 or 1-20,22,80-443) (default %q)", defaultPorts.String())) + flag.BoolVar(&useMasscan, "masscan", true, "Use masscan binary for scanning (this is faster than using pure golang)") + flag.StringVar(&mailgunAPIKey, "mailgun-api-key", "", "Mailgun API Key to use for sending email (optional)") flag.StringVar(&mailgunDomain, "mailgun-domain", "", "Mailgun Domain to use for sending email (optional)") flag.StringVar(&emailRecipient, "email-recipient", "", "Recipient for email notifications (optional)") @@ -122,10 +131,15 @@ func init() { flag.Parse() - // set log level + // Set the log level. if debug { logrus.SetLevel(logrus.DebugLevel) } + + // Set the default ports. + if len(ports) <= 0 { + ports = defaultPorts + } } func main() { @@ -145,29 +159,56 @@ func main() { log.SetFlags(0) log.SetOutput(ioutil.Discard) - logrus.Infof("Scanning for Kubernetes Dashboards and API Servers on %s over port range %#v", cidr, ports) + logrus.Infof("Scanning for Kubernetes Dashboards and API Servers on %s over port range %s", cidr, originalPorts) if len(mailgunDomain) > 0 && len(mailgunAPIKey) > 0 && len(emailRecipient) > 0 { logrus.Infof("Using Mailgun Domain %s, API Key %s to send emails to %s", mailgunDomain, mailgunAPIKey, emailRecipient) } logrus.Infof("This may take a bit...") - startTime := time.Now() + var ( + startTime = time.Now() + wg sync.WaitGroup + ) - ip, ipnet, err := net.ParseCIDR(cidr) - if err != nil { - logrus.Fatal(err) - } + if useMasscan { + m, err := doMasscan() + if err != nil { + logrus.Fatal(err) + } - var wg sync.WaitGroup - for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { - for _, port := range ports { - wg.Add(1) - go func(ip string, port int) { - defer wg.Done() + for _, result := range m { + for _, port := range result.Ports { + wg.Add(1) + go func(ip string, port int) { + defer wg.Done() - scanIP(ip, port) + scanIP(ip, port) - }(ip.String(), port) + }(result.IP, port.Port) + } + } + } else { + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + logrus.Fatal(err) + } + + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + for _, port := range ports { + wg.Add(1) + go func(ip string, port int) { + defer wg.Done() + + // Check if the port is open. + ok := portOpen(ip, port) + if !ok { + return + } + + scanIP(ip, port) + + }(ip.String(), port) + } } } @@ -178,12 +219,6 @@ func main() { } func scanIP(ip string, port int) { - // Check if the port is open. - ok := portOpen(ip, port) - if !ok { - return - } - // Check if it's a kubernetes dashboard. ok, uri := isKubernetesDashboard(ip, port) if !ok { @@ -286,6 +321,58 @@ type OrganizationJSON struct { Reference string `json:"$,omitempty"` } +// MasscanResult holds the masscan results data struct. +// Looks like: +// [ +// { +// "ip": "104.198.238.41", +// "timestamp": "1531524211", +// "ports": [ +// { +// "port": 22, +// "proto": "tcp", +// "status": "open", +// "reason": "syn-ack", +// "ttl": 56 +// } +// ] +// }, +// ... +// ] +type MasscanResult struct { + IP string `json:"ip,omitempty"` + Timestamp MasscanTime `json:"timestamp,omitempty"` + Ports []MasscanPort `json:"ports,omitempty"` +} + +// MasscanPort defines the data struct for a masscan port. +type MasscanPort struct { + Port int `json:"port,omitempty"` + Protocol string `json:"proto,omitempty"` + Status string `json:"status,omitempty"` + Reason string `json:"reason,omitempty"` + TTL int `json:"ttl,omitempty"` +} + +// MasscanTime is the time format returned by masscan. +type MasscanTime struct { + time.Time +} + +// UnmarshalJSON sets MasscanTime correctly from a string. +func (t *MasscanTime) UnmarshalJSON(b []byte) error { + s := strings.Trim(strings.TrimSpace(string(b)), `"`) + + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + + *t = MasscanTime{time.Unix(i, 0)} + + return nil +} + func getIPInfo(ip string) (b ARINResponse, err error) { req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(arinAPIEndpoint, ip), nil) if err != nil { @@ -346,3 +433,52 @@ ARIN: %s return nil } + +func doMasscan() ([]MasscanResult, error) { + // Create a temporary directory for the output. + dir, err := ioutil.TempDir(os.TempDir(), "masscan") + if err != nil { + return nil, fmt.Errorf("creating temporary directory failed: %v", err) + } + defer os.RemoveAll(dir) + + file := filepath.Join(dir, "scan.json") + + cmd := exec.Command("masscan", + fmt.Sprintf("-p%s", ports.String()), + cidr, + "--output-format", "json", + "--output-file", file, + "--rate", "1000000", + "--exclude", "255.255.255.255", + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + logrus.Infof("Running masscan command: `%s`", strings.Join(cmd.Args, " ")) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("running masscan failed: %v", err) + } + + b, err := cleanMasscanOutputFile(file) + if err != nil { + return nil, fmt.Errorf("cleaning up masscan file failed: %v", err) + } + + m := []MasscanResult{} + if err := json.Unmarshal(b, &m); err != nil { + return nil, fmt.Errorf("unmarshal json failed: %v", err) + } + + logrus.Debugf("masscan result: %#v", m) + + return m, nil +} + +func cleanMasscanOutputFile(file string) ([]byte, error) { + b, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + return []byte(strings.TrimSuffix(strings.TrimSpace(string(b)), ",\n]") + "]"), nil +}