├── go.mod ├── go.sum ├── .gitignore ├── LICENSE ├── main.go ├── utils.go ├── store.go ├── scrape.go ├── README.md └── retr.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/g0ldencybersec/CloudRecon 2 | 3 | go 1.20 4 | 5 | require github.com/mattn/go-sqlite3 v1.14.17 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= 2 | github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | CloudRecon 11 | test.txt 12 | certificates.db 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | downloaders/ 22 | download_ips.sh 23 | # Go workspace file 24 | go.work 25 | amazon/ 26 | google/ 27 | microsoft/ 28 | all/ 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 g0lden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path" 8 | ) 9 | 10 | var mainUsage = "scrape|store|retr [options]" 11 | 12 | func commandUsage(msg string, cmdFlagSet *flag.FlagSet) { 13 | fmt.Printf("Usage: %s %s\n\n", path.Base(os.Args[0]), msg) 14 | cmdFlagSet.PrintDefaults() 15 | 16 | if msg == mainUsage { 17 | fmt.Printf("\nSubcommands: \n\n") 18 | fmt.Printf("\t%-11s - Scrape given IPs and output CNs & SANs to stdout\n", "cloudrecon scrape") 19 | fmt.Printf("\t%-11s - Scrape and collect Orgs,CNs,SANs in local db file\n", "cloudrecon store") 20 | fmt.Printf("\t%-11s - Query local DB file for results\n", "cloudrecon retr") 21 | } 22 | } 23 | 24 | func main() { 25 | 26 | mainFlagSet := flag.NewFlagSet("amass", flag.ContinueOnError) 27 | var help bool 28 | mainFlagSet.BoolVar(&help, "h", false, "Show the program usage message") 29 | 30 | if len(os.Args) < 2 { 31 | commandUsage(mainUsage, mainFlagSet) 32 | return 33 | } 34 | if err := mainFlagSet.Parse(os.Args[1:]); err != nil { 35 | fmt.Printf("%v\n", err) 36 | os.Exit(1) 37 | } 38 | if help { 39 | commandUsage(mainUsage, mainFlagSet) 40 | return 41 | } 42 | 43 | switch os.Args[1] { 44 | case "scrape": 45 | runCloudScrape(os.Args[2:]) 46 | case "store": 47 | runCloudStore(os.Args[2:]) 48 | case "retr": 49 | runCloudRetr(os.Args[2:]) 50 | case "help": 51 | commandUsage(mainUsage, mainFlagSet) 52 | return 53 | default: 54 | commandUsage(mainUsage, mainFlagSet) 55 | os.Exit(1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "net" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | func getSSLCert(ip string, dialer *net.Dialer) (*x509.Certificate, error) { 14 | conn, err := tls.DialWithDialer(dialer, "tcp", ip, &tls.Config{ 15 | InsecureSkipVerify: true, 16 | }) 17 | if err != nil { 18 | return nil, err 19 | } 20 | defer conn.Close() 21 | 22 | cert := conn.ConnectionState().PeerCertificates[0] 23 | return cert, nil 24 | } 25 | 26 | // IPsFromCIDR generates a slice of IP strings from the given CIDR 27 | func IPsFromCIDR(cidr string, chanInput chan string, ports []string) error { 28 | ip, ipnet, err := net.ParseCIDR(cidr) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { 34 | for _, port := range ports { 35 | chanInput <- ip.String() + ":" + port 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | 42 | // inc increments an IP address 43 | func inc(ip net.IP) { 44 | for j := len(ip) - 1; j >= 0; j-- { 45 | ip[j]++ 46 | if ip[j] > 0 { 47 | break 48 | } 49 | } 50 | } 51 | 52 | func extractNames(cert *x509.Certificate) []string { 53 | names := append([]string{cert.Subject.CommonName}, cert.DNSNames...) 54 | return names 55 | } 56 | 57 | func intakeFunction(chanInput chan string, ports []string, input string) { 58 | if _, err := os.Stat(input); err == nil { 59 | readFile, err := os.Open(input) 60 | if err != nil { 61 | fmt.Println(err) 62 | os.Exit(1) 63 | } 64 | fileScanner := bufio.NewScanner(readFile) 65 | 66 | fileScanner.Split(bufio.ScanLines) 67 | 68 | for fileScanner.Scan() { 69 | line := fileScanner.Text() 70 | processInput(line, chanInput, ports) 71 | } 72 | readFile.Close() 73 | 74 | } else { 75 | for _, argItem := range strings.Split(input, ",") { 76 | processInput(argItem, chanInput, ports) 77 | } 78 | } 79 | } 80 | 81 | func isCIDR(value string) bool { 82 | return strings.Contains(value, `/`) 83 | } 84 | 85 | func isHostPort(value string) bool { 86 | return strings.Contains(value, `:`) 87 | } 88 | 89 | func processInput(argItem string, chanInput chan string, ports []string) { 90 | argItem = strings.TrimSpace(argItem) 91 | if isHostPort(argItem) { 92 | chanInput <- argItem 93 | } else if isCIDR(argItem) { 94 | err := IPsFromCIDR(argItem, chanInput, ports) 95 | if err != nil { 96 | panic("unable to parse CIDR" + argItem) 97 | } 98 | } else { 99 | for _, port := range ports { 100 | chanInput <- argItem + ":" + port 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | _ "github.com/mattn/go-sqlite3" // Import go-sqlite3 library 14 | ) 15 | 16 | type StoreArgs struct { 17 | Concurrency int 18 | Ports []string 19 | Timeout int 20 | PortList string 21 | Help bool 22 | Input string 23 | Database string 24 | } 25 | 26 | // struct to hold data for a database write operation 27 | type dbWriteRequest struct { 28 | ip string 29 | organization string 30 | commonName string 31 | san string 32 | } 33 | 34 | func runCloudStore(clArgs []string) { 35 | args := parseStoreCLI(clArgs) 36 | 37 | if _, err := os.Stat(args.Database); err == nil { 38 | fmt.Printf("Using database file %s\n", args.Database) 39 | } else { 40 | //Create DB file if it doesn't exist 41 | CreateDatabase(args.Database) 42 | } 43 | 44 | sqliteDatabase, _ := sql.Open("sqlite3", args.Database) // Open the created SQLite File 45 | createTable(sqliteDatabase) // Create Database Tables if needed 46 | defer sqliteDatabase.Close() // Ensure the database is closed when done 47 | 48 | dialer := &net.Dialer{ 49 | Timeout: time.Duration(args.Timeout) * time.Second, 50 | } 51 | 52 | // create a channel for these requests 53 | writeRequests := make(chan dbWriteRequest, 100) // Buffered channel 54 | 55 | //Channel for input 56 | inputChannel := make(chan string) 57 | 58 | // Start the dedicated database writer goroutine 59 | go func(db *sql.DB) { 60 | for req := range writeRequests { 61 | _, err := db.Exec("INSERT INTO certificates (ip, organization, common_name, san) VALUES (?, ?, ?, ?) ON CONFLICT(ip) DO UPDATE SET organization = excluded.organization, common_name = excluded.common_name, san = excluded.san", req.ip, req.organization, req.commonName, req.san) 62 | if err != nil { 63 | panic(err) // Or handle the error more gracefully 64 | } 65 | } 66 | }(sqliteDatabase) 67 | 68 | var inputwg sync.WaitGroup 69 | for i := 0; i < args.Concurrency; i++ { 70 | inputwg.Add(1) 71 | go func() { 72 | defer inputwg.Done() 73 | for ip := range inputChannel { 74 | cert, err := getSSLCert(ip, dialer) 75 | if err != nil { 76 | continue 77 | } 78 | names := extractNames(cert) 79 | org := "NONE" // Default value if org is not available 80 | if len(cert.Subject.Organization) > 0 { 81 | org = cert.Subject.Organization[0] 82 | } 83 | // Send the write request to the channel 84 | writeRequests <- dbWriteRequest{ 85 | ip: ip, 86 | organization: org, 87 | commonName: names[0], // Assuming names[0] is the common name 88 | san: strings.Join(names[1:], ","), 89 | } 90 | } 91 | }() 92 | } 93 | 94 | intakeFunction(inputChannel, args.Ports, args.Input) 95 | close(inputChannel) 96 | inputwg.Wait() 97 | sqliteDatabase.Close() 98 | } 99 | 100 | func parseStoreCLI(clArgs []string) StoreArgs { 101 | args := StoreArgs{} 102 | storeUsage := "store [options] -i " 103 | 104 | storeCommand := flag.NewFlagSet("scrape", flag.ContinueOnError) 105 | storeCommand.IntVar(&args.Concurrency, "c", 100, "How many goroutines running concurrently") 106 | storeCommand.StringVar(&args.PortList, "p", "443", "TLS ports to check for certificates") 107 | storeCommand.IntVar(&args.Timeout, "t", 4, "Timeout for TLS handshake") 108 | storeCommand.BoolVar(&args.Help, "h", false, "print usage!") 109 | storeCommand.StringVar(&args.Input, "i", "NONE", "Either IPs & CIDRs separated by commas, or a file with IPs/CIDRs on each line") 110 | storeCommand.StringVar(&args.Database, "db", "certificates.db", "String of the DB you want to connect to and save certs!") 111 | 112 | storeCommand.Parse(clArgs) 113 | 114 | if args.Input == "NONE" { 115 | fmt.Print("No input detected, please use the -i flag to add input!\n\n") 116 | fmt.Println(storeUsage) 117 | storeCommand.PrintDefaults() 118 | os.Exit(1) 119 | } 120 | 121 | if args.Help { 122 | fmt.Println(storeUsage) 123 | storeCommand.PrintDefaults() 124 | } 125 | 126 | args.Ports = strings.Split(args.PortList, ",") 127 | return args 128 | } 129 | 130 | func CreateDatabase(databaseName string) { 131 | os.Remove(databaseName) // I delete the file to avoid duplicated records. 132 | fmt.Println("Creating db...") 133 | file, err := os.Create(databaseName) // Create SQLite file 134 | if err != nil { 135 | panic(err.Error()) 136 | } 137 | file.Close() 138 | fmt.Println("db created") 139 | } 140 | 141 | func createTable(db *sql.DB) { 142 | statement, err := db.Prepare("CREATE TABLE IF NOT EXISTS certificates (ip TEXT PRIMARY KEY NOT NULL, organization TEXT, common_name TEXT, san TEXT)") 143 | if err != nil { 144 | panic(err) 145 | } 146 | statement.Exec() 147 | } 148 | -------------------------------------------------------------------------------- /scrape.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "strings" 10 | 11 | "time" 12 | ) 13 | 14 | type Result struct { 15 | IP string 16 | Hit bool 17 | Error error 18 | Certificate *CertificateInfo 19 | } 20 | 21 | type Worker struct { 22 | dialer *net.Dialer 23 | input <-chan string 24 | results chan<- Result 25 | } 26 | 27 | type WorkerPool struct { 28 | workers []*Worker 29 | input chan string 30 | results chan Result 31 | dialer *net.Dialer 32 | } 33 | 34 | type CertificateInfo struct { 35 | IP string `json:"ip"` 36 | Organization string `json:"organization"` 37 | CommonName string `json:"commonName"` 38 | SAN string `json:"san"` 39 | } 40 | 41 | type ScrapeArgs struct { 42 | Concurrency int 43 | Ports []string 44 | Timeout int 45 | PortList string 46 | Help bool 47 | Input string 48 | AllOutput bool 49 | JSONOutput bool 50 | } 51 | 52 | func NewWorker(dialer *net.Dialer, input <-chan string, results chan<- Result) *Worker { 53 | return &Worker{ 54 | dialer: dialer, 55 | input: input, 56 | results: results, 57 | } 58 | } 59 | 60 | func (w *Worker) run() { 61 | for ip := range w.input { 62 | cert, err := getSSLCert(ip, w.dialer) 63 | if err != nil { 64 | w.results <- Result{IP: ip, Error: err} 65 | continue 66 | } 67 | 68 | names := extractNames(cert) 69 | org := cert.Subject.Organization 70 | 71 | certInfo := CertificateInfo{ 72 | IP: ip, 73 | Organization: getOrganization(org), 74 | CommonName: names[0], 75 | SAN: joinNonEmpty(", ", names[1:]), 76 | } 77 | 78 | w.results <- Result{IP: ip, Hit: true, Certificate: &certInfo} 79 | } 80 | } 81 | 82 | func NewWorkerPool(size int, dialer *net.Dialer, input chan string, results chan Result) *WorkerPool { 83 | wp := &WorkerPool{ 84 | workers: make([]*Worker, size), 85 | input: input, 86 | results: results, 87 | dialer: dialer, 88 | } 89 | for i := range wp.workers { 90 | wp.workers[i] = NewWorker(wp.dialer, wp.input, wp.results) 91 | } 92 | return wp 93 | } 94 | 95 | func (wp *WorkerPool) Start() { 96 | for _, worker := range wp.workers { 97 | go worker.run() 98 | } 99 | } 100 | 101 | func (wp *WorkerPool) Stop() { 102 | close(wp.input) 103 | } 104 | 105 | func runCloudScrape(clArgs []string) { 106 | args := parseScrapeCLI(clArgs) 107 | 108 | dialer := &net.Dialer{ 109 | Timeout: time.Duration(args.Timeout) * time.Second, 110 | } 111 | 112 | inputChannel := make(chan string) 113 | resultChannel := make(chan Result) 114 | 115 | workerPool := NewWorkerPool(args.Concurrency, dialer, inputChannel, resultChannel) 116 | workerPool.Start() 117 | 118 | go intakeFunction(inputChannel, args.Ports, args.Input) 119 | 120 | defer func() { 121 | workerPool.Stop() 122 | close(resultChannel) 123 | }() 124 | 125 | for result := range resultChannel { 126 | if result.Error != nil { 127 | fmt.Printf("Failed to get SSL certificate from %s: %v\n", result.IP, result.Error) 128 | } else if result.Hit { 129 | if args.JSONOutput { 130 | outputJSON, _ := json.Marshal(result.Certificate) 131 | fmt.Println(string(outputJSON)) 132 | } else { 133 | fmt.Printf("Got SSL certificate from %s: [%s]\n", result.IP, result.Certificate.CommonName) 134 | } 135 | } else if args.AllOutput { 136 | fmt.Printf("No SSL certificate found for %s\n", result.IP) 137 | } 138 | } 139 | } 140 | 141 | func getOrganization(org []string) string { 142 | if len(org) > 0 { 143 | return org[0] 144 | } 145 | return "NONE" 146 | } 147 | 148 | func joinNonEmpty(sep string, elements []string) string { 149 | var result string 150 | for _, element := range elements { 151 | if element != "" { 152 | if result != "" { 153 | result += sep 154 | } 155 | result += element 156 | } 157 | } 158 | return result 159 | } 160 | 161 | func parseScrapeCLI(clArgs []string) ScrapeArgs { 162 | args := ScrapeArgs{} 163 | scrapeUsage := "scrape [options] -i " 164 | 165 | scrapeCommand := flag.NewFlagSet("scrape", flag.ContinueOnError) 166 | scrapeCommand.IntVar(&args.Concurrency, "c", 100, "How many goroutines running concurrently") 167 | scrapeCommand.StringVar(&args.PortList, "p", "443", "TLS ports to check for certificates") 168 | scrapeCommand.IntVar(&args.Timeout, "t", 4, "Timeout for TLS handshake") 169 | scrapeCommand.BoolVar(&args.Help, "h", false, "print usage!") 170 | scrapeCommand.StringVar(&args.Input, "i", "NONE", "Either IPs & CIDRs separated by commas, or a file with IPs/CIDRs on each line") 171 | scrapeCommand.BoolVar(&args.AllOutput, "a", false, "Add this flag if you want to see all output including failures") 172 | scrapeCommand.BoolVar(&args.JSONOutput, "j", false, "Generate JSON output") 173 | 174 | scrapeCommand.Parse(clArgs) 175 | 176 | if args.Input == "NONE" { 177 | fmt.Print("No input detected, please use the -i flag to add input!\n\n") 178 | fmt.Println(scrapeUsage) 179 | scrapeCommand.PrintDefaults() 180 | os.Exit(1) 181 | } 182 | 183 | if args.Help { 184 | fmt.Println(scrapeUsage) 185 | scrapeCommand.PrintDefaults() 186 | } 187 | 188 | args.Ports = strings.Split(args.PortList, ",") 189 | return args 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudRecon 2 | Finding assets from certificates! Scan the web! 3 | Tool presented @DEFCON 31 4 | 5 | ## BY 6 | ### Gunnar Andrews (@G0LDEN_infosec) 7 | 18 | 19 | ### Jason Haddix (@Jhaddix) 20 | 31 | 32 | # Install 33 | ** You must have CGO enabled, and may have to install gcc to run CloudRecon** 34 | ```sh 35 | sudo apt install gcc 36 | ``` 37 | 38 | ```sh 39 | go install github.com/g0ldencybersec/CloudRecon@latest 40 | ``` 41 | 42 | Note: 43 | Don't forget to [set your `GOPATH`](https://github.com/golang/go/wiki/SettingGOPATH) before installing. 44 | 45 | # Description 46 | **CloudRecon** 47 | 48 | 49 | CloudRecon is a suite of tools for red teamers and bug hunters to find ephemeral and development assets in their campaigns and hunts. 50 | 51 | Often, target organizations stand up cloud infrastructure that is not tied to their ASN or related to known infrastructure. Many times these assets are development sites, IT product portals, etc. Sometimes they don't have domains at all but many still need HTTPs. 52 | 53 | CloudRecon is a suite of tools to scan IP addresses or CIDRs (ex: cloud providers IPs) and find these hidden gems for testers, by inspecting those SSL certificates. 54 | 55 | The tool suite is three parts in GO: 56 | 57 | Scrape - A LIVE running tool to inspect the ranges for a keywork in SSL certs CN and SN fields in real time. 58 | 59 | Store - a tool to retrieve IPs certs and download all their Orgs, CNs, and SANs. So you can have your OWN cert.sh database. 60 | 61 | Retr - a tool to parse and search through the downloaded certs for keywords. 62 | 63 | # Usage 64 | 65 | ## Get Started 66 | ``` 67 | ## Don't forget to run in TMUX / Screen session 68 | wget https://raw.githubusercontent.com/lord-alfred/ipranges/main/all/ipv4_merged.txt 69 | CloudRecon scrape -i ipv4_merged.txt -j | tee -a certdb.json 70 | ``` 71 | 72 | Input Support: Either IPs & CIDRs separated by commas, or a file with IPs/CIDRs on each line, or file contains ip:port format list. 73 | 74 | **MAIN** 75 | ```sh 76 | Usage: CloudRecon scrape|store|retr [options] 77 | 78 | -h Show the program usage message 79 | 80 | Subcommands: 81 | 82 | cloudrecon scrape - Scrape given IPs and output CNs & SANs to stdout 83 | cloudrecon store - Scrape and collect Orgs,CNs,SANs in local db file 84 | cloudrecon retr - Query local DB file for results 85 | ``` 86 | **SCRAPE** 87 | ```sh 88 | scrape [options] -i 89 | -a Add this flag if you want to see all output including failures 90 | -c int 91 | How many goroutines running concurrently (default 100) 92 | -h print usage! 93 | -i string 94 | Either IPs & CIDRs separated by commas, or a file with IPs/CIDRs on each line (default "NONE") 95 | -j Generate JSON output ("IP, PORT, Organization, CommonName, SAN") 96 | -p string 97 | TLS ports to check for certificates (default "443") 98 | -t int 99 | Timeout for TLS handshake (default 4) 100 | ``` 101 | 102 | **STORE** 103 | ```sh 104 | store [options] -i 105 | -c int 106 | How many goroutines running concurrently (default 100) 107 | -db string 108 | String of the DB you want to connect to and save certs! (default "certificates.db") 109 | -h print usage! 110 | -i string 111 | Either IPs & CIDRs separated by commas, or a file with IPs/CIDRs on each line, or file contains ip:port format list. (default "NONE") 112 | -p string 113 | TLS ports to check for certificates (default "443") 114 | -t int 115 | Timeout for TLS handshake (default 4) 116 | ``` 117 | 118 | **RETR** 119 | ```sh 120 | retr [options] 121 | -all 122 | Return all the rows in the DB 123 | -cn string 124 | String to search for in common name column, returns like-results (default "NONE") 125 | -db string 126 | String of the DB you want to connect to and save certs! (default "certificates.db") 127 | -h print usage! 128 | -ip string 129 | String to search for in IP column, returns like-results (default "NONE") 130 | -num 131 | Return the Number of rows (results) in the DB (By IP) 132 | -org string 133 | String to search for in Organization column, returns like-results (default "NONE") 134 | -san string 135 | String to search for in common name column, returns like-results (default "NONE") 136 | ``` 137 | 138 | -------------------------------------------------------------------------------- /retr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | ) 9 | 10 | type RetrArgs struct { 11 | NumResults bool 12 | All bool 13 | Help bool 14 | Database string 15 | QueryOrg string 16 | QueryCommonName string 17 | QuerySAN string 18 | QueryIP string 19 | } 20 | 21 | type Certificate struct { 22 | IP string `json:"ip"` 23 | Organization string `json:"organization"` 24 | CommonName string `json:"common_name"` 25 | SAN string `json:"san"` 26 | } 27 | 28 | func runCloudRetr(clArgs []string) { 29 | args := parseRetrCLI(clArgs) 30 | 31 | sqliteDatabase, _ := sql.Open("sqlite3", args.Database) 32 | 33 | if args.NumResults { 34 | fmt.Printf("Cert DB has results for %d IPs\n", getNumResults(sqliteDatabase)) 35 | sqliteDatabase.Close() 36 | return 37 | } 38 | if args.All { 39 | fmt.Println(getAllResults(sqliteDatabase)) 40 | sqliteDatabase.Close() 41 | return 42 | } 43 | 44 | if args.QueryIP != "NONE" { 45 | fmt.Println(queryByIP(sqliteDatabase, args.QueryIP)) 46 | sqliteDatabase.Close() 47 | return 48 | } 49 | if args.QueryOrg != "NONE" { 50 | fmt.Println(queryByOrg(sqliteDatabase, args.QueryOrg)) 51 | sqliteDatabase.Close() 52 | return 53 | } 54 | if args.QueryCommonName != "NONE" { 55 | fmt.Println(queryByCommonName(sqliteDatabase, args.QueryCommonName)) 56 | sqliteDatabase.Close() 57 | return 58 | } 59 | if args.QuerySAN != "NONE" { 60 | fmt.Println(queryBySAN(sqliteDatabase, args.QuerySAN)) 61 | sqliteDatabase.Close() 62 | return 63 | } 64 | 65 | } 66 | 67 | func parseRetrCLI(clArgs []string) RetrArgs { 68 | args := RetrArgs{} 69 | retrUsage := "retr [options]" 70 | 71 | retrCommand := flag.NewFlagSet("scrape", flag.ContinueOnError) 72 | retrCommand.BoolVar(&args.Help, "h", false, "print usage!") 73 | retrCommand.BoolVar(&args.NumResults, "num", false, "Return the Number of rows (results) in the DB (By IP)") 74 | retrCommand.BoolVar(&args.All, "all", false, "Return all the rows in the DB") 75 | retrCommand.StringVar(&args.Database, "db", "certificates.db", "String of the DB you want to connect to and save certs!") 76 | retrCommand.StringVar(&args.QueryOrg, "org", "NONE", "String to search for in Organization column, returns like-results") 77 | retrCommand.StringVar(&args.QueryCommonName, "cn", "NONE", "String to search for in common name column, returns like-results") 78 | retrCommand.StringVar(&args.QuerySAN, "san", "NONE", "String to search for in common name column, returns like-results") 79 | retrCommand.StringVar(&args.QueryIP, "ip", "NONE", "String to search for in IP column, returns like-results") 80 | 81 | retrCommand.Parse(clArgs) 82 | 83 | if args.Help { 84 | fmt.Println(retrUsage) 85 | retrCommand.PrintDefaults() 86 | } 87 | 88 | return args 89 | } 90 | 91 | func getNumResults(db *sql.DB) int { 92 | var count int 93 | err := db.QueryRow("SELECT COUNT(*) FROM certificates").Scan(&count) 94 | if err != nil { 95 | panic(err) 96 | } 97 | return count 98 | } 99 | 100 | func getAllResults(db *sql.DB) string { 101 | rows, err := db.Query("SELECT ip, organization, common_name, san FROM certificates") 102 | if err != nil { 103 | panic(err) 104 | } 105 | defer rows.Close() 106 | 107 | var certs []Certificate 108 | for rows.Next() { 109 | var c Certificate 110 | err = rows.Scan(&c.IP, &c.Organization, &c.CommonName, &c.SAN) 111 | if err != nil { 112 | panic(err) 113 | } 114 | certs = append(certs, c) 115 | } 116 | 117 | err = rows.Err() 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | jsonData, err := json.MarshalIndent(certs, "", " ") 123 | if err != nil { 124 | panic(err) 125 | } 126 | 127 | return string(jsonData) 128 | } 129 | 130 | func queryByOrg(db *sql.DB, searchTerm string) string { 131 | rows, err := db.Query(`SELECT ip, organization, common_name, san FROM certificates WHERE organization LIKE ?`, "%"+searchTerm+"%") 132 | if err != nil { 133 | panic(err) 134 | } 135 | defer rows.Close() 136 | 137 | var certs []Certificate 138 | for rows.Next() { 139 | var c Certificate 140 | err = rows.Scan(&c.IP, &c.Organization, &c.CommonName, &c.SAN) 141 | if err != nil { 142 | panic(err) 143 | } 144 | certs = append(certs, c) 145 | } 146 | 147 | err = rows.Err() 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | jsonData, err := json.MarshalIndent(certs, "", " ") 153 | if err != nil { 154 | panic(err) 155 | } 156 | 157 | return string(jsonData) 158 | } 159 | 160 | func queryByIP(db *sql.DB, searchTerm string) string { 161 | rows, err := db.Query(`SELECT ip, organization, common_name, san FROM certificates WHERE ip LIKE ?`, searchTerm+"%") 162 | if err != nil { 163 | panic(err) 164 | } 165 | defer rows.Close() 166 | 167 | var certs []Certificate 168 | for rows.Next() { 169 | var c Certificate 170 | err = rows.Scan(&c.IP, &c.Organization, &c.CommonName, &c.SAN) 171 | if err != nil { 172 | panic(err) 173 | } 174 | certs = append(certs, c) 175 | } 176 | 177 | err = rows.Err() 178 | if err != nil { 179 | panic(err) 180 | } 181 | 182 | jsonData, err := json.MarshalIndent(certs, "", " ") 183 | if err != nil { 184 | panic(err) 185 | } 186 | 187 | return string(jsonData) 188 | } 189 | 190 | func queryByCommonName(db *sql.DB, searchTerm string) string { 191 | rows, err := db.Query(`SELECT ip, organization, common_name, san FROM certificates WHERE common_name LIKE ?`, "%"+searchTerm+"%") 192 | if err != nil { 193 | panic(err) 194 | } 195 | defer rows.Close() 196 | 197 | var certs []Certificate 198 | for rows.Next() { 199 | var c Certificate 200 | err = rows.Scan(&c.IP, &c.Organization, &c.CommonName, &c.SAN) 201 | if err != nil { 202 | panic(err) 203 | } 204 | certs = append(certs, c) 205 | } 206 | 207 | err = rows.Err() 208 | if err != nil { 209 | panic(err) 210 | } 211 | 212 | jsonData, err := json.MarshalIndent(certs, "", " ") 213 | if err != nil { 214 | panic(err) 215 | } 216 | 217 | return string(jsonData) 218 | } 219 | 220 | func queryBySAN(db *sql.DB, searchTerm string) string { 221 | rows, err := db.Query(`SELECT ip, organization, common_name, san FROM certificates WHERE san LIKE ?`, "%"+searchTerm+"%") 222 | if err != nil { 223 | panic(err) 224 | } 225 | defer rows.Close() 226 | 227 | var certs []Certificate 228 | for rows.Next() { 229 | var c Certificate 230 | err = rows.Scan(&c.IP, &c.Organization, &c.CommonName, &c.SAN) 231 | if err != nil { 232 | panic(err) 233 | } 234 | certs = append(certs, c) 235 | } 236 | 237 | err = rows.Err() 238 | if err != nil { 239 | panic(err) 240 | } 241 | 242 | jsonData, err := json.MarshalIndent(certs, "", " ") 243 | if err != nil { 244 | panic(err) 245 | } 246 | 247 | return string(jsonData) 248 | } 249 | --------------------------------------------------------------------------------