├── LICENSE ├── README.md ├── SpyDir.py └── plugins ├── AngularReactRoutes.py ├── AspNetMvcPlugin.py ├── CSharpRoute.py ├── FlaskBottleRoutes.py └── Spring_2_5_MVC.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 rreid6818 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 | # SpyDir 2 | The purpose of the SpyDir tool is to extend the functionality of BurpSuite Proxy by automating Forced Browsing. The tool attempts to enumerate application endpoints via an input directory containing the application's source code. The tool provides an option to process files as endpoints, think: ASP, PHP, HTML, or parse files to attempt to enumerate endpoints via plugins, think: MVC. Users may opt to send the discovered endpoints directly to the Burp Spider. 3 | 4 | ## New Features 5 | ### Version 0.8.6 6 | Added a mechanism within SpyDir to handle path variables, thus removing this from the plugin responsibilities. This field requires a JSON object in the format of `{"ITEM_TO_REPLACE": "NEW_VALUE"}`. e.g. SpyDir finds the endpoint: _profile/{userID}_ placing `{"{userID}":"tom"}` in "Path Variables" would result in the endpoint _profile/tom_. 7 | 8 | Modified the AngularRoutes plugin to parse React.js endpoints as well. Name changed to AngularReactRoutes.py. 9 | 10 | New plugin for C# files using `Route(String, ...)`. 11 | 12 | ### Version 0.8.5 13 | Implemented a mechanism to allow users to enable/disable plugins once they are loaded. 14 | 15 | ### Version 0.8.4 16 | Added the ability to consume a single text file to parse previously processed/stored endpoints. This is mostly for folks that aren't comfortable with making Python plugins but still want to use the tool. 17 | 18 | Implemented the ability to persist the extension settings through open/close of Burp Suite. 19 | 20 | ## Plugins 21 | Plugin requirements: 22 | 23 | * Have a `get_name()` function that returns a string with the title of the plugin. 24 | * Have a `run()` function that accepts a list containing the lines of a source file. Return a `list`, `[]`, of endpoints. 25 | * Have a `get_ext()` function that returns a string containing a comma delimited string of file extension type(s). 26 | 27 | ## Requirements 28 | [Jython2.7+](http://www.jython.org/downloads.html) stand-alone jar file. 29 | 30 | ## TODO 31 | 1. Modify the plugin return type to allow the specification of HTTP method passed to the spider. (This will require a custom HTTP request and handler) 32 | 2. Auto-resize window 33 | 3. Export data 34 | -------------------------------------------------------------------------------- /SpyDir.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module contains classes related to Burp Suite extension 3 | """ 4 | from os import walk, path 5 | from json import loads, dumps 6 | from imp import load_source 7 | from burp import (IBurpExtender, IBurpExtenderCallbacks, ITab, 8 | IContextMenuFactory) 9 | 10 | from javax.swing import (JPanel, JTextField, GroupLayout, JTabbedPane, 11 | JButton, JLabel, JScrollPane, JTextArea, 12 | JFileChooser, JCheckBox, JMenuItem, JFrame, JViewport) 13 | 14 | from java.net import URL, MalformedURLException 15 | from java.awt import GridLayout, GridBagLayout, GridBagConstraints, Dimension 16 | 17 | 18 | VERSION = "0.8.7" 19 | 20 | 21 | class BurpExtender(IBurpExtender, IBurpExtenderCallbacks, IContextMenuFactory): 22 | """ 23 | Class contains the necessary function to begin the burp extension. 24 | """ 25 | 26 | def __init__(self): 27 | self.config_tab = None 28 | self.messages = [] 29 | self._callbacks = None 30 | 31 | def registerExtenderCallbacks(self, callbacks): 32 | """ 33 | Default extension method. the objects within are related 34 | to the internal tabs of the extension 35 | """ 36 | self.config_tab = SpyTab(callbacks) 37 | self._callbacks = callbacks 38 | callbacks.addSuiteTab(self.config_tab) 39 | callbacks.registerContextMenuFactory(self) 40 | 41 | def createMenuItems(self, invocation): 42 | """Creates the Burp Menu items""" 43 | context = invocation.getInvocationContext() 44 | if context == invocation.CONTEXT_MESSAGE_EDITOR_REQUEST \ 45 | or context == invocation.CONTEXT_MESSAGE_VIEWER_REQUEST \ 46 | or context == invocation.CONTEXT_PROXY_HISTORY \ 47 | or context == invocation.CONTEXT_TARGET_SITE_MAP_TABLE: 48 | self.messages = invocation.getSelectedMessages() 49 | if len(self.messages) == 1: 50 | return [JMenuItem('Send URL to SpyDir', 51 | actionPerformed=self.pass_url)] 52 | else: 53 | return None 54 | 55 | def pass_url(self, event): 56 | """Handles the menu event""" 57 | self.config_tab.update_url(self.messages) 58 | 59 | 60 | class SpyTab(JPanel, ITab): 61 | """Defines the extension tabs""" 62 | 63 | def __init__(self, callbacks): 64 | super(SpyTab, self).__init__(GroupLayout(self)) 65 | self._callbacks = callbacks 66 | config = Config(self._callbacks, self) 67 | about = About(self._callbacks) 68 | # plugs = Plugins(self._callbacks) 69 | self.tabs = [config, about] 70 | self.j_tabs = self.build_ui() 71 | self.add(self.j_tabs) 72 | 73 | def build_ui(self): 74 | """ 75 | Builds the tabbed pane within the main extension tab 76 | Tabs are Config and About objects 77 | """ 78 | ui_tab = JTabbedPane() 79 | for tab in self.tabs: 80 | ui_tab.add(tab.getTabCaption(), tab.getUiComponent()) 81 | return ui_tab 82 | 83 | def switch_focus(self): 84 | """Terrifically hacked together refresh mechanism""" 85 | self.j_tabs.setSelectedIndex(1) 86 | self.j_tabs.setSelectedIndex(0) 87 | 88 | def update_url(self, host): 89 | """ 90 | Retrieves the selected host information from the menu click 91 | Sends it to the config tab 92 | """ 93 | service = host[0].getHttpService() 94 | url = "%s://%s:%s" % (service.getProtocol(), service.getHost(), 95 | service.getPort()) 96 | self.tabs[0].set_url(url) 97 | 98 | @staticmethod 99 | def getTabCaption(): 100 | """Returns the tab name for the Burp UI""" 101 | return "SpyDir" 102 | 103 | def getUiComponent(self): 104 | """Returns the UI component for the Burp UI""" 105 | return self 106 | 107 | 108 | class Config(ITab): 109 | """Defines the Configuration tab""" 110 | 111 | def __init__(self, callbacks, parent): 112 | # Initialze self stuff 113 | self._callbacks = callbacks 114 | self.config = {} 115 | self.ext_stats = {} 116 | self.url_reqs = [] 117 | self.parse_files = False 118 | self.tab = JPanel(GridBagLayout()) 119 | self.view_port_text = JTextArea("===SpyDir===") 120 | self.delim = JTextField(30) 121 | self.ext_white_list = JTextField(30) 122 | # I'm not sure if these fields are necessary still 123 | # why not just use Burp func to handle this? 124 | # leaving them in case I need it for the HTTP handler later 125 | # self.cookies = JTextField(30) 126 | # self.headers = JTextField(30) 127 | self.url = JTextField(30) 128 | self.parent_window = parent 129 | self.plugins = {} 130 | self.loaded_p_list = set() 131 | self.loaded_plugins = False 132 | self.config['Plugin Folder'] = None 133 | self.double_click = False 134 | self.source_input = "" 135 | self.print_stats = True 136 | self.curr_conf = JLabel() 137 | self.window = JFrame("Select plugins", 138 | preferredSize=(200, 250), 139 | windowClosing=self.p_close) 140 | self.window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE) 141 | self.window.setVisible(False) 142 | self.path_vars = JTextField(30) 143 | 144 | 145 | # Initialize local stuff 146 | tab_constraints = GridBagConstraints() 147 | status_field = JScrollPane(self.view_port_text) 148 | 149 | # Configure view port 150 | self.view_port_text.setEditable(False) 151 | 152 | labels = self.build_ui() 153 | 154 | # Add things to rows 155 | tab_constraints.anchor = GridBagConstraints.FIRST_LINE_END 156 | tab_constraints.gridx = 1 157 | tab_constraints.gridy = 0 158 | tab_constraints.fill = GridBagConstraints.HORIZONTAL 159 | self.tab.add(JButton( 160 | "Resize screen", actionPerformed=self.resize), 161 | tab_constraints) 162 | tab_constraints.gridx = 0 163 | tab_constraints.gridy = 1 164 | tab_constraints.anchor = GridBagConstraints.FIRST_LINE_START 165 | self.tab.add(labels, tab_constraints) 166 | 167 | tab_constraints.gridx = 1 168 | tab_constraints.gridy = 1 169 | tab_constraints.fill = GridBagConstraints.BOTH 170 | tab_constraints.weightx = 1.0 171 | tab_constraints.weighty = 1.0 172 | 173 | tab_constraints.anchor = GridBagConstraints.FIRST_LINE_END 174 | self.tab.add(status_field, tab_constraints) 175 | try: 176 | self._callbacks.customizeUiComponent(self.tab) 177 | except Exception: 178 | pass 179 | 180 | def build_ui(self): 181 | """Builds the configuration screen""" 182 | labels = JPanel(GridLayout(21, 1)) 183 | checkbox = JCheckBox("Attempt to parse files for URL patterns?", 184 | False, actionPerformed=self.set_parse) 185 | stats_box = JCheckBox("Show stats?", True, 186 | actionPerformed=self.set_show_stats) 187 | # The two year old in me is laughing heartily 188 | plug_butt = JButton("Specify plugins location", 189 | actionPerformed=self.set_plugin_loc) 190 | load_plug_butt = JButton("Select plugins", 191 | actionPerformed=self.p_build_ui) 192 | parse_butt = JButton("Parse directory", actionPerformed=self.parse) 193 | clear_butt = JButton("Clear text", actionPerformed=self.clear) 194 | spider_butt = JButton("Send to Spider", actionPerformed=self.scan) 195 | save_butt = JButton("Save config", actionPerformed=self.save) 196 | rest_butt = JButton("Restore config", actionPerformed=self.restore) 197 | source_butt = JButton("Input Source File/Directory", 198 | actionPerformed=self.get_source_input) 199 | 200 | # Build grid 201 | labels.add(source_butt) 202 | labels.add(self.curr_conf) 203 | labels.add(JLabel("String Delimiter:")) 204 | labels.add(self.delim) 205 | labels.add(JLabel("Extension Whitelist:")) 206 | labels.add(self.ext_white_list) 207 | labels.add(JLabel("URL:")) 208 | labels.add(self.url) 209 | labels.add(JLabel("Path Variables")) 210 | labels.add(self.path_vars) 211 | # Leaving these here for now. 212 | # labels.add(JLabel("Cookies:")) 213 | # labels.add(self.cookies) 214 | # labels.add(JLabel("HTTP Headers:")) 215 | # labels.add(self.headers) 216 | labels.add(checkbox) 217 | labels.add(stats_box) 218 | labels.add(plug_butt) 219 | labels.add(parse_butt) 220 | labels.add(JButton("Show all endpoints", 221 | actionPerformed=self.print_endpoints)) 222 | labels.add(clear_butt) 223 | labels.add(spider_butt) 224 | labels.add(JLabel("")) 225 | labels.add(save_butt) 226 | labels.add(rest_butt) 227 | labels.add(load_plug_butt) 228 | # Tool tips! 229 | self.delim.setToolTipText("Use to manipulate the final URL. " 230 | "See About tab for example.") 231 | self.ext_white_list.setToolTipText("Define a comma delimited list of" 232 | " file extensions to parse. Use *" 233 | " to parse all files.") 234 | self.url.setToolTipText("Enter the target URL") 235 | checkbox.setToolTipText("Parse files line by line using plugins" 236 | " to enumerate language/framework specific" 237 | " endpoints") 238 | parse_butt.setToolTipText("Attempt to enumerate application endpoints") 239 | clear_butt.setToolTipText("Clear status window and the parse results") 240 | spider_butt.setToolTipText("Process discovered endpoints") 241 | save_butt.setToolTipText("Saves the current config settings") 242 | rest_butt.setToolTipText("Restores previous config settings:" 243 | "
-Input Directory
-String Delim" 244 | "
-Ext WL
-URL
-Plugins") 245 | source_butt.setToolTipText("Select the application's " 246 | "source directory or file to parse") 247 | self.path_vars.setToolTipText("Supply a JSON object with values" 248 | "for dynamically enumerated query" 249 | "string variables") 250 | 251 | return labels 252 | 253 | def set_url(self, menu_url): 254 | """Changes the configuration URL to the one from the menu event""" 255 | self.url.setText(menu_url) 256 | 257 | # Event functions 258 | def set_parse(self, event): 259 | """ 260 | Handles the click event from the UI checkbox 261 | to attempt code level parsing 262 | """ 263 | self.parse_files = not self.parse_files 264 | if self.parse_files: 265 | if not self.loaded_plugins: 266 | self._plugins_missing_warning() 267 | 268 | def restore(self, event): 269 | """Attempts to restore the previously saved configuration.""" 270 | jdump = None 271 | try: 272 | jdump = loads(self._callbacks.loadExtensionSetting("config")) 273 | except Exception as exc: # Generic exception thrown directly to user 274 | self.update_scroll( 275 | "[!!] Error during restore!\n\tException: %s" % str(exc)) 276 | if jdump is not None: 277 | self.url.setText(jdump.get('URL')) 278 | # self.cookies.setText(jdump.get('Cookies')) 279 | # self.headers.setText(jdump.get("Headers")) 280 | ewl = "" 281 | for ext in jdump.get('Extension Whitelist'): 282 | ewl += ext + ", " 283 | self.ext_white_list.setText(ewl[:-2]) 284 | self.delim.setText(jdump.get('String Delimiter')) 285 | self.source_input = jdump.get("Input Directory") 286 | self.config['Plugin Folder'] = jdump.get("Plugin Folder") 287 | if (self.config['Plugin Folder'] is not None and 288 | (len(self.plugins.values()) < 1)): 289 | self._load_plugins(self.config['Plugin Folder']) 290 | self._update() 291 | self.update_scroll("[^] Restore complete!") 292 | else: 293 | self.update_scroll("[!!] Restore failed!") 294 | 295 | def save(self, event=None): 296 | """ 297 | Saves the configuration details to a Burp Suite's persistent store. 298 | """ 299 | self._update() 300 | try: 301 | if not self._callbacks.isInScope(URL(self.url.getText())): 302 | self.update_scroll("[!!] URL provided is NOT in Burp Scope!") 303 | except MalformedURLException: # If url field is blank we'll 304 | pass # still save the settings. 305 | try: 306 | self._callbacks.saveExtensionSetting("config", dumps(self.config)) 307 | self.update_scroll("[^] Settings saved!") 308 | except Exception: 309 | self.update_scroll("[!!] Error saving settings to Burp Suite!") 310 | 311 | def parse(self, event): 312 | """ 313 | Handles the click event from the UI. 314 | Attempts to parse the given directory 315 | (and/or source files) for url endpoints 316 | Saves the items found within the url_reqs list 317 | """ 318 | self._update() 319 | 320 | file_set = set() 321 | fcount = 0 322 | other_dirs = set() 323 | self.ext_stats = {} 324 | if self.loaded_plugins: 325 | self.update_scroll("[^] Attempting to parse files" + 326 | " for URL patterns. This might take a minute.") 327 | if path.isdir(self.source_input): 328 | for dirname, _, filenames in walk(self.source_input): 329 | for filename in filenames: 330 | fcount += 1 331 | ext = path.splitext(filename)[1] 332 | count = self.ext_stats.get(ext, 0) + 1 333 | filename = "%s/%s" % (dirname, filename) 334 | self.ext_stats.update({ext: count}) 335 | if self.parse_files and self._ext_test(ext): 336 | # i can haz threading? 337 | file_set.update(self._code_as_endpoints(filename, ext)) 338 | elif self._ext_test(ext): 339 | r_files, oths = self._files_as_endpoints(filename, ext) 340 | file_set.update(r_files) 341 | other_dirs.update(oths) 342 | elif path.isfile(self.source_input): 343 | ext = path.splitext(self.source_input)[1] 344 | file_set.update(self._code_as_endpoints(self.source_input, ext)) 345 | else: 346 | self.update_scroll("[!!] Input Directory is not valid!") 347 | if len(other_dirs) > 0: 348 | self.update_scroll("[*] Found files matching file extension in:\n") 349 | for other_dir in other_dirs: 350 | self.update_scroll(" " * 4 + "%s\n" % other_dir) 351 | self._handle_path_vars(file_set) 352 | self._print_parsed_status(fcount) 353 | return (other_dirs, self.url_reqs) 354 | 355 | def _handle_path_vars(self, file_set): 356 | proto = 'http://' 357 | for item in file_set: 358 | if item.startswith("http://") or item.startswith("https://"): 359 | proto = item.split("//")[0] + '//' 360 | item = item.replace(proto, "") 361 | item = self._path_vars(item) 362 | self.url_reqs.append(proto + item.replace('//', '/')) 363 | 364 | def _path_vars(self, item): 365 | p_vars = None 366 | if self.path_vars.getText(): 367 | try: 368 | p_vars = loads(str(self.path_vars.getText())) 369 | except: 370 | self.update_scroll("[!] Error reading supplied Path Variables!") 371 | if p_vars is not None: 372 | rep_str = "" 373 | try: 374 | for k in p_vars.keys(): 375 | rep_str += "[^] Replacing %s with %s!\n" % (k, str(p_vars.get(k))) 376 | self.update_scroll(rep_str) 377 | for k in p_vars.keys(): 378 | if str(k) in item: 379 | item = item.replace(k, str(p_vars.get(k))) 380 | except AttributeError: 381 | self.update_scroll("[!] Error reading supplied Path Variables! This needs to be a JSON dictionary!") 382 | return item 383 | 384 | 385 | def scan(self, event): 386 | """ 387 | handles the click event from the UI. 388 | Adds the given URL to the burp scope and sends the requests 389 | to the burp spider 390 | """ 391 | temp_url = self.url.getText() 392 | if not self._callbacks.isInScope(URL(temp_url)): 393 | if not self.double_click: 394 | self.update_scroll("[!!] URL is not in scope! Press Send to " 395 | "Spider again to add to scope and scan!") 396 | self.double_click = True 397 | return 398 | else: 399 | self._callbacks.sendToSpider(URL(temp_url)) 400 | self.update_scroll( 401 | "[^] Sending %d requests to Spider" % len(self.url_reqs)) 402 | for req in self.url_reqs: 403 | self._callbacks.sendToSpider(URL(req)) 404 | 405 | def clear(self, event): 406 | """Clears the viewport and the current parse exts""" 407 | self.view_port_text.setText("===SpyDir===") 408 | self.ext_stats = {} 409 | 410 | def print_endpoints(self, event): 411 | """Prints the discovered endpoints to the status window.""" 412 | req_str = "" 413 | if len(self.url_reqs) > 0: 414 | self.update_scroll("[*] Printing all discovered endpoints:") 415 | for req in sorted(self.url_reqs): 416 | req_str += " %s\n" % req 417 | else: 418 | req_str = "[!!] No endpoints discovered" 419 | self.update_scroll(req_str) 420 | 421 | def set_show_stats(self, event): 422 | """Modifies the show stats setting""" 423 | self.print_stats = not self.print_stats 424 | 425 | def get_source_input(self, event): 426 | """Sets the source dir/file for parsing""" 427 | source_chooser = JFileChooser() 428 | source_chooser.setFileSelectionMode( 429 | JFileChooser.FILES_AND_DIRECTORIES) 430 | source_chooser.showDialog(self.tab, "Choose Source Location") 431 | chosen_source = source_chooser.getSelectedFile() 432 | try: 433 | self.source_input = chosen_source.getAbsolutePath() 434 | except AttributeError: 435 | pass 436 | if self.source_input is not None: 437 | self.update_scroll("[*] Source location: %s" % self.source_input) 438 | self.curr_conf.setText(self.source_input) 439 | 440 | # Plugin functions 441 | def _parse_file(self, filename, file_url): 442 | """ 443 | Attempts to parse a file with the loaded plugins 444 | Returns set of endpoints 445 | """ 446 | file_set = set() 447 | with open(filename, 'r') as plug_in: 448 | lines = plug_in.readlines() 449 | ext = path.splitext(filename)[1].upper() 450 | if ext in self.plugins.keys() and self._ext_test(ext): 451 | for plug in self.plugins.get(ext): 452 | if plug.enabled: 453 | res = plug.run(lines) 454 | if len(res) > 0: 455 | for i in res: 456 | i = file_url + i 457 | file_set.add(i) 458 | elif ext == '.TXT' and self._ext_test(ext): 459 | for i in lines: 460 | i = file_url + i 461 | file_set.add(i.strip()) 462 | return file_set 463 | 464 | def set_plugin_loc(self, event): 465 | """Attempts to load plugins from a specified location""" 466 | if self.config['Plugin Folder'] is not None: 467 | choose_plugin_location = JFileChooser(self.config['Plugin Folder']) 468 | else: 469 | choose_plugin_location = JFileChooser() 470 | choose_plugin_location.setFileSelectionMode( 471 | JFileChooser.DIRECTORIES_ONLY) 472 | choose_plugin_location.showDialog(self.tab, "Choose Folder") 473 | chosen_folder = choose_plugin_location.getSelectedFile() 474 | self.config['Plugin Folder'] = chosen_folder.getAbsolutePath() 475 | self._load_plugins(self.config['Plugin Folder']) 476 | 477 | def _load_plugins(self, folder): 478 | """ 479 | Parses a local directory to get the plugins 480 | related to code level scanning 481 | """ 482 | report = "" 483 | if len(self.plugins.keys()) > 0: 484 | report = "[^] Plugins reloaded!" 485 | for _, _, filenames in walk(folder): 486 | for p_name in filenames: 487 | n_e = path.splitext(p_name) # n_e = name_extension 488 | if n_e[1] == ".py": 489 | f_loc = "%s/%s" % (folder, p_name) 490 | loaded_plug = self._validate_plugin(n_e[0], f_loc) 491 | if loaded_plug: 492 | for p in self.loaded_p_list: 493 | if p.get_name() == loaded_plug.get_name(): 494 | self.loaded_p_list.discard(p) 495 | self.loaded_p_list.add(loaded_plug) 496 | if not report.startswith("[^]"): 497 | report += "%s loaded\n" % loaded_plug.get_name() 498 | 499 | self._dictify(self.loaded_p_list) 500 | if len(self.plugins.keys()) > 0: 501 | self.loaded_plugins = True 502 | else: 503 | report = "[!!] Plugins load failure" 504 | self.loaded_plugins = False 505 | self.update_scroll(report) 506 | return report 507 | 508 | def _validate_plugin(self, p_name, f_loc): 509 | """ 510 | Attempts to verify the manditory plugin functions to prevent broken 511 | plugins from loading. 512 | Generates an error message if plugin does not contain an appropriate 513 | function. 514 | """ 515 | # Load the plugin 516 | try: 517 | plug = load_source(p_name, f_loc) 518 | except Exception as exc: # this needs to be generic. 519 | self.update_scroll( 520 | "[!!] Error loading: %s\n\tType:%s Error: %s" 521 | % (f_loc, type(exc), str(exc))) 522 | # Verify the plugin's functions 523 | funcs = dir(plug) 524 | err = [] 525 | if "get_name" not in funcs: 526 | err.append("get_name()") 527 | if "get_ext" not in funcs: 528 | err.append("get_ext()") 529 | if "run" not in funcs: 530 | err.append("run()") 531 | 532 | # Report errors & return 533 | if len(err) < 1: 534 | return Plugin(plug, True) 535 | for issue in err: 536 | self.update_scroll("[!!] %s is missing: %s func" % 537 | (p_name, issue)) 538 | return None 539 | 540 | def _dictify(self, plist): 541 | """Converts the list of loaded plugins (plist) into a dictionary""" 542 | for p in plist: 543 | exts = p.get_ext().upper() 544 | for ext in exts.split(","): 545 | prev_load = self.plugins.get(ext, []) 546 | prev_load.append(p) 547 | self.plugins[ext] = prev_load 548 | 549 | # Status window functions 550 | def _print_parsed_status(self, fcount): 551 | """Prints the parsed directory status information""" 552 | if self.parse_files and not self.loaded_plugins: 553 | self._plugins_missing_warning() 554 | if len(self.url_reqs) > 0: 555 | self.update_scroll("[*] Example URL: %s" % self.url_reqs[0]) 556 | 557 | if self.print_stats: 558 | report = (("[*] Found: %r files to be requested.\n\n" + 559 | "[*] Stats: \n " + 560 | "Found: %r files.\n") % (len(self.url_reqs), fcount)) 561 | if len(self.ext_stats) > 0: 562 | report += ("[*] Extensions found: %s" 563 | % str(dumps(self.ext_stats, 564 | sort_keys=True, indent=4))) 565 | else: 566 | report = ("[*] Found: %r files to be requested.\n" % 567 | len(self.url_reqs)) 568 | self.update_scroll(report) 569 | return report 570 | 571 | def _plugins_missing_warning(self): 572 | """Prints a warning message""" 573 | self.update_scroll("[!!] No plugins loaded!") 574 | 575 | def update_scroll(self, text): 576 | """Updates the view_port_text with the new information""" 577 | temp = self.view_port_text.getText().strip() 578 | if text not in temp or text[0:4] == "[!!]": 579 | self.view_port_text.setText("%s\n%s" % (temp, text)) 580 | elif not temp.endswith("[^] Status unchanged"): 581 | self.view_port_text.setText("%s\n[^] Status unchanged" % temp) 582 | 583 | # Internal functions 584 | def _code_as_endpoints(self, filename, ext): 585 | file_set = set() 586 | file_url = self.config.get("URL") 587 | if self.loaded_plugins or ext == '.txt': 588 | if self._ext_test(ext): 589 | file_set.update( 590 | self._parse_file(filename, file_url)) 591 | else: 592 | file_set.update( 593 | self._parse_file(filename, file_url)) 594 | return file_set 595 | 596 | def _files_as_endpoints(self, filename, ext): 597 | """Generates endpoints via files with the appropriate extension(s)""" 598 | file_url = self.config.get("URL") 599 | broken_splt = "" 600 | other_dirs = set() # directories outside of the String Delim. 601 | file_set = set() 602 | str_del = self.config.get("String Delimiter") 603 | if not str_del: 604 | self.update_scroll("[!!] No available String Delimiter!") 605 | return 606 | spl_str = filename.split(str_del) 607 | 608 | try: 609 | # Fix for index out of bounds exception while parsing 610 | # subfolders _not_ included by the split 611 | if len(spl_str) > 1: 612 | file_url += ((spl_str[1]) 613 | .replace('\\', '/')) 614 | else: 615 | broken_splt = filename.split(self.source_input)[1] 616 | other_dirs.add(broken_splt) 617 | except Exception as exc: # Generic exception thrown directly to user 618 | self.update_scroll("[!!] Error parsing: " + 619 | "%s\n\tException: %s" 620 | % (filename, str(exc))) 621 | if self._ext_test(ext): 622 | if file_url != self.config.get("URL"): 623 | file_set.add(file_url) 624 | else: 625 | other_dirs.discard(broken_splt) 626 | return file_set, other_dirs 627 | 628 | def _ext_test(self, ext): 629 | """Litmus test for extension whitelist""" 630 | val = False 631 | if "*" in self.config.get("Extension Whitelist"): 632 | val = True 633 | else: 634 | val = (len(ext) > 0 and 635 | (ext.strip().upper() 636 | in self.config.get("Extension Whitelist"))) 637 | return val 638 | 639 | def _update(self): 640 | """Updates internal data""" 641 | self.config["Input Directory"] = self.source_input 642 | self.config["String Delimiter"] = self.delim.getText() 643 | 644 | white_list_text = self.ext_white_list.getText() 645 | self.config["Extension Whitelist"] = white_list_text.upper().split(',') 646 | file_url = self.url.getText() 647 | if not (file_url.startswith('https://') or file_url.startswith('http://')): 648 | self.update_scroll("[!] Assuming protocol! Default value: 'http://'") 649 | file_url = 'http://' + file_url 650 | self.url.setText(file_url) 651 | 652 | if not file_url.endswith('/') and file_url != "": 653 | file_url += '/' 654 | 655 | self.config["URL"] = file_url 656 | # self.config["Cookies"] = self.cookies.getText() 657 | # self.config["Headers"] = self.headers.getText() 658 | del self.url_reqs[:] 659 | self.curr_conf.setText(self.source_input) 660 | 661 | # Window sizing functions 662 | def resize(self, event): 663 | """Resizes the window to better fit Burp""" 664 | if self.parent_window is not None: 665 | par_size = self.parent_window.getSize() 666 | par_size.setSize(par_size.getWidth() * .99, 667 | par_size.getHeight() * .9) 668 | self.tab.setPreferredSize(par_size) 669 | self.parent_window.validate() 670 | self.parent_window.switch_focus() 671 | 672 | def p_close(self, event): 673 | """ 674 | Handles the window close event. 675 | """ 676 | self.window.setVisible(False) 677 | self.window.dispose() 678 | 679 | def p_build_ui(self, event): 680 | """ 681 | Adds a list of checkboxes, one for each loaded plugin 682 | to the Selct plugins window 683 | """ 684 | if not self.loaded_p_list: 685 | self.update_scroll("[!!] No plugins loaded!") 686 | return 687 | 688 | scroll_pane = JScrollPane() 689 | scroll_pane.setPreferredSize(Dimension(200, 250)) 690 | check_frame = JPanel(GridBagLayout()) 691 | constraints = GridBagConstraints() 692 | constraints.fill = GridBagConstraints.HORIZONTAL 693 | constraints.gridy = 0 694 | constraints.anchor = GridBagConstraints.FIRST_LINE_START 695 | 696 | for plug in self.loaded_p_list: 697 | check_frame.add(JCheckBox(plug.get_name(), plug.enabled, 698 | actionPerformed=self.update_box), 699 | constraints) 700 | constraints.gridy += 1 701 | 702 | vport = JViewport() 703 | vport.setView(check_frame) 704 | scroll_pane.setViewport(vport) 705 | self.window.contentPane.add(scroll_pane) 706 | self.window.pack() 707 | self.window.setVisible(True) 708 | 709 | def update_box(self, event): 710 | """ 711 | Handles the check/uncheck event for the plugin's box. 712 | """ 713 | for plug in self.loaded_p_list: 714 | if plug.get_name() == event.getActionCommand(): 715 | plug.enabled = not plug.enabled 716 | if plug.enabled: 717 | self.update_scroll("[^] Enabled: %s" % 718 | event.getActionCommand()) 719 | else: 720 | self.update_scroll("[^] Disabled: %s" % 721 | event.getActionCommand()) 722 | 723 | # ITab required functions 724 | @staticmethod 725 | def getTabCaption(): 726 | """Returns the name of the Burp Suite Tab""" 727 | return "SpyDir" 728 | 729 | def getUiComponent(self): 730 | """Returns the UI component for the Burp Suite tab""" 731 | return self.tab 732 | 733 | 734 | class About(ITab): 735 | """Defines the About tab""" 736 | 737 | def __init__(self, callbacks): 738 | self._callbacks = callbacks 739 | self.tab = JPanel(GridBagLayout()) 740 | 741 | about_constraints = GridBagConstraints() 742 | 743 | about_author = (("

