diff --git a/cmd/tlsx/main.go b/cmd/tlsx/main.go index 2fa8e224..bd4138e2 100644 --- a/cmd/tlsx/main.go +++ b/cmd/tlsx/main.go @@ -132,6 +132,7 @@ func readFlags() error { flagSet.CreateGroup("output", "Output", flagSet.StringVarP(&options.OutputFile, "output", "o", "", "file to write output to"), flagSet.BoolVarP(&options.JSON, "json", "j", false, "display json format output"), + flagSet.BoolVar(&options.DisplayDns, "dns", false, "display unique hostname from SSL certificate response"), flagSet.BoolVarP(&options.RespOnly, "resp-only", "ro", false, "display tls response only"), flagSet.BoolVar(&options.Silent, "silent", false, "display silent output"), flagSet.BoolVarP(&options.NoColor, "no-color", "nc", false, "disable colors in cli output"), diff --git a/internal/runner/runner.go b/internal/runner/runner.go index cf10ec04..2c1c8f99 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -2,6 +2,7 @@ package runner import ( "bufio" + "bytes" "net" "net/url" "os" @@ -15,6 +16,7 @@ import ( "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger/formatter" "github.com/projectdiscovery/gologger/levels" + "github.com/projectdiscovery/hmap/store/hybrid" "github.com/projectdiscovery/mapcidr" "github.com/projectdiscovery/mapcidr/asn" "github.com/projectdiscovery/tlsx/pkg/output" @@ -24,10 +26,13 @@ import ( "github.com/projectdiscovery/tlsx/pkg/tlsx/openssl" errorutil "github.com/projectdiscovery/utils/errors" iputil "github.com/projectdiscovery/utils/ip" + mapsutil "github.com/projectdiscovery/utils/maps" sliceutil "github.com/projectdiscovery/utils/slice" updateutils "github.com/projectdiscovery/utils/update" ) +var hostnamesHm *hybrid.HybridMap + // Runner is a client for running the enumeration process type Runner struct { hasStdin bool @@ -108,6 +113,11 @@ func New(options *clients.Options) (*Runner, error) { } runner.outputWriter = outputWriter + hostnamesHm, err = hybrid.New(hybrid.DefaultDiskOptions) + if err != nil { + return nil, errorutil.NewWithErr(err).Msgf("could not create hmap for hostnames") + } + return runner, nil } @@ -147,6 +157,17 @@ func (r *Runner) Execute() error { close(inputs) wg.Wait() + //FIXME: this is a hack to print deduplicated hostnames + if r.options.DisplayDns && !r.options.JSON { + builder := &bytes.Buffer{} + hostnamesHm.Scan(func(k, _ []byte) error { + builder.WriteString(string(k)) + builder.WriteString("\n") + return nil + }) + _, _ = os.Stdout.Write(builder.Bytes()) + } + // Print the stats if auto fallback mode is used if r.options.ScanMode == "auto" { gologger.Info().Msgf("Connections made using crypto/tls: %d, zcrypto/tls: %d, openssl: %d", stats.LoadCryptoTLSConnections(), stats.LoadZcryptoTLSConnections(), stats.LoadOpensslTLSConnections()) @@ -180,12 +201,43 @@ func (r *Runner) processInputElementWorker(inputs chan taskInput, wg *sync.WaitG if err != nil { gologger.Warning().Msgf("Could not connect input %s: %s", task.Address(), err) } - if response != nil { - if err := r.outputWriter.Write(response); err != nil { - gologger.Warning().Msgf("Could not write output %s: %s", task.Address(), err) + + if response == nil { + continue + } + + if r.options.DisplayDns && response.CertificateResponse != nil { + uniqueHostnames := getUniqueHostnamesPerInput(response.CertificateResponse) + response.CertificateResponse.Hostname = uniqueHostnames + for _, hostname := range uniqueHostnames { + _ = hostnamesHm.Set(hostname, nil) + } + if !r.options.JSON { + continue } } + + if err := r.outputWriter.Write(response); err != nil { + gologger.Warning().Msgf("Could not write output %s: %s", task.Address(), err) + continue + } + } +} + +func getUniqueHostnamesPerInput(certResponse *clients.CertificateResponse) []string { + hostnameSet := map[string]struct{}{} + if certResponse.SubjectCN != "" { + hostnameSet[trimWildcardPrefix(certResponse.SubjectCN)] = struct{}{} } + for _, hostname := range certResponse.SubjectAN { + hostnameSet[trimWildcardPrefix(hostname)] = struct{}{} + } + + return mapsutil.GetKeys(hostnameSet) +} + +func trimWildcardPrefix(hostname string) string { + return strings.TrimPrefix(hostname, "*.") } // normalizeAndQueueInputs normalizes the inputs and queues them for execution diff --git a/pkg/tlsx/clients/clients.go b/pkg/tlsx/clients/clients.go index 934aa013..d0aff9a3 100644 --- a/pkg/tlsx/clients/clients.go +++ b/pkg/tlsx/clients/clients.go @@ -62,6 +62,8 @@ type Options struct { Version bool // JSON enables display of JSON output JSON bool + // DisplayDns enables display of unique hostname from SSL certificate response + DisplayDns bool // TLSChain enables printing TLS chain information to output TLSChain bool // Deprecated: AllCiphers exists for historical compatibility and should not be used @@ -269,6 +271,8 @@ type CertificateResponse struct { SubjectOrg []string `json:"subject_org,omitempty"` // SubjectAN is a list of Subject Alternative Names for the certificate SubjectAN []string `json:"subject_an,omitempty"` + // Hostname is list of deduplicated subject_cn + subject_an + Hostname []string `json:"hostname,omitempty"` //Serial is the certificate serial number Serial string `json:"serial,omitempty"` // IssuerDN is the distinguished name for cert