├── 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 = (("