-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathmain.go
297 lines (236 loc) · 10.3 KB
/
main.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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package main
import (
"bytes"
"context"
"encoding/binary"
"flag"
"fmt"
"github.com/RyanJarv/cdn-proxy/lib"
"github.com/RyanJarv/cdn-proxy/pkg/cloudflare"
"github.com/RyanJarv/cdn-proxy/pkg/cloudfront"
cloudflareSdk "github.com/cloudflare/cloudflare-go"
"io/fs"
"io/ioutil"
"log"
"math"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"syscall"
)
var (
workers = flag.Int("workers", 100, "Maximum number of workers used to make requests, defaults to 100.")
domain = flag.String("domain", "", "The domain to route requests through, fetched from AWS if not specified.")
report = flag.String("report", "", "JSON report file output location.")
cloudfrontCmd = flag.NewFlagSet("cloudfront", flag.ExitOnError)
cloudflareCmd = flag.NewFlagSet("cloudflare", flag.ExitOnError)
region = cloudfrontCmd.String("region", "", "Proxy domain AWS Region, not used if -proxyDomain is passed")
profile = cloudfrontCmd.String("profile", "", "Proxy domain AWS Profile, not used if -proxyDomain is passed.")
subcommands = map[string]*flag.FlagSet{
cloudfrontCmd.Name(): cloudfrontCmd,
cloudflareCmd.Name(): cloudflareCmd,
}
)
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of cdn-scanner: %s [-domain string] [-report string] [-workers int] "+
"<cloudfront|cloudflare> [args...] <IP/CIDR/Path to File> ...\n", os.Args[0])
flag.PrintDefaults()
fmt.Printf(`
Overview
The cloudflare or cloudfront subcommands both take a list of IPs, Hostnames, CIDRs or optionally files which in turn
should contain a list of additional IPs, Hostnames, or CIDRs. Each network asset is then scanned, once for http and once
for https, both directly as well as proxied through the CDN specified, the responses are then compared to determine
whether IP allow listing is in effect for the asset.
For example, if the TCP connection for direct http request responds is closed by the remote host and the request when
proxied through the CDN responds with a 200 then this would indicate IP allow listing is used on the scanned asset.
Example output
******************************************************************
http://1.2.3.4 -- Both: open (200)
******************************************************************
https://1.2.3.5 -- Via Proxy: filtered (504), Origin: closed (0)
******************************************************************
In the output above the first line indicates that port 80 (http) at the address 1.2.3.4 is accessible both from
the scanners current IP as well as from the source IP of the CDN used.
The second line indicates that port 443 (https) at the address 1.2.3.5 was filtered (did not respond) when accessed
through the proxy and when accessed directly the remote closed (remote rejected the connection) the connection.
The status (open/closed/filtered) when for the proxied request is determined by the HTTP status code returned by the
CDN the request was proxied through. Typically CDNs will have different 5XX error codes for various failure
conditions which should allow you to determine if the remote rejected the connection, didn't respond, or timed out
in some other way. Worth keeping in mind that associating these HTTP status codes with the correct state is a work
in progress currently.
Generally the interesting results you likely want to look for will be when the proxied request returns a 2XX status
code and the direct request either is closed, filtered, or denied access (403). This very likely means there is
IP allow listing in place on the origin and that you can bypass this by routing requests through a distribution you
control in the CDN.
`)
cloudfrontUsage := getDefaults(cloudfrontCmd).String()
cloudfrontUsage = strings.ReplaceAll(cloudfrontUsage, "\n", "\n\t\t")
cloudflareUsage := getDefaults(cloudflareCmd).String()
cloudflareUsage = strings.ReplaceAll(cloudflareUsage, "\n", "\n\t\t")
fmt.Printf(`
Sub Commands
cloudfront [IP/Hostname/CIDR/file path] ...
%s
The cloudfront subcommand assumes the value passed with -domain is a cloudfront distribution set up with
cdn-proxy. If -domain is not passed then cdn-scanner will attempt to look for a CloudFront distribution in the
current account created by cdn-proxy.
The origin configuration is set dynamically for each request, making the CloudFront scanner much faster then
the cloudflare one.
cloudflare [IP/Hostname/CIDR/file path] ...
Note: Access keys need are assumed to be in the environment variables CLOUDFLARE_API_KEY and CLOUDFLARE_API_EMAIL.
The CloudFlare scanner works a bit differently, it does not necessarily need anything set up by cdn-proxy. This
scanner only needs access to a spare CloudFlare zone. Please use a spare, rather then risking running this
on any important domain.
Each network address first adds a proxied DNS subdomain to the zone passed in the -domain option. It then
makes a request to the recently created subdomain as well as to the network address directly comparing the two
responses.
After some time the subdomains will start getting reused, because it's difficult to know exactly when a change
to the CloudFlare API has gone into effect it is necessary to peform several steps which slow this scan down
quite a bit. First the subdomain that will be rotated is deleted from CloudFlare and we wait for either
CloudFlare to start returning the appropriate error message or for DNS to fail. Once the subdomain has
successfully been deleted it is then created again withh the new updated record. The proxied request then can
be served once the subdomain is observed to start working again. So far this has been the most reilable method
found, without these steps the results eventually become out of sync with the state the CloudFlare network was
in when the request is made. This typically doesn't happen at first, but rather after the scan has run for some
time suggesting that the CloudFlare API may start queing requests for individual users after some threshold.
%s
`, cloudfrontUsage, cloudflareUsage)
}
flag.Parse()
if flag.NArg() < 1 {
log.Fatalln("Expected either cloudflare or cloudfront subcommands.")
}
cmd := subcommands[flag.Args()[0]]
if cmd == nil {
fmt.Println("Expected either cloudflare or cloudfront subcommands.")
os.Exit(1)
}
err := cmd.Parse(flag.Args()[1:])
if err != nil {
log.Fatalln(err)
}
setUlimitOpenFiles(4096)
reporter := lib.NewReporter()
switch cmd.Name() {
case "cloudfront":
if *domain == "" {
proxy, err := cloudfront.GetProxy(context.Background(), *profile, *region)
if err != nil {
fmt.Println("error: ", err)
}
domain = proxy.DomainName
}
cf := cloudfront.NewScanner(*domain, *workers, 1000)
getTargets(flag.Args()[1:], cf.Submit)
cf.Pool.StopAndWait()
case "cloudflare":
api, err := cloudflareSdk.NewWithAPIToken(os.Getenv("CLOUDFLARE_API_KEY"))
if err != nil {
log.Fatalln(err)
}
cf := cloudflare.NewScanner(api, reporter, *workers, *domain, 1000)
getTargets(flag.Args()[1:], cf.Submit)
cf.Pool.StopAndWait()
default:
fmt.Println("Expected either cloudflare or cloudfront subcommands.")
os.Exit(2)
}
if report != nil && *report != "" {
json, err := reporter.ToJson()
if err != nil {
log.Fatalln(err)
}
err = ioutil.WriteFile(*report, json, fs.FileMode(0o0640))
if err != nil {
log.Fatalf("JSON report location: %s\n", err)
}
}
}
func getDefaults(cmd *flag.FlagSet) *bytes.Buffer {
buf := new(bytes.Buffer)
cmd.SetOutput(buf)
cmd.PrintDefaults()
return buf
}
func setUlimitOpenFiles(i uint64) {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
fmt.Println("Error Getting Rlimit ", err)
}
if rLimit.Cur >= i {
fmt.Println("Ulimit # of files open is currently set to", rLimit.Cur)
return
}
if rLimit.Max > i {
fmt.Println("Ulimit max # of files open is less then", i, "raising this value may fail if the current " +
"user does not have permissions to override this value.")
}
rLimit.Cur = i
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
fmt.Println("error setting Rlimit ", err)
}
}
func getTargets(args []string, submit func(req *http.Request)) {
for _, v := range args {
content, err := ioutil.ReadFile(v)
if os.IsNotExist(err) {
submit(&http.Request{Host: v, URL: &url.URL{Scheme: "http", Host: v, Path: "/"}, Header: map[string][]string{}})
submit(&http.Request{Host: v, URL: &url.URL{Scheme: "https", Host: v, Path: "/"}, Header: map[string][]string{}})
} else if err != nil {
log.Fatalln("unable to read from file: ", err)
} else {
fmt.Printf("Reading contents of %s\n", v)
targetsFromString(content, submit)
}
}
}
func targetsFromString(s []byte, submit func(req *http.Request)) []string {
regIp, _ := regexp.Compile(`([0-9]{1,3}\.){3}[0-9]{1,3}(?:/\d\d?)?`)
regDomain, _ := regexp.Compile(`\S+.*\.(com|net|io|org)`)
found := regIp.FindAll(s, math.MaxInt)
found = append(found, regDomain.FindAll(s, math.MaxInt)...)
resp := make([]string, 0, 10)
for _, m := range found {
if s := string(m); strings.HasSuffix(s, "/") {
ips, err := cidrToIps(s)
if err != nil {
log.Fatalln(err)
}
for _, ip := range ips {
submit(&http.Request{Host: ip, URL: &url.URL{Scheme: "http", Host: ip, Path: "/"}, Header: map[string][]string{}})
submit(&http.Request{Host: ip, URL: &url.URL{Scheme: "https", Host: ip, Path: "/"}, Header: map[string][]string{}})
}
} else {
submit(&http.Request{Host: s, URL: &url.URL{Scheme: "http", Host: s, Path: "/"}, Header: map[string][]string{}})
submit(&http.Request{Host: s, URL: &url.URL{Scheme: "https", Host: s, Path: "/"}, Header: map[string][]string{}})
}
}
return resp
}
func cidrToIps(cidr string) ([]string, error) {
_, ipv4Net, err := net.ParseCIDR(cidr)
if err != nil {
return []string{}, fmt.Errorf("error parsing cidr %s: %s", cidr, err)
}
// convert IPNet struct mask and address to uint32
// network is BigEndian
mask := binary.BigEndian.Uint32(ipv4Net.Mask)
start := binary.BigEndian.Uint32(ipv4Net.IP)
// find the final address
finish := (start & mask) | (mask ^ 0xffffffff)
resp := make([]string, 0, finish)
// loop through addresses as uint32
for i := start; i <= finish; i++ {
// convert back to net.IP
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, i)
resp = append(resp, ip.String())
}
return resp, nil
}