Skip to content

Commit 407f60a

Browse files
committed
Initial
0 parents  commit 407f60a

File tree

11 files changed

+511
-0
lines changed

11 files changed

+511
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
assetfinder

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# assetfinder
2+
3+
## Install
4+
5+
```
6+
go get -u github.com/tomnomnom/assetfinder
7+
```
8+
9+
## Usage
10+
11+
```
12+
assetfinder [--subs-only] <domain>
13+
```
14+
15+
## TODO:
16+
* http://api.passivetotal.org/api/docs/
17+
* https://findsubdomains.com
18+
* https://community.riskiq.com/ (?)
19+
* https://riddler.io/
20+
* http://www.dnsdb.org/
21+
* https://certdb.com/api-documentation

certspotter.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
func fetchCertSpotter(domain string) ([]string, error) {
8+
out := make([]string, 0)
9+
10+
fetchURL := fmt.Sprintf("https://certspotter.com/api/v0/certs?domain=%s", domain)
11+
12+
wrapper := []struct {
13+
DNSNames []string `json:"dns_names"`
14+
}{}
15+
err := fetchJSON(fetchURL, &wrapper)
16+
if err != nil {
17+
return out, err
18+
}
19+
20+
for _, w := range wrapper {
21+
out = append(out, w.DNSNames...)
22+
}
23+
24+
return out, nil
25+
}

crtsh.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
)
8+
9+
func fetchCrtSh(domain string) ([]string, error) {
10+
resp, err := http.Get(
11+
fmt.Sprintf("https://crt.sh/?q=%%25.%s&output=json", domain),
12+
)
13+
if err != nil {
14+
return []string{}, err
15+
}
16+
defer resp.Body.Close()
17+
18+
output := make([]string, 0)
19+
20+
dec := json.NewDecoder(resp.Body)
21+
22+
// The crt.sh API is a little funky... It returns multiple
23+
// JSON objects with no delimiter, so you just have to keep
24+
// attempting a decode until you hit EOF
25+
for {
26+
wrapper := struct {
27+
Name string `json:"name_value"`
28+
}{}
29+
30+
err := dec.Decode(&wrapper)
31+
if err != nil {
32+
break
33+
}
34+
35+
output = append(output, wrapper.Name)
36+
}
37+
return output, nil
38+
}

facebook.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"os"
9+
)
10+
11+
func fetchFacebook(domain string) ([]string, error) {
12+
13+
appId := os.Getenv("FB_APP_ID")
14+
appSecret := os.Getenv("FB_APP_SECRET")
15+
if appId == "" || appSecret == "" {
16+
// fail silently because it's reasonable not to have
17+
// the Facebook API creds
18+
return []string{}, nil
19+
}
20+
21+
accessToken, err := facebookAuth(appId, appSecret)
22+
if err != nil {
23+
return []string{}, err
24+
}
25+
26+
domains, err := getFacebookCerts(accessToken, domain)
27+
if err != nil {
28+
return []string{}, err
29+
}
30+
31+
return domains, nil
32+
}
33+
34+
func getFacebookCerts(accessToken, query string) ([]string, error) {
35+
out := make([]string, 0)
36+
fetchURL := fmt.Sprintf(
37+
"https://graph.facebook.com/certificates?fields=domains&access_token=%s&query=*.%s",
38+
accessToken, query,
39+
)
40+
41+
for {
42+
43+
wrapper := struct {
44+
Data []struct {
45+
Domains []string `json:"domains"`
46+
} `json:"data"`
47+
48+
Paging struct {
49+
Next string `json:"next"`
50+
} `json:"paging"`
51+
}{}
52+
53+
err := fetchJSON(fetchURL, &wrapper)
54+
if err != nil {
55+
return out, err
56+
}
57+
58+
for _, data := range wrapper.Data {
59+
for _, d := range data.Domains {
60+
out = append(out, d)
61+
}
62+
}
63+
64+
fetchURL = wrapper.Paging.Next
65+
if fetchURL == "" {
66+
break
67+
}
68+
}
69+
return out, nil
70+
}
71+
72+
func facebookAuth(appId, appSecret string) (string, error) {
73+
authUrl := fmt.Sprintf(
74+
"https://graph.facebook.com/oauth/access_token?client_id=%s&client_secret=%s&grant_type=client_credentials",
75+
appId, appSecret,
76+
)
77+
78+
resp, err := http.Get(authUrl)
79+
if err != nil {
80+
return "", err
81+
}
82+
83+
defer resp.Body.Close()
84+
85+
dec := json.NewDecoder(resp.Body)
86+
87+
auth := struct {
88+
AccessToken string `json:"access_token"`
89+
}{}
90+
err = dec.Decode(&auth)
91+
if err != nil {
92+
return "", err
93+
}
94+
95+
if auth.AccessToken == "" {
96+
return "", errors.New("no access token in Facebook API response")
97+
}
98+
99+
return auth.AccessToken, nil
100+
}

