diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..deb9588 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Nemesis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..35cf1fa --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# JSRecon +## A powerful tool designed for identifying hidden endpoints and sensitive information within JavaScript files on a website. + + + +## Description: +JSRecon is a powerful tool designed for identifying hidden endpoints and sensitive information within JavaScript files on a website. It finds hidden URLs and hard-coded sensitive information to assist with detecting vulnerabilities. + + +## Features: + +- Fast crawler +- Finds sensitive information(API keys, e-mail(s), internal addresses...) +- Discovers hidden endpoints +- Built in Go + + +## Installation: + +### Option 1: + +[Download](https://github.com/Nemesis0U/JSRecon/releases) from releases + +### Option 2: +Run the following command to get the repo: + + $ go install -v github.com/Nemesis0U/JSRecon@latest + + +## Usage: + +### Options: + +``` +./jsrecon -h +NAME: + JSRecon - Scan and extract endpoint URLs and sensitive data from JS files on a website + +USAGE: + JSRecon [global options] command [command options] [arguments...] + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --url value, -u value URL of the website to scan (required) + --keyword value Keyword to search for in JavaScript code (optional) + --output value, -o value Output file to save the links (optional) + --show-as-domain Show results as domains instead of full URLs (optional) (default: false) + --show-sensitive Show sensitive data found in JS files (optional) (default: false) + --cookie value Custom cookie to include in the request (optional) + --help, -h show help + +``` + +### Example: + +``` +./jsrecon -u https://www.tiktok.com --show-sensitive --output results.txt --show-as-domain + +Data saved to results.txt + +IP Address: 1.0.0.73 +IP Address: 1.0.1.234 +API Key: 3319de946467a5e2530ff6f04830521452419c9a548f85fca089ebc9cf8c22a8 +Credential: username +Credential: Username +Credential: Password +Credential: password +Email Address: roaogardo@gmail.com +Email Address: nemilio@tripon-entertainment.com +Email Address: smashingpencilsart@gmail.com +Email Address: Raziirawani@gmail.com +Email Address: nbellarteskids@gmail.com +API Key: 2023101515264400AB6AE6E1431E45CF25 +API Key: 858a8ca65482457eac325ed2eeb463b0 +API Key: f0dae91b3b5c2419f57f9e25a02df551 +API Key: 47ee01b829cee66c47ef333f6fd4d7bb +API Key: f549fe8da2aebb5b2bae6f5389b6a016 + +... + +IP Address: 1.0.0.201 +Credential: secret +sf16-website-login.neutral.ttwstatic.com +lf16-tiktok-web.tiktokcdn-us.com +im-api-va.tiktok.com +m.tiktok.com +www.tiktok.com +starling-oversea.byteoversea.com +mcs-va-useast2a.tiktokv.com +vmweb-va.byteoversea.com +webcast.tiktok.com +f-p.sgsnssdk.com +sf16-tcc-tos-va.byteoversea.com +api.tiktok.com +``` + +## License + +Distributed under the MIT License. See `LICENSE` for more information. diff --git a/background.png b/background.png new file mode 100644 index 0000000..1c580e7 Binary files /dev/null and b/background.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b71af0b --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/Nemesis0U/JSRecon + +go 1.21.3 + +require ( + github.com/PuerkitoBio/goquery v1.8.1 + github.com/urfave/cli/v2 v2.25.7 +) + +require ( + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/net v0.7.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f67e017 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e5fef18 --- /dev/null +++ b/main.go @@ -0,0 +1,264 @@ +// Created by Nemesis +// Contact: nemesisuks@protonmail.com + +package main + +import ( + "bufio" + "fmt" + "log" + "net/http" + "os" + "regexp" + "strings" + "sync" + + "github.com/PuerkitoBio/goquery" + "github.com/urfave/cli/v2" +) + +// Defines regex patterns as global constants +const ( + urlPattern = `https?://[^\s'"]+` + emailPattern = `\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b` + apiKeyPattern = `(?i)\b[0-9a-f]{32,64}\b` + ipAddressPattern = `(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})` + credentialPattern = `(?i)\b(?:username|password|token|secret)\b` +) + +func main() { + app := &cli.App{ + Name: "JSRecon", + Usage: "Scan and extract endpoint URLs and sensitive data from JS files on a website", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Aliases: []string{"u"}, + Usage: "URL of the website to scan (required)", + Required: true, + }, + &cli.StringFlag{ + Name: "keyword", + Usage: "Keyword to search for in JavaScript code (optional)", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Output file to save the links (optional)", + }, + &cli.BoolFlag{ + Name: "show-as-domain", + Usage: "Show results as domains instead of full URLs (optional)", + }, + &cli.BoolFlag{ + Name: "show-sensitive", + Usage: "Show sensitive data found in JS files (optional)", + }, + &cli.StringFlag{ + Name: "cookie", + Usage: "Custom cookie to include in the request (optional)", + }, + }, + Action: func(c *cli.Context) error { + websiteURL := c.String("url") + keyword := c.String("keyword") + showAsDomain := c.Bool("show-as-domain") + outputFile := c.String("output") + showSensitive := c.Bool("show-sensitive") + customCookie := c.String("cookie") + + client := &http.Client{} + crawlWebsite(client, websiteURL, keyword, outputFile, showAsDomain, showSensitive, customCookie) + + return nil + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func crawlWebsite(client *http.Client, websiteURL, keyword, outputFile string, showAsDomain, showSensitive bool, customCookie string) { + req, err := http.NewRequest("GET", websiteURL, nil) + if err != nil { + log.Fatal(err) + } + + if customCookie != "" { + req.Header.Add("Cookie", customCookie) + } + + response, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + log.Fatalf("Request failed with status code %d", response.StatusCode) + } + + doc, err := goquery.NewDocumentFromReader(response.Body) + if err != nil { + log.Fatal(err) + } + + var links []string + var sensitiveData []string + var wg sync.WaitGroup + + // Defines regex patterns for sensitive data + regexPatterns := map[*regexp.Regexp]string{ + regexp.MustCompile(emailPattern): "Email Address", + regexp.MustCompile(apiKeyPattern): "API Key", + regexp.MustCompile(ipAddressPattern): "IP Address", + regexp.MustCompile(credentialPattern): "Credential", + } + + doc.Find("script").Each(func(index int, scriptElement *goquery.Selection) { + wg.Add(1) + go func(script *goquery.Selection) { + defer wg.Done() + scriptLinks, sensitiveScriptData := processScript(script, keyword, regexPatterns) + links = append(links, scriptLinks...) + sensitiveData = append(sensitiveData, sensitiveScriptData...) + }(scriptElement) + }) + + wg.Wait() + + if outputFile != "" { + dataToSave := make([]string, 0) + + if showSensitive { + for _, data := range sensitiveData { + dataToSave = append(dataToSave, data) + } + } + + for _, link := range links { + if !showSensitive || (showSensitive && !strings.Contains(link, "@")) { + if showAsDomain { + parts := strings.Split(link, "//") + if len(parts) > 1 { + domainParts := strings.Split(parts[1], "/") + if len(domainParts) > 0 { + dataToSave = append(dataToSave, domainParts[0]) + } + } + } else { + dataToSave = append(dataToSave, link) + } + } + } + + if err := saveDataToFile(outputFile, dataToSave); err != nil { + log.Fatal(err) + } + + fmt.Printf("\nData saved to %s\n", outputFile) + } + + printJavaScriptLinks(outputFile, links, sensitiveData, showAsDomain, showSensitive) +} + +func processScript(scriptElement *goquery.Selection, keyword string, regexPatterns map[*regexp.Regexp]string) ([]string, []string) { + scriptText := scriptElement.Text() + scriptLinks := findJavaScriptLinks(scriptText, keyword) + sensitiveScriptData := extractSensitiveData(scriptText, regexPatterns) + return scriptLinks, sensitiveScriptData +} + +func extractSensitiveData(scriptText string, regexPatterns map[*regexp.Regexp]string) []string { + var sensitiveData []string + for pattern, dataType := range regexPatterns { + matches := pattern.FindAllString(scriptText, -1) + for _, match := range matches { + cleanMatch := strings.Trim(match, `" '`) + sensitiveData = append(sensitiveData, dataType+": "+cleanMatch) + } + } + return sensitiveData +} + +func findMatches(text string, pattern *regexp.Regexp) []string { + matches := pattern.FindAllString(text, -1) + var cleanMatches []string + for _, match := range matches { + cleanMatch := strings.Trim(match, `" '`) + cleanMatches = append(cleanMatches, cleanMatch) + } + return cleanMatches +} + +func saveDataToFile(filename string, data []string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + for _, entry := range data { + _, err := writer.WriteString(entry + "\n") + if err != nil { + return err + } + } + writer.Flush() + + return nil +} + +func printJavaScriptLinks(outputFile string, links, sensitiveData []string, showAsDomain, showSensitive bool) { + printedDomains := make(map[string]bool) + uniqueLinks := make(map[string]bool) + uniqueSensitiveData := make(map[string]bool) + + if showSensitive { + for _, data := range sensitiveData { + if !uniqueSensitiveData[data] { + fmt.Println(data) + uniqueSensitiveData[data] = true + } + } + } + + for _, link := range links { + if !showSensitive || (showSensitive && !strings.Contains(link, "@")) { + if showAsDomain { + parts := strings.Split(link, "//") + if len(parts) > 1 { + domainParts := strings.Split(parts[1], "/") + if len(domainParts) > 0 && !printedDomains[domainParts[0]] { + fmt.Println(domainParts[0]) + printedDomains[domainParts[0]] = true + } + } + } else { + if !uniqueLinks[link] { + fmt.Println(link) + uniqueLinks[link] = true + } + } + } + } +} + +func findJavaScriptLinks(scriptText, keyword string) []string { + re := regexp.MustCompile(urlPattern) + matches := re.FindAllString(scriptText, -1) + + var links []string + + for _, match := range matches { + if keyword == "" || strings.Contains(match, keyword) { + cleanLink := strings.Trim(match, `" '`) + links = append(links, cleanLink) + } + } + + return links +}