├── .gitignore ├── LICENSE.md ├── README.md └── burp_git_bridge.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jonathan Foote 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git Bridge extension for Burp Suite Pro 2 | 3 | The Git Bridge plugin lets Burp users store Burp data and collaborate via git. Users can right-click supported items in Burp to send them to a git repo and use the Git Bridge tab to send items back to their respective Burp tools. 4 | 5 | ## How to Use 6 | 7 | ### Load the extension 8 | 9 | Download `burp_git_bridge.py` and load the plugin via the "Extender" tab as usual. Note: This plugin is written in Python so you'll need follow the steps to setup Jython in Burp if you haven't already. 10 | 11 | Git Bridge creates a git repo at `~/.burp_git_bridge`. 12 | 13 | ![](http://foote.pub/images/burp-git/burp-git-install.png) 14 | 15 | ### Store Revisions Locally 16 | 17 | Right click on an interesting Scanner or Repeater item and choose `Send to Git Bridge` 18 | 19 | ![](http://foote.pub/images/burp-git/burp-git-send-to-git.png) 20 | 21 | 22 | ### Share (or Create a Remote Backup of) Burp data 23 | 24 | Open a shell, change directories to the Burp git bridge repo and git it. 25 | 26 | ``` 27 | $ cd ~/.burp_git_bridge 28 | $ git remote add origin ssh://git@github.com/jfoote/burp-git-bridge-test.git 29 | $ git push -u origin master 30 | $ git branch my_findings 31 | ``` 32 | 33 | ![](http://foote.pub/images/burp-git/burp-git-github.png) 34 | 35 | PSA: Only interact with git servers you trust, especially when dealing with sensitive data. 36 | 37 | ### Load Shared Burp data 38 | 39 | Open a shell, change directories to the Burp git bridge repo and issue a pull. 40 | 41 | ``` 42 | $ cd ~/.burp_git_bridge 43 | $ git pull 44 | ``` 45 | 46 | Back in Burp, flip to the "Git Bridge" tab and click "Reload" 47 | 48 | ![](http://foote.pub/images/burp-git/burp-git-reload.png) 49 | 50 | Then send items to their respective tools 51 | 52 | ![](http://foote.pub/images/burp-git/burp-git-send-to-tools.png) 53 | 54 | Burp away 55 | 56 | ![](http://foote.pub/images/burp-git/burp-git-repeater.png) 57 | 58 | ## Notes 59 | 60 | This extension is a PoC. Right now only Repeater and Scanner are supported, 61 | and the code could use refactoring. If you're interested in a more polished 62 | version or more features let me know, or better yet consider sending me a pull request. 63 | 64 | Thanks for checking it out. 65 | 66 | ``` 67 | Jonathan Foote 68 | jmfoote@loyola.edu 69 | 2015-04-21 70 | ``` 71 | -------------------------------------------------------------------------------- /burp_git_bridge.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Git Bridge extension for Burp Suite Pro 3 | 4 | The Git Bridge plugin lets Burp users store and share findings and other Burp 5 | items via git. Users can right-click supported items in Burp to send them to 6 | a git repo and use the Git Bridge tab to send items back to their respective 7 | Burp tools. 8 | 9 | For more information see https://github.com/jfoote/burp-git-bridge. 10 | 11 | This extension is a PoC. Right now only Repeater and Scanner are supported, 12 | and the code could use refactoring. If you're interested in a more polished 13 | version or more features let me know, or better yet consider sending me a pull request. 14 | 15 | Thanks for checking it out. 16 | 17 | Jonathan Foote 18 | jmfoote@loyola.edu 19 | 2015-04-21 20 | ''' 21 | 22 | from burp import IBurpExtender, ITab, IHttpListener, IMessageEditorController, IContextMenuFactory, IScanIssue, IHttpService, IHttpRequestResponse 23 | from java.awt import Component 24 | from java.awt.event import ActionListener 25 | from java.io import PrintWriter 26 | from java.util import ArrayList, List 27 | from java.net import URL 28 | from javax.swing import JScrollPane, JSplitPane, JTabbedPane, JTable, SwingUtilities, JPanel, JButton, JLabel, JMenuItem, BoxLayout 29 | from javax.swing.table import AbstractTableModel 30 | from threading import Lock 31 | import datetime, os, hashlib 32 | import sys 33 | 34 | 35 | ''' 36 | Entry point for Burp Git Bridge extension. 37 | ''' 38 | 39 | class BurpExtender(IBurpExtender): 40 | ''' 41 | Entry point for plugin; creates UI and Log 42 | ''' 43 | 44 | def registerExtenderCallbacks(self, callbacks): 45 | 46 | # Assign stdout/stderr for debugging and set extension name 47 | 48 | sys.stdout = callbacks.getStdout() 49 | sys.stderr = callbacks.getStderr() 50 | callbacks.setExtensionName("Git Bridge") 51 | 52 | 53 | # Create major objects and load user data 54 | 55 | self.log = Log(callbacks) 56 | self.ui = BurpUi(callbacks, self.log) 57 | self.log.setUi(self.ui) 58 | self.log.reload() 59 | 60 | 61 | 62 | ''' 63 | Classes that support logging of data to in-Burp extension UI as well 64 | as the underlying git repo 65 | ''' 66 | 67 | class LogEntry(object): 68 | ''' 69 | Hacky dictionary used to store Burp tool data. Objects of this class 70 | are stored in the Java-style table represented in the Burp UI table. 71 | They are created by the BurpUi when a user sends Burp tool data to Git 72 | Bridge, or by Git Bridge when a user's git repo is reloaded into Burp. 73 | ''' 74 | def __init__(self, *args, **kwargs): 75 | self.__dict__ = kwargs 76 | 77 | 78 | # Hash most of the tool data to uniquely identify this entry. 79 | # Note: Could be more pythonic. 80 | 81 | md5 = hashlib.md5() 82 | for k, v in self.__dict__.iteritems(): 83 | if v and k != "messages": 84 | if not getattr(v, "__getitem__", False): 85 | v = str(v) 86 | md5.update(k) 87 | md5.update(v[:2048]) 88 | self.md5 = md5.hexdigest() 89 | 90 | 91 | 92 | class Log(): 93 | ''' 94 | Log of burp activity: this class encapsulates both the Burp UI log and the git 95 | repo log. A single object of this class is created when the extension is 96 | loaded. It is used by BurpExtender when it logs input events or the 97 | in-Burp Git Bridge log is reloaded from the underlying git repo. 98 | ''' 99 | 100 | def __init__(self, callbacks): 101 | ''' 102 | Creates GUI log and git log objects 103 | ''' 104 | 105 | self.ui = None 106 | self._callbacks = callbacks 107 | self._helpers = callbacks.getHelpers() 108 | self.gui_log = GuiLog(callbacks) 109 | self.git_log = GitLog(callbacks) 110 | 111 | def setUi(self, ui): 112 | ''' 113 | There is a circular dependency between the Log and Burp GUI objects: 114 | the GUI needs a handle to the Log to add new Burp tool data, and the 115 | Log needs a handle to the GUI to update in the in-GUI table. 116 | 117 | The GUI takes the Log in its constructor, and this function gives the 118 | Log a handle to the GUI. 119 | ''' 120 | 121 | self.ui = ui 122 | self.gui_log.ui = ui 123 | 124 | def reload(self): 125 | ''' 126 | Reloads the Log from on the on-disk git repo. 127 | ''' 128 | self.gui_log.clear() 129 | for entry in self.git_log.entries(): 130 | self.gui_log.add_entry(entry) 131 | 132 | def add_repeater_entry(self, messageInfo): 133 | ''' 134 | Loads salient info from the Burp-supplied messageInfo object and 135 | stores it to the GUI and Git logs 136 | ''' 137 | 138 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 139 | service = messageInfo.getHttpService() 140 | entry = LogEntry(tool="repeater", 141 | host=service.getHost(), 142 | port=service.getPort(), 143 | protocol=service.getProtocol(), 144 | url=str(self._helpers.analyzeRequest(messageInfo).getUrl()), 145 | timestamp=timestamp, 146 | who=self.git_log.whoami(), 147 | request=messageInfo.getRequest(), 148 | response=messageInfo.getResponse()) 149 | self.gui_log.add_entry(entry) 150 | self.git_log.add_repeater_entry(entry) 151 | 152 | def add_scanner_entry(self, scanIssue): 153 | ''' 154 | Loads salient info from the Burp-supplied scanInfo object and 155 | stores it to the GUI and Git logs 156 | ''' 157 | 158 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 159 | 160 | # Gather info from messages. Oi, ugly. 161 | 162 | messages = [] 163 | for message in scanIssue.getHttpMessages(): 164 | service = message.getHttpService() 165 | msg_entry = LogEntry(tool="scanner_message", 166 | host=service.getHost(), 167 | port=service.getPort(), 168 | protocol=service.getProtocol(), 169 | comment=message.getComment(), 170 | highlight=message.getHighlight(), 171 | request=message.getRequest(), 172 | response=message.getResponse(), 173 | timestamp=timestamp) 174 | messages.append(msg_entry) 175 | 176 | 177 | # Gather info for scan issue 178 | 179 | service = scanIssue.getHttpService() 180 | entry = LogEntry(tool="scanner", 181 | timestamp=timestamp, 182 | who=self.git_log.whoami(), 183 | messages=messages, 184 | host=service.getHost(), 185 | port=service.getPort(), 186 | protocol=service.getProtocol(), 187 | confidence=scanIssue.getConfidence(), 188 | issue_background=scanIssue.getIssueBackground(), 189 | issue_detail=scanIssue.getIssueDetail(), 190 | issue_name=scanIssue.getIssueName(), 191 | issue_type=scanIssue.getIssueType(), 192 | remediation_background=scanIssue.getRemediationBackground(), 193 | remediation_detail=scanIssue.getRemediationDetail(), 194 | severity=scanIssue.getSeverity(), 195 | url=str(scanIssue.getUrl())) 196 | 197 | self.gui_log.add_entry(entry) 198 | self.git_log.add_scanner_entry(entry) 199 | 200 | def remove(self, entry): 201 | ''' 202 | Removes the supplied entry from the Log 203 | ''' 204 | 205 | self.git_log.remove(entry) 206 | self.gui_log.remove_entry(entry) 207 | 208 | 209 | class GuiLog(AbstractTableModel): 210 | ''' 211 | Acts as an AbstractTableModel for the table that is shown in the UI tab: 212 | when this data structure changes, the in-UI table is updated. 213 | ''' 214 | 215 | def __init__(self, callbacks): 216 | ''' 217 | Creates a Java-style ArrayList to hold LogEntries that appear in the table 218 | ''' 219 | 220 | self.ui = None 221 | self._log = ArrayList() 222 | self._lock = Lock() 223 | self._callbacks = callbacks 224 | self._helpers = callbacks.getHelpers() 225 | 226 | def clear(self): 227 | ''' 228 | Clears all entries from the table 229 | ''' 230 | 231 | self._lock.acquire() 232 | last = self._log.size() 233 | if last > 0: 234 | self._log.clear() 235 | self.fireTableRowsDeleted(0, last-1) 236 | # Note: if callees modify table this could deadlock 237 | self._lock.release() 238 | 239 | def add_entry(self, entry): 240 | ''' 241 | Adds entry to the table 242 | ''' 243 | 244 | self._lock.acquire() 245 | row = self._log.size() 246 | self._log.add(entry) 247 | # Note: if callees modify table this could deadlock 248 | self.fireTableRowsInserted(row, row) 249 | self._lock.release() 250 | 251 | def remove_entry(self, entry): 252 | ''' 253 | Removes entry from the table 254 | ''' 255 | 256 | self._lock.acquire() 257 | for i in range(0, len(self._log)): 258 | ei = self._log[i] 259 | if ei.md5 == entry.md5: 260 | self._log.remove(i) 261 | break 262 | self.fireTableRowsDeleted(i, i) 263 | self._lock.release() 264 | 265 | def getRowCount(self): 266 | ''' 267 | Used by the Java Swing UI 268 | ''' 269 | 270 | try: 271 | return self._log.size() 272 | except: 273 | return 0 274 | 275 | def getColumnCount(self): 276 | ''' 277 | Used by the Java Swing UI 278 | ''' 279 | 280 | return 5 281 | 282 | def getColumnName(self, columnIndex): 283 | ''' 284 | Used by the Java Swing UI 285 | ''' 286 | 287 | cols = ["Time added", 288 | "Tool", 289 | "URL", 290 | "Issue", 291 | "Who"] 292 | try: 293 | return cols[columnIndex] 294 | except KeyError: 295 | return "" 296 | 297 | def get(self, rowIndex): 298 | ''' 299 | Gets the LogEntry at rowIndex 300 | ''' 301 | return self._log.get(rowIndex) 302 | 303 | def getValueAt(self, rowIndex, columnIndex): 304 | ''' 305 | Used by the Java Swing UI 306 | ''' 307 | 308 | logEntry = self._log.get(rowIndex) 309 | if columnIndex == 0: 310 | return logEntry.timestamp 311 | elif columnIndex == 1: 312 | return logEntry.tool.capitalize() 313 | elif columnIndex == 2: 314 | return logEntry.url 315 | elif columnIndex == 3: 316 | if logEntry.tool == "scanner": 317 | return logEntry.issue_name 318 | else: 319 | return "N/A" 320 | elif columnIndex == 4: 321 | return logEntry.who 322 | 323 | return "" 324 | 325 | import os, subprocess 326 | class GitLog(object): 327 | ''' 328 | Represents the underlying Git Repo that stores user information. Used 329 | by the Log object. As it stands, uses only a single git repo at a fixed 330 | path. 331 | ''' 332 | 333 | def __init__(self, callbacks): 334 | ''' 335 | Creates the git repo if it doesn't exist 336 | ''' 337 | 338 | self.callbacks = callbacks 339 | 340 | # Set directory paths and if necessary, init git repo 341 | 342 | home = os.path.expanduser("~") 343 | self.repo_path = os.path.join(home, ".burp-git-bridge") 344 | 345 | if not os.path.exists(self.repo_path): 346 | subprocess.check_call(["git", "init", self.repo_path], cwd=home) 347 | 348 | def add_repeater_entry(self, entry): 349 | ''' 350 | Adds a LogEntry containing Burp Repeater data to the git repo 351 | ''' 352 | 353 | # Make directory for this entry 354 | 355 | entry_dir = os.path.join(self.repo_path, entry.md5) 356 | if not os.path.exists(entry_dir): 357 | os.mkdir(entry_dir) 358 | 359 | # Add and commit repeater data to git repo 360 | 361 | self.write_entry(entry, entry_dir) 362 | subprocess.check_call(["git", "commit", "-m", "Added Repeater entry"], 363 | cwd=self.repo_path) 364 | 365 | def add_scanner_entry(self, entry): 366 | ''' 367 | Adds a LogEntry containing Burp Scanner data to the git repo 368 | ''' 369 | 370 | # Create dir hierarchy for this issue 371 | 372 | entry_dir = os.path.join(self.repo_path, entry.md5) 373 | 374 | 375 | # Log this entry; log 'messages' to its own subdir 376 | 377 | messages = entry.messages 378 | del entry.__dict__["messages"] 379 | self.write_entry(entry, entry_dir) 380 | messages_dir = os.path.join(entry_dir, "messages") 381 | if not os.path.exists(messages_dir): 382 | os.mkdir(messages_dir) 383 | lpath = os.path.join(messages_dir, ".burp-list") 384 | open(lpath, "wt") 385 | subprocess.check_call(["git", "add", lpath], cwd=self.repo_path) 386 | i = 0 387 | for message in messages: 388 | message_dir = os.path.join(messages_dir, str(i)) 389 | if not os.path.exists(message_dir): 390 | os.mkdir(message_dir) 391 | self.write_entry(message, message_dir) 392 | i += 1 393 | 394 | subprocess.check_call(["git", "commit", "-m", "Added scanner entry"], 395 | cwd=self.repo_path) 396 | 397 | 398 | def write_entry(self, entry, entry_dir): 399 | ''' 400 | Stores a LogEntry to entry_dir and adds it to git repo. 401 | ''' 402 | 403 | if not os.path.exists(entry_dir): 404 | os.mkdir(entry_dir) 405 | for filename, data in entry.__dict__.iteritems(): 406 | if not data: 407 | data = "" 408 | if not getattr(data, "__getitem__", False): 409 | data = str(data) 410 | path = os.path.join(entry_dir, filename) 411 | with open(path, "wb") as fp: 412 | fp.write(data) 413 | fp.flush() 414 | fp.close() 415 | subprocess.check_call(["git", "add", path], 416 | cwd=self.repo_path) 417 | 418 | 419 | def entries(self): 420 | ''' 421 | Generator; yields a LogEntry for each entry in the on-disk git repo 422 | ''' 423 | 424 | def load_entry(entry_path): 425 | ''' 426 | Loads a single entry from the path. Could be a "list" entry (see 427 | below) 428 | ''' 429 | 430 | entry = LogEntry() 431 | for filename in os.listdir(entry_path): 432 | file_path = os.path.join(entry_path, filename) 433 | if os.path.isdir(file_path): 434 | if ".burp-list" in os.listdir(file_path): 435 | list_entry = load_list(file_path) 436 | entry.__dict__[filename] = list_entry 437 | else: 438 | sub_entry = load_entry(file_path) 439 | entry.__dict__[filename] = sub_entry 440 | else: 441 | entry.__dict__[filename] = open(file_path, "rb").read() 442 | return entry 443 | 444 | def load_list(entry_path): 445 | ''' 446 | Loads a "list" entry (corresponds to a python list, or a Java 447 | ArrayList, such as the "messages" member of a Burp Scanner Issue). 448 | ''' 449 | 450 | entries = [] 451 | for filename in os.listdir(entry_path): 452 | file_path = os.path.join(entry_path, filename) 453 | if filename == ".burp-list": 454 | continue 455 | entries.append(load_entry(file_path)) 456 | return entries 457 | 458 | 459 | # Process each of the directories in the underlying git repo 460 | 461 | for entry_dir in os.listdir(self.repo_path): 462 | if entry_dir == ".git": 463 | continue 464 | entry_path = os.path.join(self.repo_path, entry_dir) 465 | if not os.path.isdir(entry_path): 466 | continue 467 | entry = load_entry(entry_path) 468 | yield entry 469 | 470 | 471 | def whoami(self): 472 | ''' 473 | Returns user.name from the underlying git repo. Used to note who 474 | created or modified an entry. 475 | ''' 476 | 477 | return subprocess.check_output(["git", "config", "user.name"], 478 | cwd=self.repo_path) 479 | 480 | def remove(self, entry): 481 | ''' 482 | Removes the given LogEntry from the underlying git repo. 483 | ''' 484 | entry_path = os.path.join(self.repo_path, entry.md5) 485 | subprocess.check_output(["git", "rm", "-rf", entry_path], 486 | cwd=self.repo_path) 487 | subprocess.check_call(["git", "commit", "-m", "Removed entry at %s" % 488 | entry_path], cwd=self.repo_path) 489 | 490 | 491 | 492 | ''' 493 | Implementation of extension's UI. 494 | ''' 495 | 496 | class BurpUi(ITab): 497 | ''' 498 | The collection of objects that make up this extension's Burp UI. Created 499 | by BurpExtender. 500 | ''' 501 | 502 | def __init__(self, callbacks, log): 503 | ''' 504 | Creates GUI objects, registers right-click handlers, and adds the 505 | extension's tab to the Burp UI. 506 | ''' 507 | 508 | # Create split pane with top and bottom panes 509 | 510 | self._splitpane = JSplitPane(JSplitPane.VERTICAL_SPLIT) 511 | self.bottom_pane = UiBottomPane(callbacks, log) 512 | self.top_pane = UiTopPane(callbacks, self.bottom_pane, log) 513 | self.bottom_pane.setLogTable(self.top_pane.logTable) 514 | self._splitpane.setLeftComponent(self.top_pane) 515 | self._splitpane.setRightComponent(self.bottom_pane) 516 | 517 | 518 | # Create right-click handler 519 | 520 | self.log = log 521 | rc_handler = RightClickHandler(callbacks, log) 522 | callbacks.registerContextMenuFactory(rc_handler) 523 | 524 | 525 | # Add the plugin's custom tab to Burp's UI 526 | 527 | callbacks.customizeUiComponent(self._splitpane) 528 | callbacks.addSuiteTab(self) 529 | 530 | 531 | def getTabCaption(self): 532 | return "Git" 533 | 534 | def getUiComponent(self): 535 | return self._splitpane 536 | 537 | class RightClickHandler(IContextMenuFactory): 538 | ''' 539 | Creates menu items for Burp UI right-click menus. 540 | ''' 541 | 542 | def __init__(self, callbacks, log): 543 | self.callbacks = callbacks 544 | self.log = log 545 | 546 | def createMenuItems(self, invocation): 547 | ''' 548 | Invoked by Burp when a right-click menu is created; adds Git Bridge's 549 | options to the menu. 550 | ''' 551 | 552 | context = invocation.getInvocationContext() 553 | tool = invocation.getToolFlag() 554 | if tool == self.callbacks.TOOL_REPEATER: 555 | if context in [invocation.CONTEXT_MESSAGE_EDITOR_REQUEST, invocation.CONTEXT_MESSAGE_VIEWER_RESPONSE]: 556 | item = JMenuItem("Send to Git Bridge") 557 | item.addActionListener(self.RepeaterHandler(self.callbacks, invocation, self.log)) 558 | items = ArrayList() 559 | items.add(item) 560 | return items 561 | elif tool == self.callbacks.TOOL_SCANNER: 562 | if context in [invocation.CONTEXT_SCANNER_RESULTS]: 563 | item = JMenuItem("Send to Git Bridge") 564 | item.addActionListener(self.ScannerHandler(self.callbacks, invocation, self.log)) 565 | items = ArrayList() 566 | items.add(item) 567 | return items 568 | else: 569 | # TODO: add support for other tools 570 | pass 571 | 572 | class ScannerHandler(ActionListener): 573 | ''' 574 | Handles selection of the 'Send to Git Bridge' menu item when shown 575 | on a Scanner right click menu. 576 | ''' 577 | 578 | def __init__(self, callbacks, invocation, log): 579 | self.callbacks = callbacks 580 | self.invocation = invocation 581 | self.log = log 582 | 583 | def actionPerformed(self, actionEvent): 584 | for issue in self.invocation.getSelectedIssues(): 585 | self.log.add_scanner_entry(issue) 586 | 587 | class RepeaterHandler(ActionListener): 588 | ''' 589 | Handles selection of the 'Send to Git Bridge' menu item when shown 590 | on a Repeater right click menu. 591 | ''' 592 | 593 | def __init__(self, callbacks, invocation, log): 594 | self.callbacks = callbacks 595 | self.invocation = invocation 596 | self.log = log 597 | 598 | def actionPerformed(self, actionEvent): 599 | for message in self.invocation.getSelectedMessages(): 600 | self.log.add_repeater_entry(message) 601 | 602 | class UiBottomPane(JTabbedPane, IMessageEditorController): 603 | ''' 604 | The bottom pane in the this extension's UI tab. It shows detail of 605 | whatever is selected in the top pane. 606 | ''' 607 | 608 | def __init__(self, callbacks, log): 609 | self.commandPanel = CommandPanel(callbacks, log) 610 | self.addTab("Git Bridge Commands", self.commandPanel) 611 | self._requestViewer = callbacks.createMessageEditor(self, False) 612 | self._responseViewer = callbacks.createMessageEditor(self, False) 613 | self._issueViewer = callbacks.createMessageEditor(self, False) 614 | callbacks.customizeUiComponent(self) 615 | 616 | def setLogTable(self, log_table): 617 | ''' 618 | Passes the Log table to the "Send to Tools" component so it can grab 619 | the selected rows 620 | ''' 621 | self.commandPanel.log_table = log_table 622 | 623 | def show_log_entry(self, log_entry): 624 | ''' 625 | Shows the log entry in the bottom pane of the UI 626 | ''' 627 | 628 | self.removeAll() 629 | self.addTab("Git Bridge Commands", self.commandPanel) 630 | if getattr(log_entry, "request", False): 631 | self.addTab("Request", self._requestViewer.getComponent()) 632 | self._requestViewer.setMessage(log_entry.request, True) 633 | if getattr(log_entry, "response", False): 634 | self.addTab("Response", self._responseViewer.getComponent()) 635 | self._responseViewer.setMessage(log_entry.response, False) 636 | if log_entry.tool == "scanner": 637 | self.addTab("Issue Summary", self._issueViewer.getComponent()) 638 | self._issueViewer.setMessage(self.getScanIssueSummary(log_entry), 639 | False) 640 | self._currentlyDisplayedItem = log_entry 641 | 642 | def getScanIssueSummary(self, log_entry): 643 | ''' 644 | A quick hack to generate a plaintext summary of a Scanner issue. 645 | This is shown in the bottom pane of the Git Bridge tab when a Scanner 646 | item is selected. 647 | ''' 648 | 649 | out = [] 650 | for key, val in sorted(log_entry.__dict__.items()): 651 | if key in ["messages", "tool", "md5"]: 652 | continue 653 | out.append("%s: %s" % (key, val)) 654 | return "\n\n".join(out) 655 | 656 | ''' 657 | The three methods below implement IMessageEditorController st. requests 658 | and responses are shown in the UI pane 659 | ''' 660 | 661 | def getHttpService(self): 662 | return self._currentlyDisplayedItem.requestResponse.getHttpService() 663 | 664 | def getRequest(self): 665 | return self._currentlyDisplayedItem.requestResponse.getRequest() 666 | 667 | def getResponse(self): 668 | return self._currentlyDisplayedItem.getResponse() 669 | 670 | 671 | class UiTopPane(JTabbedPane): 672 | ''' 673 | The top pane in this extension's UI tab. It shows the in-Burp version of 674 | the Git Repo. 675 | ''' 676 | 677 | def __init__(self, callbacks, bottom_pane, log): 678 | self.logTable = UiLogTable(callbacks, bottom_pane, log.gui_log) 679 | scrollPane = JScrollPane(self.logTable) 680 | self.addTab("Repo", scrollPane) 681 | callbacks.customizeUiComponent(self) 682 | 683 | class UiLogTable(JTable): 684 | ''' 685 | Table of log entries that are shown in the top pane of the UI when 686 | the corresponding tab is selected. 687 | 688 | Note, as a JTable, this stays synchronized with the underlying 689 | ArrayList. 690 | ''' 691 | 692 | def __init__(self, callbacks, bottom_pane, gui_log): 693 | self.setAutoCreateRowSorter(True) 694 | self.bottom_pane = bottom_pane 695 | self._callbacks = callbacks 696 | self.gui_log = gui_log 697 | self.setModel(gui_log) 698 | callbacks.customizeUiComponent(self) 699 | 700 | def getSelectedEntries(self): 701 | return [self.gui_log.get(i) for i in self.getSelectedRows()] 702 | 703 | def changeSelection(self, row, col, toggle, extend): 704 | ''' 705 | Displays the selected item in the content pane 706 | ''' 707 | 708 | JTable.changeSelection(self, row, col, toggle, extend) 709 | self.bottom_pane.show_log_entry(self.gui_log.get(row)) 710 | 711 | class CommandPanel(JPanel, ActionListener): 712 | ''' 713 | This is the "Git Bridge Commands" Panel shown in the bottom of the Git 714 | Bridge tab. 715 | ''' 716 | 717 | def __init__(self, callbacks, log): 718 | self.callbacks = callbacks 719 | self.log = log 720 | self.log_table = None # to be set by caller 721 | 722 | self.setLayout(BoxLayout(self, BoxLayout.PAGE_AXIS)) 723 | 724 | label = JLabel("Reload from Git Repo:") 725 | button = JButton("Reload") 726 | button.addActionListener(CommandPanel.ReloadAction(log)) 727 | self.add(label) 728 | self.add(button) 729 | 730 | label = JLabel("Send selected entries to respective Burp tools:") 731 | button = JButton("Send") 732 | button.addActionListener(CommandPanel.SendAction(self)) 733 | self.add(label) 734 | self.add(button) 735 | 736 | label = JLabel("Remove selected entries from Git Repo:") 737 | button = JButton("Remove") 738 | button.addActionListener(CommandPanel.RemoveAction(self, log)) 739 | self.add(label) 740 | self.add(button) 741 | 742 | # TODO: maybe add a git command box 743 | 744 | class ReloadAction(ActionListener): 745 | ''' 746 | Handles when the "Reload" button is clicked. 747 | ''' 748 | 749 | def __init__(self, log): 750 | self.log = log 751 | 752 | def actionPerformed(self, event): 753 | self.log.reload() 754 | 755 | class SendAction(ActionListener): 756 | ''' 757 | Handles when the "Send to Tools" button is clicked. 758 | ''' 759 | 760 | def __init__(self, panel): 761 | self.panel = panel 762 | 763 | def actionPerformed(self, actionEvent): 764 | ''' 765 | Iterates over each entry that is selected in the UI table and 766 | calls the proper Burp "send to" callback with the entry data. 767 | ''' 768 | 769 | for entry in self.panel.log_table.getSelectedEntries(): 770 | if entry.tool == "repeater": 771 | https = (entry.protocol == "https") 772 | self.panel.callbacks.sendToRepeater(entry.host, 773 | int(entry.port), https, entry.request, 774 | entry.timestamp) 775 | elif entry.tool == "scanner": 776 | issue = BurpLogScanIssue(entry) 777 | self.panel.callbacks.addScanIssue(issue) 778 | 779 | class RemoveAction(ActionListener): 780 | ''' 781 | Handles when the "Send to Tools" button is clicked. 782 | ''' 783 | 784 | def __init__(self, panel, log): 785 | self.panel = panel 786 | self.log = log 787 | 788 | def actionPerformed(self, event): 789 | ''' 790 | Iterates over each entry that is selected in the UI table and 791 | removes it from the Log. 792 | ''' 793 | entries = self.panel.log_table.getSelectedEntries() 794 | for entry in entries: 795 | self.log.remove(entry) 796 | 797 | 798 | ''' 799 | Burp Interoperability Class Definitions 800 | ''' 801 | 802 | class BurpLogHttpService(IHttpService): 803 | ''' 804 | Burp expects the object passed to "addScanIssue" to include a member 805 | that implements this interface; that is what this object is used for. 806 | ''' 807 | 808 | def __init__(self, host, port, protocol): 809 | self._host = host 810 | self._port = port 811 | self._protocol = protocol 812 | 813 | def getHost(self): 814 | return self._host 815 | 816 | def getPort(self): 817 | return int(self._port) 818 | 819 | def getProtocol(self): 820 | return self._protocol 821 | 822 | class BurpLogHttpRequestResponse(IHttpRequestResponse): 823 | ''' 824 | Burp expects the object passed to "addScanIssue" to include a member 825 | that implements this interface; that is what this object is used for. 826 | ''' 827 | 828 | def __init__(self, entry): 829 | self.entry = entry 830 | 831 | def getRequest(self): 832 | return self.entry.request 833 | def getResponse(self): 834 | return self.entry.response 835 | def getHttpService(self): 836 | return BurpLogHttpService(self.entry.host, 837 | self.entry.port, self.entry.protocol) 838 | 839 | 840 | class BurpLogScanIssue(IScanIssue): 841 | ''' 842 | Passed to addScanItem. 843 | 844 | Note that a pythonic solution that dynamically creates method based on 845 | LogEntry attributes via functools.partial will not work here as the 846 | interface classes supplied by Burp (IScanIssue, etc.) include read-only 847 | attributes corresponding to strings that would be used by such a solution. 848 | ''' 849 | 850 | def __init__(self, entry): 851 | self.entry = entry 852 | self.messages = [BurpLogHttpRequestResponse(m) for m in self.entry.messages] 853 | self.service = BurpLogHttpService(self.entry.host, self.entry.port, self.entry.protocol) 854 | 855 | def getHttpMessages(self): 856 | return self.messages 857 | def getHttpService(self): 858 | return self.service 859 | 860 | def getConfidence(self): 861 | return self.entry.confidence 862 | def getIssueBackground(self): 863 | return self.entry.issue_background 864 | def getIssueDetail(self): 865 | return self.entry.issue_detail 866 | def getIssueName(self): 867 | return self.entry.issue_name 868 | def getIssueType(self): 869 | return self.entry.issue_type 870 | def getRemediationDetail(self): 871 | return self.entry.remediation_detail 872 | def getSeverity(self): 873 | return self.entry.severity 874 | def getUrl(self): 875 | return URL(self.entry.url) 876 | --------------------------------------------------------------------------------