SpyDir

Version: " 744 | "%s
Created by: Ryan Reid" 745 | " (@_aur3lius)
https://github.com/aur3lius-dev/" 746 | "SpyDir

") 747 | % VERSION) 748 | about_spydir = """
749 | SpyDir is an extension that assists in the enumeration of 750 | application
751 | endpoints via an input directory containing the application's
752 | source code. It provides an option to process files as endpoints,
753 | think: ASP, PHP, HTML, or parse files to attempt to enumerate
754 | endpoints via plugins, think: MVC. Users may opt to send the
755 | discovered endpoints directly to the Burp Spider. 756 |

757 | This tool is in Alpha! Please provide feedback on the 758 | GitHub page!

""" 759 | getting_started = """Getting started:
760 | 772 | """ 773 | advanced_info = r"""String Delimiter
774 | String Delimiter allows us to append the necessary section 775 | of the folder structure.
776 | Suppose the target application is hosted at the following URL: 777 | https://localhost:8080.
The target code base is stored in: 778 | 'C:\Source\TestApp'.
Within the TestApp folder there is a 779 | subfolder, 'views', with static .html files.
780 | In this case the String Delimiter will need to equal 'TestApp'. 781 |
With the expectation that the tool will produce an example URL 782 | will such as:
https://localhost:8080/views/view1.html.