hackertarget.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"strings"
8+
)
9+
10+
func fetchHackerTarget(domain string) ([]string, error) {
11+
out := make([]string, 0)
12+
13+
raw, err := httpGet(
14+
fmt.Sprintf("https://api.hackertarget.com/hostsearch/?q=%s", domain),
15+
)
16+
if err != nil {
17+
return out, err
18+
}
19+
20+
sc := bufio.NewScanner(bytes.NewReader(raw))
21+
for sc.Scan() {
22+
parts := strings.SplitN(sc.Text(), ",", 2)
23+
if len(parts) != 2 {
24+
continue
25+
}
26+
27+
out = append(out, parts[0])
28+
}
29+
30+
return out, sc.Err()
31+
}

main.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"os"
10+
"strings"
11+
"sync"
12+
)
13+
14+
func main() {
15+
var subsOnly bool
16+
flag.BoolVar(&subsOnly, "subs-only", false, "Only incluse subdomains of search domain")
17+
flag.Parse()
18+
19+
domain := flag.Arg(0)
20+
if domain == "" {
21+
fmt.Println("no domain specified")
22+
return
23+
}
24+
domain = strings.ToLower(domain)
25+
26+
sources := []fetchFn{
27+
fetchCertSpotter,
28+
fetchHackerTarget,
29+
fetchThreatCrowd,
30+
fetchCrtSh,
31+
fetchFacebook,
32+
//fetchWayback, // A little too slow :(
33+
fetchVirusTotal,
34+
}
35+
36+
out := make(chan string)
37+
var wg sync.WaitGroup
38+
39+
// call each of the source workers in a goroutine
40+
for _, source := range sources {
41+
wg.Add(1)
42+
fn := source
43+
44+
go func() {
45+
defer wg.Done()
46+
47+
names, err := fn(domain)
48+
49+
if err != nil {
50+
fmt.Fprintf(os.Stderr, "err: %s\n", err)
51+
return
52+
}
53+
54+
for _, n := range names {
55+
out <- n
56+
}
57+
}()
58+
}
59+
60+
// close the output channel when all the workers are done
61+
go func() {
62+
wg.Wait()
63+
close(out)
64+
}()
65+
66+
// track what we've already printed to avoid duplicates
67+
printed := make(map[string]bool)
68+
69+
for n := range out {
70+
n = cleanDomain(n)
71+
if _, ok := printed[n]; ok {
72+
continue
73+
}
74+
if subsOnly && !strings.HasSuffix(n, domain) {
75+
continue
76+
}
77+
fmt.Println(n)
78+
printed[n] = true
79+
}
80+
}
81+
82+
type fetchFn func(string) ([]string, error)
83+
84+
func httpGet(url string) ([]byte, error) {
85+
res, err := http.Get(url)
86+
if err != nil {
87+
return []byte{}, err
88+
}
89+
90+
raw, err := ioutil.ReadAll(res.Body)
91+
92+
res.Body.Close()
93+
if err != nil {
94+
return []byte{}, err
95+
}
96+
97+
return raw, nil
98+
}
99+
100+
func cleanDomain(d string) string {
101+
d = strings.ToLower(d)
102+
103+
// no idea what this is, but we can't clean it ¯\_(ツ)_/¯
104+
if len(d) < 2 {
105+
return d
106+
}
107+
108+
if d[0] == '*' || d[0] == '%' {
109+
d = d[1:]
110+
}
111+
112+
if d[0] == '.' {
113+
d = d[1:]
114+
}
115+
116+
return d
117+
118+
}
119+
120+
func fetchJSON(url string, wrapper interface{}) error {
121+
resp, err := http.Get(url)
122+
if err != nil {
123+
return err
124+
}
125+
defer resp.Body.Close()
126+
dec := json.NewDecoder(resp.Body)
127+
128+
return dec.Decode(wrapper)
129+
}

0 commit comments

Comments
 (0)