├── BappDescription.html ├── BappManifest.bmf ├── LICENSE ├── README.md └── FransLinkfinder.py /BappDescription.html: -------------------------------------------------------------------------------- 1 | Burp Extension for a passively scanning JavaScript files for endpoint links. 2 | 3 | - Export results the text file 4 | - Exclude specific 'js' files e.g. jquery, google-analytics 5 | 6 | -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: 0e61c786db0c4ac787a08c4516d52ccf 2 | ExtensionType: 2 3 | Name: JS Link Finder 4 | RepoName: js-link-finder 5 | ScreenVersion: 1.0.0 6 | SerialVersion: 1 7 | MinPlatformVersion: 0 8 | ProOnly: True 9 | Author: InitRoot 10 | ShortDescription: Burp Extension for passively scanning JavaScript files for endpoint links. 11 | EntryPoint: FransLinkfinder.py 12 | BuildCommand: 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 InitRoot 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BurpJSLinkFinder - Find links within JS files. 2 | Burp Extension for a passive scanning JS files for endpoint links. 3 | - Export results the text file 4 | - Exclude specific 'js' files e.g. jquery, google-analytics 5 | 6 | Copyright (c) 2019 Frans Hendrik Botes 7 | 8 | 9 | Credit to https://github.com/GerbenJavado/LinkFinder for the idea and regex 10 | 11 | ## Setup 12 | For use with the professional version of Burp Suite. Ensure you have JPython loaded and setup 13 | before installing. 14 | 15 | You can modify the exclusion list by updating the strings on line 33. 16 | Currently any strings that include the included words will not be analysed. 17 | 18 | ``` 19 | # Needed params 20 | 21 | JSExclusionList = ['jquery', 'google-analytics','gpt.js'] 22 | 23 | ``` 24 | 25 | ## Usage 26 | 27 | Once you've loaded the plugin there is some things to consider. 28 | Burp performs threading on passive scanning by itself. This can be controlled by looking at the Scanner options. 29 | For quick scanning I make use of the following settings with this plugin: 30 | 31 | Scanner --> Live Scanning 32 | - Live Active Scanning : Disabled 33 | - Live Passive Scanning : Use suite scope 34 | 35 | As with ALL the burp scanner items, you have to give it a minute or so to work through the data. You shouldn't be waiting several minutes for a result tho. 36 | 37 | If the links have been excluded monitor the OUTPUT of the extension under the Extender options to verify. 38 | 39 | 40 | ## Screenshot 41 | ![](https://i.imgur.com/KnmJrp1.gif) 42 | 43 | ## Update 44 | - Added swing memory management (14/06/2019) 45 | - Added exclusion list on line 33 of code ['jquery', 'google-analytics','gpt.js'] (14/06/2019) 46 | - Added ability to export files (15/06/2019) 47 | -------------------------------------------------------------------------------- /FransLinkfinder.py: -------------------------------------------------------------------------------- 1 | # 2 | # BurpLinkFinder - Find links within JS files. 3 | # 4 | # Copyright (c) 2019 Frans Hendrik Botes 5 | # Credit to https://github.com/GerbenJavado/LinkFinder for the idea and regex 6 | # 7 | from burp import IBurpExtender, IScannerCheck, IScanIssue, ITab 8 | from java.io import PrintWriter 9 | from java.net import URL 10 | from java.util import ArrayList, List 11 | from java.util.regex import Matcher, Pattern 12 | import binascii 13 | import base64 14 | import re 15 | from javax import swing 16 | from java.awt import Font, Color 17 | from threading import Thread 18 | from array import array 19 | from java.awt import EventQueue 20 | from java.lang import Runnable 21 | from thread import start_new_thread 22 | from javax.swing import JFileChooser 23 | 24 | # Using the Runnable class for thread-safety with Swing 25 | class Run(Runnable): 26 | def __init__(self, runner): 27 | self.runner = runner 28 | 29 | def run(self): 30 | self.runner() 31 | 32 | # Needed params 33 | 34 | JSExclusionList = ['jquery', 'google-analytics','gpt.js'] 35 | 36 | class BurpExtender(IBurpExtender, IScannerCheck, ITab): 37 | def registerExtenderCallbacks(self, callbacks): 38 | self.callbacks = callbacks 39 | self.helpers = callbacks.getHelpers() 40 | callbacks.setExtensionName("BurpJSLinkFinder") 41 | 42 | callbacks.issueAlert("BurpJSLinkFinder Passive Scanner enabled") 43 | 44 | stdout = PrintWriter(callbacks.getStdout(), True) 45 | stderr = PrintWriter(callbacks.getStderr(), True) 46 | callbacks.registerScannerCheck(self) 47 | self.initUI() 48 | self.callbacks.addSuiteTab(self) 49 | 50 | print ("Burp JS LinkFinder loaded.") 51 | print ("Copyright (c) 2019 Frans Hendrik Botes") 52 | self.outputTxtArea.setText("Burp JS LinkFinder loaded." + "\n" + "Copyright (c) 2019 Frans Hendrik Botes" + "\n") 53 | 54 | def initUI(self): 55 | self.tab = swing.JPanel() 56 | 57 | # UI for Output 58 | self.outputLabel = swing.JLabel("LinkFinder Log:") 59 | self.outputLabel.setFont(Font("Tahoma", Font.BOLD, 14)) 60 | self.outputLabel.setForeground(Color(255,102,52)) 61 | self.logPane = swing.JScrollPane() 62 | self.outputTxtArea = swing.JTextArea() 63 | self.outputTxtArea.setFont(Font("Consolas", Font.PLAIN, 12)) 64 | self.outputTxtArea.setLineWrap(True) 65 | self.logPane.setViewportView(self.outputTxtArea) 66 | self.clearBtn = swing.JButton("Clear Log", actionPerformed=self.clearLog) 67 | self.exportBtn = swing.JButton("Export Log", actionPerformed=self.exportLog) 68 | self.parentFrm = swing.JFileChooser() 69 | 70 | 71 | 72 | # Layout 73 | layout = swing.GroupLayout(self.tab) 74 | layout.setAutoCreateGaps(True) 75 | layout.setAutoCreateContainerGaps(True) 76 | self.tab.setLayout(layout) 77 | 78 | layout.setHorizontalGroup( 79 | layout.createParallelGroup() 80 | .addGroup(layout.createSequentialGroup() 81 | .addGroup(layout.createParallelGroup() 82 | .addComponent(self.outputLabel) 83 | .addComponent(self.logPane) 84 | .addComponent(self.clearBtn) 85 | .addComponent(self.exportBtn) 86 | ) 87 | ) 88 | ) 89 | 90 | layout.setVerticalGroup( 91 | layout.createParallelGroup() 92 | .addGroup(layout.createParallelGroup() 93 | .addGroup(layout.createSequentialGroup() 94 | .addComponent(self.outputLabel) 95 | .addComponent(self.logPane) 96 | .addComponent(self.clearBtn) 97 | .addComponent(self.exportBtn) 98 | ) 99 | ) 100 | ) 101 | 102 | def getTabCaption(self): 103 | return "BurpJSLinkFinder" 104 | 105 | def getUiComponent(self): 106 | return self.tab 107 | 108 | def clearLog(self, event): 109 | self.outputTxtArea.setText("Burp JS LinkFinder loaded." + "\n" + "Copyright (c) 2019 Frans Hendrik Botes" + "\n" ) 110 | 111 | def exportLog(self, event): 112 | chooseFile = JFileChooser() 113 | ret = chooseFile.showDialog(self.logPane, "Choose file") 114 | filename = chooseFile.getSelectedFile().getCanonicalPath() 115 | print("\n" + "Export to : " + filename) 116 | open(filename, 'w', 0).write(self.outputTxtArea.text) 117 | 118 | 119 | def doPassiveScan(self, ihrr): 120 | 121 | try: 122 | urlReq = ihrr.getUrl() 123 | testString = str(urlReq) 124 | linkA = linkAnalyse(ihrr,self.helpers) 125 | # check if JS file 126 | if ".js" in str(urlReq): 127 | # Exclude casual JS files 128 | if any(x in testString for x in JSExclusionList): 129 | print("\n" + "[-] URL excluded " + str(urlReq)) 130 | else: 131 | self.outputTxtArea.append("\n" + "[+] Valid URL found: " + str(urlReq)) 132 | issueText = linkA.analyseURL() 133 | for counter, issueText in enumerate(issueText): 134 | #print("TEST Value returned SUCCESS") 135 | self.outputTxtArea.append("\n" + "\t" + str(counter)+' - ' +issueText['link']) 136 | 137 | issues = ArrayList() 138 | issues.add(SRI(ihrr, self.helpers)) 139 | return issues 140 | except UnicodeEncodeError: 141 | print ("Error in URL decode.") 142 | return None 143 | 144 | 145 | def consolidateDuplicateIssues(self, isb, isa): 146 | return -1 147 | 148 | def extensionUnloaded(self): 149 | print "Burp JS LinkFinder unloaded" 150 | return 151 | 152 | class linkAnalyse(): 153 | 154 | def __init__(self, reqres, helpers): 155 | self.helpers = helpers 156 | self.reqres = reqres 157 | 158 | 159 | regex_str = """ 160 | 161 | (?:"|') # Start newline delimiter 162 | 163 | ( 164 | ((?:[a-zA-Z]{1,10}://|//) # Match a scheme [a-Z]*1-10 or // 165 | [^"'/]{1,}\. # Match a domainname (any character + dot) 166 | [a-zA-Z]{2,}[^"']{0,}) # The domainextension and/or path 167 | 168 | | 169 | 170 | ((?:/|\.\./|\./) # Start with /,../,./ 171 | [^"'><,;| *()(%%$^/\\\[\]] # Next character can't be... 172 | [^"'><,;|()]{1,}) # Rest of the characters can't be 173 | 174 | | 175 | 176 | ([a-zA-Z0-9_\-/]{1,}/ # Relative endpoint with / 177 | [a-zA-Z0-9_\-/]{1,} # Resource name 178 | \.(?:[a-zA-Z]{1,4}|action) # Rest + extension (length 1-4 or action) 179 | (?:[\?|/][^"|']{0,}|)) # ? mark with parameters 180 | 181 | | 182 | 183 | ([a-zA-Z0-9_\-]{1,} # filename 184 | \.(?:php|asp|aspx|jsp|json| 185 | action|html|js|txt|xml) # . + extension 186 | (?:\?[^"|']{0,}|)) # ? mark with parameters 187 | 188 | ) 189 | 190 | (?:"|') # End newline delimiter 191 | 192 | """ 193 | 194 | def parser_file(self, content, regex_str, mode=1, more_regex=None, no_dup=1): 195 | #print ("TEST parselfile #2") 196 | regex = re.compile(regex_str, re.VERBOSE) 197 | items = [{"link": m.group(1)} for m in re.finditer(regex, content)] 198 | if no_dup: 199 | # Remove duplication 200 | all_links = set() 201 | no_dup_items = [] 202 | for item in items: 203 | if item["link"] not in all_links: 204 | all_links.add(item["link"]) 205 | no_dup_items.append(item) 206 | items = no_dup_items 207 | 208 | # Match Regex 209 | filtered_items = [] 210 | for item in items: 211 | # Remove other capture groups from regex results 212 | if more_regex: 213 | if re.search(more_regex, item["link"]): 214 | #print ("TEST parselfile #3") 215 | filtered_items.append(item) 216 | else: 217 | filtered_items.append(item) 218 | return filtered_items 219 | 220 | # Potential for use in the future... 221 | def threadAnalysis(self): 222 | thread = Thread(target=self.analyseURL(), args=(session,)) 223 | thread.daemon = True 224 | thread.start() 225 | 226 | def analyseURL(self): 227 | 228 | endpoints = "" 229 | #print("TEST AnalyseURL #1") 230 | mime_type=self.helpers.analyzeResponse(self.reqres.getResponse()).getStatedMimeType() 231 | if mime_type.lower() == 'script': 232 | url = self.reqres.getUrl() 233 | encoded_resp=binascii.b2a_base64(self.reqres.getResponse()) 234 | decoded_resp=base64.b64decode(encoded_resp) 235 | endpoints=self.parser_file(decoded_resp, self.regex_str) 236 | #print("TEST AnalyseURL #2") 237 | return endpoints 238 | return endpoints 239 | 240 | 241 | class SRI(IScanIssue,ITab): 242 | def __init__(self, reqres, helpers): 243 | self.helpers = helpers 244 | self.reqres = reqres 245 | 246 | def getHost(self): 247 | return self.reqres.getHost() 248 | 249 | def getPort(self): 250 | return self.reqres.getPort() 251 | 252 | def getProtocol(self): 253 | return self.reqres.getProtocol() 254 | 255 | def getUrl(self): 256 | return self.reqres.getUrl() 257 | 258 | def getIssueName(self): 259 | return "Linkfinder Analysed JS files" 260 | 261 | def getIssueType(self): 262 | return 0x08000000 # See http:#portswigger.net/burp/help/scanner_issuetypes.html 263 | 264 | def getSeverity(self): 265 | return "Information" # "High", "Medium", "Low", "Information" or "False positive" 266 | 267 | def getConfidence(self): 268 | return "Certain" # "Certain", "Firm" or "Tentative" 269 | 270 | def getIssueBackground(self): 271 | return str("JS files holds links to other parts of web applications. Refer to TAB for results.") 272 | 273 | def getRemediationBackground(self): 274 | return "This is an informational finding only.
" 275 | 276 | def getIssueDetail(self): 277 | return str("Burp Scanner has analysed the following JS file for links: " 278 | "%s

" % (self.reqres.getUrl().toString())) 279 | 280 | def getRemediationDetail(self): 281 | return None 282 | 283 | def getHttpMessages(self): 284 | #print ("................raising issue................") 285 | rra = [self.reqres] 286 | return rra 287 | 288 | def getHttpService(self): 289 | return self.reqres.getHttpService() 290 | 291 | 292 | if __name__ in ('__main__', 'main'): 293 | EventQueue.invokeLater(Run(BurpExtender)) 294 | --------------------------------------------------------------------------------