783 | Path Vars
Use this option to swap values for dynamically 784 | enumerated query string parameters. This needs to be a JSON object. 785 |
Example:{"{userID}": "aur3lius", "admin_status=": 786 | "admin_status=True"}

787 | 788 | 789 | Note: String Delimiter is ignored if parsing files using 790 | plugins! 791 | """ 792 | about_constraints.anchor = GridBagConstraints.FIRST_LINE_START 793 | about_constraints.weightx = 1.0 794 | about_constraints.weighty = 1.0 795 | self.tab.add(JLabel("%s\n%s\n%s\n%s" % (about_author, about_spydir, 796 | getting_started, 797 | advanced_info)), 798 | about_constraints) 799 | 800 | @staticmethod 801 | def getTabCaption(): 802 | """Returns name of tab for Burp UI""" 803 | return "About" 804 | 805 | def getUiComponent(self): 806 | """Returns UI component for Burp UI""" 807 | return self.tab 808 | 809 | 810 | class Plugin(): 811 | """Defines attributes for loaded extensions""" 812 | 813 | def __init__(self, plugin, enabled): 814 | self.plug = plugin 815 | self.name = plugin.get_name() 816 | self.exts = plugin.get_ext() 817 | self.enabled = enabled 818 | 819 | def run(self, lines): 820 | """Runs the plugin""" 821 | return self.plug.run(lines) 822 | 823 | def get_name(self): 824 | """Returns the name of the plugin""" 825 | return self.name 826 | 827 | def get_ext(self): 828 | """Returns the extension of the plugin""" 829 | return self.exts 830 | -------------------------------------------------------------------------------- /plugins/AngularReactRoutes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | This is a sample plugin for Angular applications using ui-routing. 5 | Now handles some React router cases too! Yay efficiency! 6 | It's not going to find everything! 7 | """ 8 | import re 9 | 10 | def get_ext(): 11 | """Returns the file extensions associated with this plugin""" 12 | return ".js" 13 | 14 | def run(filename): 15 | """ 16 | SpyDir Extension method contains main function to 17 | process Angular and React Routes 18 | """ 19 | ang_str = re.compile(r"(url:\s*')(/.*)(')") 20 | react_str = re.compile(r"(Route[\s]+path[\s]?=['\"])([^'\"\s]+)(['\"]+)") 21 | route_list = set() 22 | 23 | for line in filename: 24 | line = line.strip() 25 | if not line.startswith("//"): # Avoid commented lines. We want the real thing. 26 | route = None 27 | ang_find = ang_str.search(line) 28 | react_find = react_str.search(line) 29 | if ang_find: 30 | route = ret_route(ang_find) 31 | elif react_find: 32 | route = ret_route(react_find) 33 | if route is not None: 34 | route_list.add(route) 35 | return list(route_list) 36 | 37 | def ret_route(found): 38 | """returns the route for both rules""" 39 | if len(found.group(2).strip()) > 1: 40 | return "#" + str(found.group(2).strip()) 41 | 42 | 43 | 44 | def get_name(): 45 | """SpyDir Extension method used to return the name""" 46 | return "Angular/React Routing" 47 | -------------------------------------------------------------------------------- /plugins/AspNetMvcPlugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | This is a sample plugin for ASP.NET MVC applications. 5 | It's not going to find everything! 6 | """ 7 | import re 8 | from random import randint 9 | 10 | 11 | class Status(): 12 | """Sample class to parse HTTP method""" 13 | def __init__(self): 14 | """Init""" 15 | self.last_lines = [] 16 | self.handle_method = False 17 | 18 | def handle_http_method(self): 19 | """find http method""" 20 | http_meth = "" 21 | if self.handle_method: 22 | for prev_line in self.last_lines: 23 | if "HttpPost" in prev_line: 24 | http_meth = "POST" 25 | break 26 | else: 27 | http_meth = "GET" 28 | return http_meth 29 | 30 | 31 | def param_parse(params): 32 | """ 33 | Function to parse and provide random values for parameters of ActionResults 34 | Only handles certain builtin types within ASP.NET MVC! 35 | Returns a dictionary of parameter name and the "generated" value 36 | """ 37 | results = {} 38 | for p in params.split(','): 39 | if '?' in p: 40 | p = p.replace('?', '') 41 | if 'bool' in p: 42 | pname = p.split('bool')[1] 43 | val = "false" 44 | elif 'sbyte' in p: 45 | pname = p.split('sbyte')[1] 46 | val = '123' 47 | elif 'int' in p: 48 | pname = p.split('int ')[1] 49 | val = randint(-2147483648, 2147483647) 50 | elif 'string' in p: 51 | pname = p.split('string ')[1] 52 | val = "" 53 | else: 54 | pname = p.split()[1] 55 | val = "" 56 | if '=' in pname: 57 | pname = pname.split('=')[0].strip() 58 | pname = pname.strip() 59 | results[pname] = val 60 | 61 | return results 62 | 63 | def get_ext(): 64 | """returns the extensions associated with this plugin""" 65 | return ".cs" 66 | 67 | def run(filename): 68 | """ 69 | MUST HAVE FUNCTION! 70 | Begins the plugin processing 71 | Returns a list of endpoints 72 | """ 73 | run_results = [] 74 | url = None 75 | cont = None 76 | # location isn't currently used 77 | location = "" 78 | prog = re.compile(r"((\s:\s){1}(.)*Controller)", flags=re.IGNORECASE) 79 | stats = Status() 80 | 81 | for line in filename: 82 | try: 83 | if prog.search(line): 84 | cont = line.split("Controller")[0].split("class ")[1] 85 | if cont: 86 | stats.last_lines.append(line) 87 | if " ActionResult " in line and cont: 88 | params = line.split("(")[1].split(")")[0] 89 | action_point = line.split("ActionResult ")[1].split("(")[0] 90 | http_meth = stats.handle_http_method() 91 | if params: 92 | p_string = "?" 93 | for k, v in param_parse(params).items(): 94 | p_string += '%s=%s&' % (k, v) 95 | url = "%s/%s/%s%s\t%s" % (location, 96 | cont, action_point, 97 | p_string[:-1], http_meth) 98 | else: 99 | url = "%s/%s/%s\t%s" % (location, 100 | cont, action_point, http_meth) 101 | if url is not None: 102 | run_results.append(url.strip()) 103 | url = None 104 | except Exception as e: 105 | # Print the offending line the BurpSuite's extension Output tab 106 | print("Error! Couldn't parse: %s" % line) 107 | return run_results 108 | 109 | 110 | def get_name(): 111 | """MUST HAVE FUNCTION! Returns plugin name.""" 112 | return "ASP.NET MVC" 113 | -------------------------------------------------------------------------------- /plugins/CSharpRoute.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | This is a sample plugin for ASP.NET MVC applications. 5 | It's not going to find everything! 6 | """ 7 | import re 8 | 9 | def get_ext(): 10 | """Returns the ext associated with this plugin""" 11 | return ".cs" 12 | 13 | def run(filename): 14 | """ 15 | MUST HAVE FUNCTION! 16 | Begins the plugin processing 17 | Returns a list of endpoints 18 | """ 19 | run_results = set() 20 | r_rule = re.compile(r"(Route\(\"[^,)]+)", flags=re.IGNORECASE) 21 | 22 | for line in filename: 23 | try: 24 | route_match = r_rule.search(line) 25 | if route_match: 26 | run_results.add(route_match.group(1)[7:-1]) 27 | except Exception: 28 | # Print the offending line the BurpSuite's extension Output tab 29 | print("Error! Couldn't parse: %s" % line) 30 | return list(run_results) 31 | 32 | 33 | def get_name(): 34 | """MUST HAVE FUNCTION! Returns plugin name.""" 35 | return "C# Route" 36 | -------------------------------------------------------------------------------- /plugins/FlaskBottleRoutes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | import re 4 | from random import randint 5 | 6 | # Define on a per app basis: dictionary of {var_name: str(type)} 7 | #PATH_VARS = {'unknown': 'int'} 8 | PATH_VARS = {'': 'str'} 9 | 10 | 11 | def handle_path_vars(var_names): 12 | """ 13 | Handles the path vars found during run 14 | Returns dict of {var_name: 'random' value} 15 | """ 16 | ret_val = {} 17 | for var in var_names: 18 | if var in PATH_VARS.keys(): 19 | if PATH_VARS[var] == "int": 20 | ret_val[var] = randint(0, 47) 21 | elif PATH_VARS[var] == 'str': 22 | ret_val[var] = "test" 23 | # Define more based on need 24 | else: 25 | ret_val[var] = "" 26 | return ret_val 27 | 28 | def get_ext(): 29 | """Defines the extension type expected within SpyDir""" 30 | return ".py" 31 | 32 | def run(filename): 33 | """ 34 | SpyDir Extension method contains main function to 35 | process Flask/Bottle Routes 36 | """ 37 | route_rule = r"""^@*(app\.route\(|^bottle\.route\(|route\()""" 38 | path_rule = r"(<\w+>)" 39 | route_list = [] 40 | 41 | for line in filename: 42 | line = line.replace("'", '"').replace('"', "").strip() 43 | if re.search(route_rule, line): 44 | if "methods" in line: # this is ignored currently 45 | methods = line.split("[")[1].split("]")[0].split(",") 46 | line = re.split(route_rule, line)[2].split(")")[0].split(',')[0] 47 | if re.search(path_rule, line): 48 | path_values = handle_path_vars(re.findall(path_rule, line)) 49 | for k, v in path_values.items(): 50 | line = line.replace(k, str(v)) 51 | route_list.append(line) 52 | return route_list 53 | 54 | 55 | def get_name(): 56 | """SpyDir Extension method used to return the name""" 57 | return "Flask/Bottle Routes" 58 | -------------------------------------------------------------------------------- /plugins/Spring_2_5_MVC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | This is a sample plugin for Spring Framework 2.5+ MVC applications. 5 | It's not going to find everything! 6 | """ 7 | import re 8 | 9 | 10 | def set_path_vars(): 11 | """Update these variables and values on a per app basis.""" 12 | return { 13 | "{id}": 2, 14 | "{eventId}": 3, 15 | "{receiver}": "Steve", 16 | "{user}": "Bob", 17 | "{userId}": 4, 18 | "{friend}": "Paul", 19 | "{owner}": "Tom", 20 | "{name}": "John", 21 | "{amount}": "3.50", 22 | "{hidden}": "secret", 23 | "{oldPassword}": "12345", 24 | "{newPassword}": "hunter2"} 25 | 26 | 27 | def set_param_vals(): 28 | return { 29 | "{String}": "str12345", 30 | "{int}": 12345, 31 | "{Long}": 9012310231013 32 | } 33 | 34 | def handle_params(params): 35 | assignment = "" 36 | reg = re.compile('(.*?"\))([\s])(.*?)(,|\))') 37 | for par in params: 38 | par_find = reg.search(par) 39 | if par_find: 40 | par_name = par_find.group(1).replace('"', "").replace(")", "") 41 | par_type = par_find.group(3).split()[0].strip() 42 | assignment += "%s={%s}&" % (par_name, par_type) 43 | for k, v in set_param_vals().items(): 44 | assignment = assignment.replace(k, str(v)) 45 | return assignment[:-1] 46 | 47 | 48 | def handle_path_vars(route): 49 | """ 50 | Replaces the placeholder variables with values from set_path_vars() 51 | Returns a string containing the updated route 52 | """ 53 | new_route = route 54 | for k, v in set_path_vars().items(): 55 | new_route = new_route.replace(k, str(v)) 56 | return new_route 57 | 58 | 59 | def get_ext(): 60 | return ".java" 61 | 62 | 63 | def run(filename): 64 | """ 65 | SpyDir Extension method contains main function to 66 | process Spring 2.5+ MVC Routes 67 | """ 68 | req_map = "@RequestMapping(" 69 | route_rule = re.compile("(value\s*=\s*[{]?)([\"].*[\"])(,|\))|([\"].*[\"])") 70 | path_rule = re.compile("({\w+})") 71 | req_param = "@RequestParam(\"" 72 | 73 | route_list = [] 74 | 75 | for line in filename: 76 | line = line.strip() 77 | if not line.startswith("//"): 78 | route = None 79 | if req_map in line: 80 | line = line.replace(req_map, "").replace(")", "") 81 | val_find = route_rule.search(line) 82 | if val_find: 83 | if val_find.group(2) is not None: 84 | route = val_find.group(2).replace( 85 | "\"", "").strip().split(',')[0] 86 | elif val_find.group(4) is not None: 87 | route = val_find.group(4).replace("\"", "").strip() 88 | if ',' in val_find.group(4): 89 | for r in val_find.group(4).split(','): 90 | r = r.strip().replace('"', '') 91 | route_list.append(r) 92 | route = r 93 | if route is not None: 94 | path_finder = path_rule.search(route) 95 | if path_finder: 96 | route = handle_path_vars(route) 97 | route_list.append(route) 98 | prev_route = route 99 | if req_param in line: 100 | params = line.split(req_param) 101 | w_pars = "%s?%s" % (prev_route, handle_params(params[1:])) 102 | route_list.append(w_pars) 103 | 104 | # Don't currently process methods at the extension level 105 | # mult_method = re.search("([,\s*|)\s*]method\s*=\s*\{.*\})", line) 106 | # if mult_method: 107 | # mult_method = mult_method.group().strip() 108 | route_list.sort() 109 | return route_list 110 | 111 | 112 | def get_name(): 113 | """SpyDir Extension method used to return the name""" 114 | return "Spring 2.5+ MVC" 115 | --------------------------------------------------------------------------------