├── LICENSE ├── README.md ├── common.go ├── github.go ├── main.go ├── msfaux.go ├── nse.go └── scanner.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, AverageSecurityGuy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | Neither the name of AverageSecurityGuy nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Searchscan 2 | ========== 3 | Both Nmap and Metasploit are constantly adding new scanning capabilities. In addition, developers routinely create custom NSE scripts as well. Searchscan can help you find the script you need to scan what you want. Searchscan will search the local machine for installed Nmap NSE and MSF Auxiliary scripts. In addition, it will search GitHub for Nmap NSE scripts. 4 | 5 | Usage 6 | ----- 7 | ./searchscan [options] keyword 8 | -c Build the GitHub cache. 9 | -d Show description along with name and path. 10 | -n Search for keyword in the name only. 11 | 12 | Examples 13 | -------- 14 | ./searchscan ms17 15 | 16 | smb-vuln-ms17-010.nse - /usr/share/nmap/scripts/smb-vuln-ms17-010.nse 17 | smb_ms17_010.rb - /usr/share/metasploit-framework/modules/auxiliary/scanner/smb/smb_ms17_010.rb 18 | 19 | ./searchscan -d ms17 20 | 21 | smb-vuln-ms17-010.nse 22 | ===================== 23 | Path: /usr/share/nmap/scripts/smb-vuln-ms17-010.nse 24 | 25 | Attempts to detect if a Microsoft SMBv1 server is vulnerable to a remote code 26 | execution vulnerability (ms17-010, a.k.a. EternalBlue). The vulnerability is 27 | actively exploited by WannaCry and Petya ransomware and other malware. 28 | 29 | The script connects to the $IPC tree, executes a transaction on FID 0 and 30 | checks if the error "STATUS_INSUFF_SERVER_RESOURCES" is returned to determine 31 | if the target is not patched against ms17-010. Additionally it checks for 32 | known error codes returned by patched systems. 33 | 34 | Tested on Windows XP, 2003, 7, 8, 8.1, 10, 2008, 2012 and 2016. 35 | 36 | References: 37 | * https://technet.microsoft.com/en-us/library/security/ms17-010.aspx 38 | * https://blogs.technet.microsoft.com/msrc/2017/05/12/customer-guidance-for-wannacrypt-attacks/ 39 | * https://msdn.microsoft.com/en-us/library/ee441489.aspx 40 | * https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/smb/smb_ms17_010.rb 41 | * https://github.com/cldrn/nmap-nse-scripts/wiki/Notes-about-smb-vuln-ms17-010 42 | 43 | 44 | smb_ms17_010.rb 45 | =============== 46 | Path: /usr/share/metasploit-framework/modules/auxiliary/scanner/smb/smb_ms17_010.rb 47 | 48 | Uses information disclosure to determine if MS17-010 has been patched or not. 49 | Specifically, it connects to the IPC$ tree and attempts a transaction on FID 50 | 0. If the status returned is "STATUS_INSUFF_SERVER_RESOURCES", the machine 51 | does not have the MS17-010 patch. If the machine is missing the MS17-010 52 | patch, the module will check for an existing DoublePulsar (ring 0 53 | shellcode/malware) infection. This module does not require valid SMB 54 | credentials in default server configurations. It can log on as the user "\" 55 | and connect to IPC$. 56 | 57 | Configuration 58 | ------------- 59 | Searchscan reads and parses the Nmap NSE and Metasploit scripts that are installed locally on the machine. Searchscan was designed to run on Kali Linux. If you are running Searchscan on any other OS you will need to ensure Nmap and Metasploit are installed and you will need to modify the `config.nsePath` and `config.msfauxPath` variables in the main.go file. 60 | 61 | If you want to search GitHub, you will need to build the local cache using `./searchscan -c`. Before you can build the cache you will need to modify the `config.username`, `config.apitoken`, and `config.cachePath` variables in the main.go file. Once the cache is built you can update it periodically by running `./searchscan -c`. 62 | 63 | If you need to create an access token, follow the directions at https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/. The access token only needs the default permissions. 64 | 65 | Building 66 | -------- 67 | Building Searchscan is easy and follows a similar pattern to most Golang scripts. 68 | 69 | git clone https:// 70 | cd 71 | go build 72 | ./searchscan 73 | 74 | To Do 75 | ----- 76 | Add support for MSF Aux modules on GitHub, if possible. 77 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // Recursively search a directory for files with the specified extension. Modified 11 | // from https://gist.github.com/moongears/f1f2eec925997502a755 12 | func findFiles(root, ext string) []string { 13 | var files []string 14 | 15 | err := filepath.Walk(root, func(path string, file os.FileInfo, err error) error { 16 | if err != nil { 17 | return err 18 | } 19 | 20 | if file.IsDir() { 21 | return nil 22 | } 23 | 24 | if filepath.Ext(path) == ext { 25 | files = append(files, path) 26 | } 27 | 28 | return nil 29 | }) 30 | 31 | if err != nil { 32 | if os.IsNotExist(err) { 33 | fmt.Println("Path does not exist.") 34 | return files 35 | } 36 | 37 | fmt.Println(err) 38 | return files 39 | } 40 | 41 | return files 42 | } 43 | 44 | // Wrap the given text at 80 characters. Modified from 45 | // https://www.rosettacode.org/wiki/Word_wrap#Go 46 | func wrap(text string, hang bool) string { 47 | width := 78 48 | words := strings.Fields(text) 49 | 50 | if len(words) == 0 { 51 | return text 52 | } 53 | 54 | wrapped := words[0] 55 | spaceLeft := width - len(wrapped) 56 | 57 | for _, word := range words[1:] { 58 | if len(word)+1 > spaceLeft { 59 | if hang { 60 | wrapped += "\n " + word 61 | spaceLeft = width - len(word) - 4 62 | } else { 63 | wrapped += "\n" + word 64 | spaceLeft = width - len(word) 65 | } 66 | } else { 67 | wrapped += " " + word 68 | spaceLeft -= 1 + len(word) 69 | } 70 | } 71 | 72 | return wrapped 73 | } 74 | -------------------------------------------------------------------------------- /github.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "path" 11 | "path/filepath" 12 | "regexp" 13 | "os" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | var re_next = regexp.MustCompile(`^<(https://api.github.com.*)>; rel="next"`) 19 | 20 | type result struct { 21 | Items []item `json:"items"` 22 | } 23 | 24 | type item struct { 25 | Name string `json:"name"` 26 | Sha string `json:"sha"` 27 | Url string `json:"html_url"` 28 | } 29 | 30 | func request(query string) ([]byte, string, error) { 31 | var body []byte 32 | 33 | next := "" 34 | url := config.apibase + query 35 | client := &http.Client{} 36 | 37 | req, err := http.NewRequest("GET", url, nil) 38 | req.SetBasicAuth(config.username, config.apitoken) 39 | resp, err := client.Do(req) 40 | 41 | switch { 42 | case err != nil: 43 | return body, next, err 44 | case resp.StatusCode == 404: 45 | return body, next, errors.New("Endpoint not found or invalid authentication.") 46 | default: 47 | } 48 | 49 | defer resp.Body.Close() 50 | 51 | // Get the next link for our search 52 | links := strings.Split(resp.Header.Get("link"), ", ") 53 | for _, l := range(links) { 54 | m := re_next.FindStringSubmatch(l) 55 | if m != nil { 56 | next = m[1] 57 | break 58 | } 59 | } 60 | 61 | body, err = ioutil.ReadAll(resp.Body) 62 | if err != nil { 63 | return body, next, errors.New("Could not read response.") 64 | } 65 | 66 | return body, next, nil 67 | } 68 | 69 | func download(url string) { 70 | script := path.Base(url) 71 | filename := filepath.Join(config.cachePath, script) 72 | 73 | fmt.Printf("Downloading %s\n", script) 74 | 75 | resp, err := http.Get(url) 76 | if err != nil { 77 | fmt.Printf("Could not access %s.\n", url) 78 | } 79 | 80 | defer resp.Body.Close() 81 | 82 | body, err := ioutil.ReadAll(resp.Body) 83 | if err != nil { 84 | fmt.Println("Could not read HTTP response.") 85 | } 86 | 87 | f, err := os.Create(filename) 88 | if err != nil { 89 | fmt.Printf("Could not create file %s\n", filename) 90 | } 91 | 92 | _, err = f.WriteString(fmt.Sprintf("\n-- @GitHub %s\n", url)) 93 | if err != nil { 94 | fmt.Printf("Could not write file.") 95 | } 96 | 97 | _, err = f.Write(body) 98 | if err != nil { 99 | fmt.Printf("Could not write file.") 100 | } 101 | } 102 | 103 | // Build a cache of NSE scripts from GitHub 104 | func buildGithubCache(stype string) error { 105 | var query string 106 | var result result 107 | var items []item 108 | var nmaps []string 109 | 110 | fmt.Printf("Building GitHub cache for %s scripts.\n", stype) 111 | 112 | if config.username == "" || config.apitoken == "" { 113 | return errors.New("Invalid Github credentials. Cannot build GitHub cache.") 114 | } 115 | 116 | if _, err := os.Stat(config.cachePath); os.IsNotExist(err) { 117 | return errors.New(fmt.Sprintf("GitHub cache path (%s) does not exist.\n", config.cachePath)) 118 | } 119 | 120 | switch { 121 | case stype == "nse": 122 | query = "nse in:path language:lua extension:nse" 123 | case stype == "msfaux": 124 | query = "" 125 | } 126 | 127 | params := url.Values{} 128 | params.Set("q", query) 129 | params.Set("per_page", "100") 130 | params.Set("page", "1") 131 | 132 | resp, next, err := request(params.Encode()) 133 | if err != nil { 134 | return err 135 | } 136 | json.Unmarshal(resp, &result) 137 | items = append(items, result.Items...) 138 | 139 | for { 140 | // No more results. Quit 141 | if next == "" { 142 | break 143 | } 144 | 145 | resp, next, err = request(next[35:]) 146 | if err != nil { 147 | return err 148 | } 149 | json.Unmarshal(resp, &result) 150 | items = append(items, result.Items...) 151 | 152 | time.Sleep(2500 * time.Millisecond) 153 | } 154 | 155 | // Flag items from the Nmap repo so we can remove any files that are 156 | // duplicates. 157 | urls := make(map[string] string) 158 | for _, item := range items { 159 | switch { 160 | case strings.HasPrefix(item.Url, "https://github.com/nmap/"): 161 | nmaps = append(nmaps, item.Sha) 162 | default: 163 | urls[item.Sha] = item.Url 164 | } 165 | } 166 | 167 | // Use the SHA1 hash to delete files that are duplicates of official Nmap NSEs 168 | for _, sha := range nmaps { 169 | delete(urls, sha) 170 | } 171 | 172 | fmt.Printf("Downloading %d %s scripts from Github.\n", len(urls), stype) 173 | for _, url := range urls { 174 | // Need the raw URL. 175 | url = strings.Replace(url, "github.com", "raw.githubusercontent.com", 1) 176 | url = strings.Replace(url, "blob/", "", 1) 177 | download(url) 178 | } 179 | 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, AverageSecurityGuy 3 | # All rights reserved. 4 | */ 5 | 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "os" 12 | ) 13 | 14 | type Configuration struct { 15 | nsePath string 16 | msfauxPath string 17 | cachePath string 18 | username string 19 | apitoken string 20 | apibase string 21 | nameOnly bool 22 | showDesc bool 23 | githubCache bool 24 | } 25 | 26 | var config Configuration 27 | 28 | func configuration() { 29 | config.nsePath = "/usr/share/nmap/scripts" 30 | config.msfauxPath = "/usr/share/metasploit-framework/modules/auxiliary/scanner" 31 | config.cachePath = "" 32 | config.username = "" 33 | config.apitoken = "" 34 | config.apibase = "https://api.github.com/search/code?" 35 | } 36 | 37 | func usage() { 38 | fmt.Println("Usage: searchscan [options] keyword") 39 | flag.PrintDefaults() 40 | } 41 | 42 | func main() { 43 | flag.Usage = usage 44 | flag.BoolVar(&config.showDesc, "d", false, "Show description along with name and path.") 45 | flag.BoolVar(&config.nameOnly, "n", false, "Search for keyword in the name only.") 46 | flag.BoolVar(&config.githubCache, "c", false, "Build the GitHub cache.") 47 | 48 | flag.Parse() 49 | configuration() 50 | 51 | if config.githubCache == true { 52 | err := buildGithubCache("nse") 53 | if err != nil { 54 | fmt.Println(err) 55 | os.Exit(0) 56 | } 57 | 58 | os.Exit(0) 59 | } 60 | 61 | if len(flag.Args()) != 1 { 62 | flag.Usage() 63 | os.Exit(0) 64 | } 65 | 66 | for _, s := range findScanners(flag.Arg(0)) { 67 | if config.showDesc == true { 68 | fmt.Println(s.Detail()) 69 | } else { 70 | fmt.Println(s.Summary()) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /msfaux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Parse Nmap NSE scripts. 5 | */ 6 | 7 | import ( 8 | "io/ioutil" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | var re_auxdesc = regexp.MustCompile(`(?m)'Description' +=> +'(.*)'`) 15 | var re_auxdesc_m = regexp.MustCompile(`(?sm)'Description' +=> +%q{\n(.*?)}`) 16 | 17 | func reformat(data string) string { 18 | data = strings.Join(strings.Fields(data), " ") 19 | return wrap(data, false) 20 | } 21 | 22 | func parseMsfaux(data []byte) string { 23 | var description string 24 | 25 | m := re_auxdesc.FindSubmatch(data) 26 | if m != nil { 27 | description = string(m[1]) 28 | } else { 29 | m = re_auxdesc_m.FindSubmatch(data) 30 | if m != nil { 31 | description = reformat(string(m[1])) 32 | } 33 | } 34 | 35 | return description 36 | } 37 | 38 | func loadMsfAux(filename string) (scanner, error) { 39 | var msfaux scanner 40 | 41 | data, err := ioutil.ReadFile(filename) 42 | if err != nil { 43 | return msfaux, err 44 | } 45 | 46 | msfaux.SetName(filepath.Base(filename)) 47 | msfaux.SetPath(filename) 48 | msfaux.SetDescription(parseMsfaux(data)) 49 | 50 | return msfaux, nil 51 | } 52 | -------------------------------------------------------------------------------- /nse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Parse Nmap NSE scripts. 5 | */ 6 | 7 | import ( 8 | "fmt" 9 | "io/ioutil" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | var re_desc = regexp.MustCompile(`(?m)^description = "(.*)"`) 16 | var re_desc_m = regexp.MustCompile(`(?sm)^description = \[\[\n(.*?)\]\]`) 17 | var re_emph = regexp.MustCompile(`\*([a-zA-Z ]+)\*`) 18 | var re_list = regexp.MustCompile(` *[*-] +`) 19 | var re_url = regexp.MustCompile(`(?m)-- @GitHub (.*)`) 20 | 21 | 22 | func clean(data string) string { 23 | data = re_emph.ReplaceAllString(data, "$1") 24 | data = strings.Replace(data, "", "", -1) 25 | data = strings.Replace(data, "", "", -1) 26 | 27 | return data 28 | } 29 | 30 | func list(data string) string { 31 | items := re_list.Split(data, -1) 32 | items[0] = wrap(items[0], false) 33 | 34 | for i, _ := range items[1:] { 35 | items[i+1] = fmt.Sprintf("\n * %s", wrap(items[i+1], true)) 36 | } 37 | 38 | return strings.Join(items, "") 39 | } 40 | 41 | func format(data string) string { 42 | var paragraphs []string 43 | 44 | for _, p := range strings.Split(data, "\n\n") { 45 | paragraphs = append(paragraphs, list(p)) 46 | } 47 | 48 | return strings.Join(paragraphs, "\n\n") 49 | } 50 | 51 | func parseNSE(data []byte) (string, string) { 52 | var description string 53 | var url string 54 | 55 | m := re_desc.FindSubmatch(data) 56 | if m != nil { 57 | description = clean(string(m[1])) 58 | } else { 59 | m = re_desc_m.FindSubmatch(data) 60 | if m != nil { 61 | description = format(clean(string(m[1]))) 62 | } 63 | } 64 | 65 | m = re_url.FindSubmatch(data) 66 | if m != nil { 67 | url = string(m[1]) 68 | } 69 | 70 | return description, url 71 | } 72 | 73 | func loadNSE(filename string) (scanner, error) { 74 | var nse scanner 75 | 76 | data, err := ioutil.ReadFile(filename) 77 | if err != nil { 78 | return nse, err 79 | } 80 | 81 | nse.SetName(filepath.Base(filename)) 82 | nse.SetPath(filename) 83 | 84 | description, url := parseNSE(data) 85 | 86 | // If the NSE is in our cache then use the GitHub URL for the path. 87 | if url != "" { 88 | nse.SetPath(url) 89 | } 90 | 91 | nse.SetDescription(description) 92 | 93 | return nse, nil 94 | } 95 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Struct and methods to store script information 10 | type scanner struct { 11 | name string 12 | path string 13 | description string 14 | } 15 | 16 | func (s *scanner) SetName(name string) { 17 | s.name = name 18 | } 19 | 20 | func (s *scanner) SetPath(path string) { 21 | s.path = path 22 | } 23 | 24 | func (s *scanner) SetDescription(desc string) { 25 | s.description = desc 26 | } 27 | 28 | func (s *scanner) Detail() string { 29 | var str bytes.Buffer 30 | 31 | str.WriteString(fmt.Sprintf("%s\n", s.name)) 32 | str.WriteString(fmt.Sprintf("%s\n", strings.Repeat("=", len(s.name)))) 33 | str.WriteString(fmt.Sprintf("Path: %s\n\n", s.path)) 34 | str.WriteString(fmt.Sprintf("%s\n\n", s.description)) 35 | 36 | return str.String() 37 | } 38 | 39 | func (s *scanner) Summary() string { 40 | return fmt.Sprintf("%s - %s", s.name, s.path) 41 | } 42 | 43 | func (s *scanner) Check(keyword string, nameOnly bool) bool { 44 | keyword = strings.ToLower(keyword) 45 | name := strings.ToLower(s.name) 46 | desc := strings.ToLower(s.description) 47 | 48 | if nameOnly == true { 49 | return strings.Contains(name, keyword) 50 | } else { 51 | return strings.Contains(name, keyword) || strings.Contains(desc, keyword) 52 | } 53 | } 54 | 55 | func loadScanners(stype string) []scanner { 56 | var scanners []scanner 57 | var files []string 58 | var loader func(string) (scanner, error) 59 | 60 | switch { 61 | case stype == "nse": 62 | fmt.Println("Searching local Nmap NSE scripts.") 63 | files = findFiles(config.nsePath, ".nse") 64 | loader = loadNSE 65 | case stype == "msfaux": 66 | fmt.Println("Searching local MSF Auxiliary scripts.") 67 | files = findFiles(config.msfauxPath, ".rb") 68 | loader = loadMsfAux 69 | case stype == "ghnse": 70 | fmt.Println("Searching Github NSE scripts.") 71 | files = findFiles(config.cachePath, ".nse") 72 | loader = loadNSE 73 | default: 74 | return scanners 75 | } 76 | 77 | for _, f := range files { 78 | scanner, err := loader(f) 79 | 80 | if err != nil { 81 | fmt.Println(err) 82 | continue 83 | } 84 | 85 | scanners = append(scanners, scanner) 86 | } 87 | 88 | return scanners 89 | } 90 | 91 | func findScanners(keyword string) []scanner { 92 | var scanners []scanner 93 | 94 | scanners = append(scanners, loadScanners("nse")...) 95 | scanners = append(scanners, loadScanners("msfaux")...) 96 | scanners = append(scanners, loadScanners("ghnse")...) 97 | 98 | var found []scanner 99 | for _, s := range scanners { 100 | if s.Check(keyword, config.nameOnly) { 101 | found = append(found, s) 102 | } 103 | } 104 | 105 | return found 106 | } 107 | --------------------------------------------------------------------------------