package main import ( "flag" "fmt" "net" "net/url" "os" "strconv" "strings" "sync" "time" "golang.org/x/net/proxy" log "github.com/sirupsen/logrus" "github.com/RumbleDiscovery/jarm-go" "github.com/RumbleDiscovery/rumble-tools/pkg/rnd" ) // Version is set by the goreleaser build var Version = "dev" var defaultPorts = flag.String("p", "443", "default ports") var workerCount = flag.Int("w", 256, "worker count") var quietMode = flag.Bool("q", false, "quiet mode") var retries = flag.Int("r", 0, "number of times to retry dialing") // ValidPort determines if a port number is valid func ValidPort(pnum int) bool { if pnum < 1 || pnum > 65535 { return false } return true } // CrackPortsWithDefaults turns a comma-delimited port list into an array, handling defaults func CrackPortsWithDefaults(pspec string, defaults []uint16) ([]int, error) { results := []int{} // Use a map to dedup and shuffle ports ports := make(map[int]bool) bits := strings.Split(pspec, ",") for _, bit := range bits { // Support the magic strings "default" and "defaults" if bit == "default" || bit == "defaults" { for _, pnum := range defaults { ports[int(pnum)] = true } continue } // Split based on dash prange := strings.Split(bit, "-") // Scan all ports if the specifier is a single dash if bit == "-" { prange = []string{"1", "65535"} } // No port range if len(prange) == 1 { pnum, err := strconv.Atoi(bit) if err != nil || !ValidPort(pnum) { return results, fmt.Errorf("invalid port %s", bit) } // Record the valid port ports[pnum] = true continue } if len(prange) != 2 { return results, fmt.Errorf("invalid port range %s (%d)", prange, len(prange)) } pstart, err := strconv.Atoi(prange[0]) if err != nil || !ValidPort(pstart) { return results, fmt.Errorf("invalid start port %d", pstart) } pstop, err := strconv.Atoi(prange[1]) if err != nil || !ValidPort(pstop) { return results, fmt.Errorf("invalid stop port %d", pstop) } if pstart > pstop { return results, fmt.Errorf("invalid port range %d-%d", pstart, pstop) } for pnum := pstart; pnum <= pstop; pnum++ { ports[pnum] = true } } // Create the results from the map for port := range ports { results = append(results, port) } return results, nil } // Fingerprint probes a single host/port func Fingerprint(t target, och chan result) { results := []string{} for _, probe := range jarm.GetProbes(t.Host, t.Port) { dialer := proxy.FromEnvironmentUsing(&net.Dialer{Timeout: time.Second * 2}) addr := net.JoinHostPort(t.Host, fmt.Sprintf("%d", t.Port)) c := net.Conn(nil) n := 0 for c == nil && n <= t.Retries { // Ignoring error since error message was already being dropped. // Also, if theres an error, c == nil. if c, _ = dialer.Dial("tcp", addr); c != nil || t.Retries == 0 { break } bo := t.Backoff if bo == nil { bo = DefualtBackoff } time.Sleep(bo(n, t.Retries)) n++ } if c == nil { return } data := jarm.BuildProbe(probe) c.SetWriteDeadline(time.Now().Add(time.Second * 5)) _, err := c.Write(data) if err != nil { results = append(results, "") c.Close() continue } c.SetReadDeadline(time.Now().Add(time.Second * 5)) buff := make([]byte, 1484) c.Read(buff) c.Close() ans, err := jarm.ParseServerHello(buff, probe) if err != nil { results = append(results, "") continue } results = append(results, ans) } och <- result{ Target: t, Hash: jarm.RawHashToFuzzyHash(strings.Join(results, ",")), } } var DefualtBackoff = func(r, m int) time.Duration { return time.Second } type target struct { Host string Port int Retries int Backoff func(r, m int) time.Duration } type result struct { Target target Hash string Error error } func main() { flag.Parse() if len(os.Args) < 2 { log.Fatalf("usage: ./jarm -p [host] ...") } if *workerCount < 1 { log.Fatalf("invalid worker count: %d", *workerCount) } if *quietMode { dn, _ := os.Create(os.DevNull) log.SetOutput(dn) } defaultPorts, err := CrackPortsWithDefaults(*defaultPorts, []uint16{}) if err != nil { log.Fatalf("invalid ports: %s", err) } tch := make(chan target, 1) och := make(chan result, 1) wgo := sync.WaitGroup{} wgt := sync.WaitGroup{} for x := 0; x <= *workerCount; x++ { wgt.Add(1) go func() { defer wgt.Done() for t := range tch { Fingerprint(t, och) } }() } // Output consolidator wgo.Add(1) go func() { defer wgo.Done() for o := range och { if o.Error != nil { log.Printf("failed to scan %s:%d: %s", o.Target.Host, o.Target.Port, o.Error) continue } if len(o.Target.Host) > 24 { fmt.Printf("JARM\t%s:%d\t%s\n", o.Target.Host, o.Target.Port, o.Hash) } else { fmt.Printf("JARM\t%24s:%d\t%s\n", o.Target.Host, o.Target.Port, o.Hash) } } }() // Process targets for _, s := range flag.Args() { t := target{} // Try parsing as a URL first if u, err := url.Parse(s); err == nil { t.Host = u.Hostname() port, _ := strconv.Atoi(u.Port()) t.Port = port } // Next try parsing as an address:port pair if t.Host == "" { host, portStr, _ := net.SplitHostPort(s) port, _ := strconv.Atoi(portStr) t.Host = host t.Port = port } // Next try parsing as a host,port pair if t.Host == "" { bits := strings.SplitN(s, ",", 2) if len(bits) == 2 && bits[0] != "" { t.Host = bits[0] port, _ := strconv.Atoi(bits[1]) t.Port = port } } // Finally try parsing as a host:port pair if t.Host == "" { bits := strings.SplitN(s, ":", 2) if bits[0] != "" { t.Host = bits[0] } if len(bits) == 2 && bits[0] != "" { port, _ := strconv.Atoi(bits[1]) t.Port = port } } hosts := []string{t.Host} for _, host := range hosts { // Support CIDR networks as targets hch := make(chan string, 1) qch := make(chan int, 1) hwg := sync.WaitGroup{} hwg.Add(1) go func() { defer hwg.Done() for thost := range hch { ports := defaultPorts if t.Port != 0 { ports = []int{t.Port} } for _, port := range ports { tch <- target{ Host: thost, Port: port, Retries: *retries, } } } }() // Try to iterate the host as a CIDR range herr := rnd.AddressesFromCIDR(host, hch, qch) // Not a parseable range, handle it as a bare host instead if herr != nil { hch <- host } // Wrap up and wait close(hch) hwg.Wait() close(qch) } } // Wait for scans to complete close(tch) wgt.Wait() // Wait for output to finish close(och) wgo.Wait() }