-
Notifications
You must be signed in to change notification settings - Fork 80
/
cero.go
203 lines (176 loc) · 4.97 KB
/
cero.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package main
import (
"bufio"
"crypto/tls"
"flag"
"fmt"
"net"
"os"
"strings"
"sync"
"time"
)
/* result of processing a domain name */
type procResult struct {
addr string
names []string
err error
}
// run parameters (filled from CLI arguments)
var (
verbose bool
concurrency int
defaultPorts []string
timeout int
onlyValidDomainNames bool
)
var usage = "" +
`usage: cero [options] [targets]
if [targets] not provided in commandline arguments, will read from stdin
`
func main() {
// parse CLI arguments
var ports string
flag.BoolVar(&verbose, "v", false, `Be verbose: Output results as 'addr -- [result list]', output errors to stderr as 'addr -- error message'`)
flag.IntVar(&concurrency, "c", 100, "Concurrency level")
flag.StringVar(&ports, "p", "443", "TLS ports to use, if not specified explicitly in host address. Use comma-separated list")
flag.IntVar(&timeout, "t", 4, "TLS Connection timeout in seconds")
flag.BoolVar(&onlyValidDomainNames, "d", false, "Output only valid domain names (e.g. strip IPs, wildcard domains and gibberish)")
// set custom usage text
flag.Usage = func() {
fmt.Fprintln(os.Stderr, usage)
fmt.Fprintln(os.Stderr, "options:")
flag.PrintDefaults()
}
flag.Parse()
// parse default port list into string slice
defaultPorts = strings.Split(ports, `,`)
// channels
chanInput := make(chan string)
chanResult := make(chan *procResult)
// a common dialer
dialer := &net.Dialer{
Timeout: time.Duration(timeout) * time.Second,
}
// create and start concurrent workers
var workersWG sync.WaitGroup
for i := 0; i < concurrency; i++ {
workersWG.Add(1)
go func() {
for addr := range chanInput {
result := &procResult{addr: addr}
result.names, result.err = grabCert(addr, dialer, onlyValidDomainNames)
chanResult <- result
}
workersWG.Done()
}()
}
// close result channel when workers are done
go func() {
workersWG.Wait()
close(chanResult)
}()
// create and start result-processing worker
var outputWG sync.WaitGroup
outputWG.Add(1)
go func() {
for result := range chanResult {
// in verbose mode, print all errors and results, with corresponding input values
if verbose {
if result.err != nil {
fmt.Fprintf(os.Stderr, "%s -- %s\n", result.addr, result.err)
} else {
fmt.Fprintf(os.Stdout, "%s -- %s\n", result.addr, result.names)
}
} else {
// non-verbose: just print scraped names, one at line
for _, name := range result.names {
fmt.Fprintln(os.Stdout, name)
}
}
}
outputWG.Done()
}()
// consume output to start things moving
if len(flag.Args()) > 0 {
for _, addr := range flag.Args() {
processInputItem(addr, chanInput, chanResult)
}
} else {
// every line of stdin is considered as a input
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
addr := strings.TrimSpace(sc.Text())
processInputItem(addr, chanInput, chanResult)
}
}
// close input channel when input fully consumed
close(chanInput)
// wait for processing to finish
outputWG.Wait()
}
// process input item
// if orrors occur during parsing, they are pushed straight to result channel
func processInputItem(input string, chanInput chan string, chanResult chan *procResult) {
// initial inputs are skipped
input = strings.TrimSpace(input)
if input == "" {
return
}
// split input to host and port (if specified)
host, port := splitHostPort(input)
// get ports list to use
var ports []string
if port == "" {
// use ports from default list if not specified explicitly
ports = defaultPorts
} else {
ports = []string{port}
}
// CIDR?
if isCIDR(host) {
// expand CIDR
ips, err := expandCIDR(host)
if err != nil {
chanResult <- &procResult{addr: input, err: err}
return
}
// feed IPs from CIDR to input channel
for ip := range ips {
for _, port := range ports {
chanInput <- net.JoinHostPort(ip, port)
}
}
} else {
// feed atomic host to input channel
for _, port := range ports {
chanInput <- net.JoinHostPort(host, port)
}
}
}
/* connects to addr and grabs certificate information.
returns slice of domain names from grabbed certificate */
func grabCert(addr string, dialer *net.Dialer, onlyValidDomainNames bool) ([]string, error) {
// dial
conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, err
}
defer conn.Close()
// get first certificate in chain
cert := conn.ConnectionState().PeerCertificates[0]
// get CommonName and all SANs into a slice
names := make([]string, 0, len(cert.DNSNames)+1)
if onlyValidDomainNames && isDomainName(cert.Subject.CommonName) || !onlyValidDomainNames {
names = append(names, cert.Subject.CommonName)
}
// append all SANs, excluding one that is equal to CN (if any)
for _, name := range cert.DNSNames {
if name != cert.Subject.CommonName {
if onlyValidDomainNames && isDomainName(name) || !onlyValidDomainNames {
names = append(names, name)
}
}
}
return names, nil
}