├── BappDescription.html ├── BappManifest.bmf ├── LICENSE ├── README.md └── ResponseClusterer.py /BappDescription.html: -------------------------------------------------------------------------------- 1 |
This extension clusters similar responses together, and shows a summary with 2 | one request/response per cluster. This allows the tester to get an overview of 3 | the tested website's responses from all Burp Suite tools. This is powerful, because 4 | it adds an additional vulnerability detection mechanism. Instead of using known techniques 5 | (error-based, inband sleep-based, out-of-band Burp Collaborator, etc.), this extension 6 | will assist in finding anomalies with a semi-automated approach allowing you to review 7 | a selection of server responses.
8 |Options for determining similarity can be configured, in case too few or too 9 | many clusters are generated. Because the similarity comparison can consume a lot 10 | of ressources, only small, in-scope responses that have interesting response 11 | codes, file extensions and MIME types are processed.
12 |The extension persists results in the project.
13 | -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: e63f09f290ad4d9ea20031e84767b303 2 | ExtensionType: 2 3 | Name: Response Clusterer 4 | RepoName: response-clusterer 5 | ScreenVersion: 0.0.3 6 | SerialVersion: 4 7 | MinPlatformVersion: 0 8 | ProOnly: False 9 | Author: floyd, modzero AG 10 | ShortDescription: Clusters similar responses together. 11 | EntryPoint: ResponseClusterer.py 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Tobias Ospelt 2 | 2017 - 2019 modzero AG 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY 15 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 24 | DAMAGE. 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ResponseClusterer 2 | 3 | This extension clusters similar responses together, and shows a summary with one request/response per cluster. This allows the tester to get an overview of the tested website's responses from all Burp Suite tools. This is powerful, because it adds an additional vulnerability detection mechanism. Instead of using known techniques (error-based, inband sleep-based, out-of-band Burp Collaborator, etc.), this extension will assist in finding anomalies with a semi-automated approach allowing you to review a selection of server responses. 4 | 5 | Options for determining similarity can be configured, in case too few or too many clusters are generated. Because the similarity comparison can consume a lot of ressources, only small, in-scope responses that have interesting response codes, file extensions and MIME types are processed. 6 | 7 | The extension persists results in the project. -------------------------------------------------------------------------------- /ResponseClusterer.py: -------------------------------------------------------------------------------- 1 | from burp import IBurpExtender 2 | from burp import ITab 3 | from burp import IHttpListener 4 | from burp import IMessageEditorController 5 | from burp import IHttpRequestResponse 6 | from burp import IHttpService 7 | from java.awt import Component 8 | from java.awt import GridBagLayout 9 | from java.awt import GridBagConstraints 10 | from java.awt.event import ActionListener 11 | from java.io import PrintWriter 12 | from java.util import ArrayList 13 | from java.util import List 14 | from javax.swing import JScrollPane 15 | from javax.swing import JSplitPane 16 | from javax.swing import JTabbedPane 17 | from javax.swing import JTable 18 | from javax.swing import JPanel 19 | from javax.swing import JLabel 20 | from javax.swing.event import DocumentListener 21 | from javax.swing import JCheckBox 22 | from javax.swing import SwingUtilities 23 | from javax.swing import JTextField 24 | from javax.swing.table import AbstractTableModel 25 | from threading import Lock 26 | 27 | import difflib 28 | import time 29 | import urlparse 30 | import pickle 31 | 32 | class BurpExtender(IBurpExtender, ITab, IHttpListener, IMessageEditorController, AbstractTableModel, ActionListener, DocumentListener): 33 | 34 | # 35 | # implement IBurpExtender 36 | # 37 | 38 | def registerExtenderCallbacks(self, callbacks): 39 | 40 | # keep a reference to our callbacks object 41 | self._callbacks = callbacks 42 | 43 | # obtain an extension helpers object 44 | self._helpers = callbacks.getHelpers() 45 | 46 | # set our extension name 47 | callbacks.setExtensionName("Response Clusterer") 48 | 49 | # create the log and a lock on which to synchronize when adding log entries 50 | self._log = ArrayList() 51 | self._lock = Lock() 52 | 53 | # main split pane 54 | self._main_jtabedpane = JTabbedPane() 55 | 56 | # The split pane with the log and request/respponse details 57 | self._splitpane = JSplitPane(JSplitPane.VERTICAL_SPLIT) 58 | 59 | # table of log entries 60 | logTable = Table(self) 61 | scrollPane = JScrollPane(logTable) 62 | self._splitpane.setLeftComponent(scrollPane) 63 | 64 | # List of log entries 65 | self._log_entries = [] 66 | 67 | # tabs with request/response viewers 68 | tabs = JTabbedPane() 69 | self._requestViewer = callbacks.createMessageEditor(self, False) 70 | self._responseViewer = callbacks.createMessageEditor(self, False) 71 | tabs.addTab("Request", self._requestViewer.getComponent()) 72 | tabs.addTab("Response", self._responseViewer.getComponent()) 73 | self._splitpane.setRightComponent(tabs) 74 | 75 | #Setup the options 76 | self._optionsJPanel = JPanel() 77 | gridBagLayout = GridBagLayout() 78 | gbc = GridBagConstraints() 79 | self._optionsJPanel.setLayout(gridBagLayout) 80 | 81 | self.max_clusters = 500 82 | self.JLabel_max_clusters = JLabel("Maximum amount of clusters: ") 83 | gbc.gridy=0 84 | gbc.gridx=0 85 | self._optionsJPanel.add(self.JLabel_max_clusters, gbc) 86 | self.JTextField_max_clusters = JTextField(str(self.max_clusters), 5) 87 | self.JTextField_max_clusters.getDocument().addDocumentListener(self) 88 | gbc.gridx=1 89 | self._optionsJPanel.add(self.JTextField_max_clusters, gbc) 90 | callbacks.customizeUiComponent(self.JLabel_max_clusters) 91 | callbacks.customizeUiComponent(self.JTextField_max_clusters) 92 | 93 | self.similarity = 0.95 94 | self.JLabel_similarity = JLabel("Similarity (between 0 and 1)") 95 | gbc.gridy=1 96 | gbc.gridx=0 97 | self._optionsJPanel.add(self.JLabel_similarity, gbc) 98 | self.JTextField_similarity = JTextField(str(self.similarity), 5) 99 | self.JTextField_similarity.getDocument().addDocumentListener(self) 100 | gbc.gridx=1 101 | self._optionsJPanel.add(self.JTextField_similarity, gbc) 102 | callbacks.customizeUiComponent(self.JLabel_similarity) 103 | callbacks.customizeUiComponent(self.JTextField_similarity) 104 | 105 | self.use_quick_similar = False 106 | self.JLabel_use_quick_similar = JLabel("Use set intersection of space splitted tokens for similarity (default: optimized difflib.SequenceMatcher.quick_ratio)") 107 | gbc.gridy=2 108 | gbc.gridx=0 109 | self._optionsJPanel.add(self.JLabel_use_quick_similar, gbc) 110 | self.JCheckBox_use_quick_similar = JCheckBox("") 111 | self.JCheckBox_use_quick_similar.addActionListener(self) 112 | gbc.gridx=1 113 | self._optionsJPanel.add(self.JCheckBox_use_quick_similar, gbc) 114 | callbacks.customizeUiComponent(self.JCheckBox_use_quick_similar) 115 | 116 | self.response_max_size = 10*1024 #10kb 117 | self.JLabel_response_max_size = JLabel("Response max size (bytes)") 118 | gbc.gridy=3 119 | gbc.gridx=0 120 | self._optionsJPanel.add(self.JLabel_response_max_size, gbc) 121 | self.JTextField_response_max_size = JTextField(str(self.response_max_size), 5) 122 | self.JTextField_response_max_size.getDocument().addDocumentListener(self) 123 | gbc.gridx=1 124 | self._optionsJPanel.add(self.JTextField_response_max_size, gbc) 125 | callbacks.customizeUiComponent(self.JLabel_response_max_size) 126 | callbacks.customizeUiComponent(self.JTextField_response_max_size) 127 | 128 | self.uninteresting_mime_types = ('JPEG', 'CSS', 'GIF', 'script', 'GIF', 'PNG', 'image') 129 | self.uninteresting_status_codes = () 130 | self.uninteresting_url_file_extensions = ('js', 'css', 'zip', 'war', 'jar', 'doc', 'docx', 'xls', 'xlsx', 'pdf', 'exe', 'dll', 'png', 'jpeg', 'jpg', 'bmp', 'tif', 'tiff', 'gif', 'webp', 'm3u', 'mp4', 'm4a', 'ogg', 'aac', 'flac', 'mp3', 'wav', 'avi', 'mov', 'mpeg', 'wmv', 'swf', 'woff', 'woff2') 131 | 132 | about = "" 133 | about += "Author: floyd, @floyd_ch, http://www.floyd.ch" 138 | about += "This plugin clusters all response bodies by similarity and shows a summary, one request/response per cluster. " 139 | about += 'Adjust similarity in the options if you get too few or too many entries in the "One member of each cluster" ' 140 | about += "tab. The plugin will allow a tester to get an overview of the tested website's responses from all tools (scanner, proxy, etc.). " 141 | about += "As similarity comparison " 142 | about += "can use a lot of ressources, only small, in-scope responses that have interesting response codes, " 143 | about += "file extensions and mime types are processed. " 144 | about += "
" 145 | about += "" 146 | self.JLabel_about = JLabel(about) 147 | self.JLabel_about.setLayout(GridBagLayout()) 148 | self._aboutJPanel = JScrollPane(self.JLabel_about) 149 | 150 | # customize our UI components 151 | callbacks.customizeUiComponent(self._splitpane) 152 | callbacks.customizeUiComponent(logTable) 153 | callbacks.customizeUiComponent(scrollPane) 154 | callbacks.customizeUiComponent(tabs) 155 | 156 | # add the splitpane and options to the main jtabedpane 157 | self._main_jtabedpane.addTab("One member of each cluster", None, self._splitpane, None) 158 | self._main_jtabedpane.addTab("Options", None, self._optionsJPanel, None) 159 | self._main_jtabedpane.addTab("About & README", None, self._aboutJPanel, None) 160 | 161 | # clusters will grow up to self.max_clusters response bodies... 162 | self._clusters = set() 163 | self.Similarity = Similarity() 164 | 165 | # Now load the already stored 166 | with self._lock: 167 | log_entries_from_storage = self.load_project_setting("log_entries") 168 | if log_entries_from_storage: 169 | for toolFlag, req, resp, url in log_entries_from_storage: 170 | try: 171 | self.add_new_log_entry(toolFlag, req, resp, url) 172 | except Exception as e: 173 | print "Exception when deserializing a stored log entry", toolFlag, url 174 | print e 175 | 176 | # Important: Do this at the very end (otherwise we could run into troubles locking up entire threads) 177 | # add the custom tab to Burp's UI 178 | callbacks.addSuiteTab(self) 179 | 180 | # register ourselves as an HTTP listener 181 | callbacks.registerHttpListener(self) 182 | 183 | # 184 | # implement what happens when options are changed 185 | # 186 | 187 | def changedUpdate(self, document): 188 | pass 189 | 190 | def removeUpdate(self, document): 191 | self.actionPerformed(None) 192 | 193 | def insertUpdate(self, document): 194 | self.actionPerformed(None) 195 | 196 | def actionPerformed(self, actionEvent): 197 | self.use_quick_similar = self.JCheckBox_use_quick_similar.isSelected() 198 | try: 199 | self.max_clusters = int(self.JTextField_max_clusters.getText()) 200 | except: 201 | self.JTextField_max_clusters.setText("200") 202 | 203 | try: 204 | self.similarity = float(self.JTextField_similarity.getText()) 205 | if self.similarity > 1.0 or self.similarity < 0.0: 206 | self.JTextField_similarity.setText("0.9") 207 | except: 208 | self.JTextField_similarity.setText("0.9") 209 | 210 | try: 211 | self.response_max_size = float(self.JTextField_response_max_size.getText()) 212 | if self.response_max_size < 0.0: 213 | self.JTextField_response_max_size.setText(str(10*1024)) 214 | except: 215 | self.JTextField_response_max_size.setText(str(10*1024)) 216 | 217 | print self.JCheckBox_use_quick_similar.isSelected(), self.JTextField_max_clusters.getText(), self.JTextField_similarity.getText(), self.JTextField_response_max_size.getText() 218 | 219 | # 220 | # implement ITab 221 | # 222 | 223 | def getTabCaption(self): 224 | return "Response Clusterer" 225 | 226 | def getUiComponent(self): 227 | return self._main_jtabedpane 228 | 229 | # 230 | # implement IHttpListener 231 | # 232 | 233 | def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): 234 | if not messageIsRequest: 235 | if len(self._clusters) >= self.max_clusters: 236 | return 237 | resp = messageInfo.getResponse() 238 | if len(resp) >= self.response_max_size: 239 | print "Message was too long" 240 | return 241 | iResponseInfo = self._helpers.analyzeResponse(resp) 242 | mime_type = iResponseInfo.getStatedMimeType() 243 | if mime_type in self.uninteresting_mime_types: 244 | print "Mime type", mime_type, "is ignored" 245 | return 246 | if iResponseInfo.getStatusCode() in self.uninteresting_status_codes: 247 | print "Status code", iResponseInfo.getStatusCode(), "is ignored" 248 | return 249 | req = messageInfo.getRequest() 250 | iRequestInfo = self._helpers.analyzeRequest(messageInfo) 251 | if not iRequestInfo.getUrl(): 252 | print "iRequestInfo.getUrl() returned None, so bailing out of analyzing this request" 253 | return 254 | if '.' in iRequestInfo.getUrl().getFile() and iRequestInfo.getUrl().getFile().split('.')[-1] in self.uninteresting_url_file_extensions: 255 | print iRequestInfo.getUrl().getFile().split('.')[-1], "is an ignored file extension" 256 | return 257 | if not self._callbacks.isInScope(iRequestInfo.getUrl()): 258 | print iRequestInfo.getUrl(), "is not in scope" 259 | return 260 | body = resp[iResponseInfo.getBodyOffset():] 261 | with self._lock: 262 | similarity_func = self.Similarity.similar 263 | if self.use_quick_similar: 264 | similarity_func = self.Similarity.quick_similar 265 | start_time = time.time() 266 | for response_code, item in self._clusters: 267 | if not response_code == iResponseInfo.getStatusCode(): 268 | #Different response codes -> different clusters 269 | continue 270 | if similarity_func(str(body), str(item), self.similarity): 271 | return #break 272 | else: #when no break/return occures in the for loop 273 | self.add_new_log_entry(toolFlag, req, resp, iRequestInfo.getUrl().toString()) 274 | self.save_project_setting("log_entries", self._log_entries) 275 | taken_time = time.time() - start_time 276 | if taken_time > 0.5: 277 | print "Plugin took", taken_time, "seconds to process request... body length:", len(body), "current cluster length:", len(self._clusters) 278 | print "URL:", str(iRequestInfo.getUrl()), 279 | 280 | def add_new_log_entry(self, toolFlag, request, response, service_url): 281 | self._log_entries.append((toolFlag, request, response, service_url)) 282 | iResponseInfo = self._helpers.analyzeResponse(response) 283 | body = response[iResponseInfo.getBodyOffset():] 284 | self._clusters.add((iResponseInfo.getStatusCode(), body)) 285 | row = self._log.size() 286 | service = CustomHttpService(service_url) 287 | r = CustomRequestResponse(None, None, service, request, response) 288 | iRequestInfo = self._helpers.analyzeRequest(r) 289 | self._log.add(LogEntry(toolFlag, self._callbacks.saveBuffersToTempFiles(r), iRequestInfo.getUrl())) 290 | self.fireTableRowsInserted(row, row) 291 | 292 | # 293 | # extend AbstractTableModel 294 | # 295 | 296 | def getRowCount(self): 297 | try: 298 | return self._log.size() 299 | except: 300 | return 0 301 | 302 | def getColumnCount(self): 303 | return 2 304 | 305 | def getColumnName(self, columnIndex): 306 | if columnIndex == 0: 307 | return "Tool" 308 | if columnIndex == 1: 309 | return "URL" 310 | return "" 311 | 312 | def getValueAt(self, rowIndex, columnIndex): 313 | logEntry = self._log.get(rowIndex) 314 | if columnIndex == 0: 315 | return self._callbacks.getToolName(logEntry._tool) 316 | if columnIndex == 1: 317 | return logEntry._url.toString() 318 | return "" 319 | 320 | # 321 | # implement IMessageEditorController 322 | # this allows our request/response viewers to obtain details about the messages being displayed 323 | # 324 | 325 | def getHttpService(self): 326 | return self._currentlyDisplayedItem.getHttpService() 327 | 328 | def getRequest(self): 329 | return self._currentlyDisplayedItem.getRequest() 330 | 331 | def getResponse(self): 332 | return self._currentlyDisplayedItem.getResponse() 333 | 334 | def save_project_setting(self, name, value): 335 | value = pickle.dumps(value).encode("base64") 336 | request = "GET /"+name+" HTTP/1.0\r\n\r\n" \ 337 | "You can ignore this item in the site map. It was created by the ResponseClusterer extension. The \n" \ 338 | "reason is that the Burp API is missing a certain functionality to save settings. \n" \ 339 | "TODO Burp API limitation: This is a hackish way to be able to store project-scope settings.\n" \ 340 | "We don't want to restore requests/responses of tabs in a totally different Burp project.\n" \ 341 | "However, unfortunately there is no saveExtensionProjectSetting in the Burp API :(\n" \ 342 | "So we have to abuse the addToSiteMap API to store project-specific things\n" \ 343 | "Even when using this hack we currently cannot persist Collaborator interaction checks\n" \ 344 | "(IBurpCollaboratorClientContext is not serializable and Threads loose their Python class\n" \ 345 | "functionality when unloaded) due to Burp API limitations." 346 | response = None 347 | if value: 348 | response = "HTTP/1.1 200 OK\r\n" + value 349 | rr = CustomRequestResponse(name, '', CustomHttpService('http://responseclustererextension.local/'), request, response) 350 | self._callbacks.addToSiteMap(rr) 351 | 352 | def load_project_setting(self, name): 353 | rrs = self._callbacks.getSiteMap('http://responseclustererextension.local/'+name) 354 | if rrs: 355 | rr = rrs[0] 356 | if rr.getResponse(): 357 | val = "\r\n".join(FloydsHelpers.jb2ps(rr.getResponse()).split("\r\n")[1:]) 358 | return pickle.loads(val.decode("base64")) 359 | else: 360 | return None 361 | else: 362 | return None 363 | 364 | 365 | class Similarity(object): 366 | 367 | def quick_similar(self, a, b, threshold=0.9): 368 | if threshold <= 0: 369 | return True 370 | elif threshold >= 1.0: 371 | return a == b 372 | 373 | set_a = set(a.split(' ')) 374 | set_b = set(b.split(' ')) 375 | 376 | if len(set_a) < 10 or len(set_b) < 10: 377 | return self.similar(a, b, threshold) 378 | else: 379 | return threshold < float(len(set_a.intersection(set_b))) / max(len(set_a), len(set_b)) 380 | 381 | def similar(self, a, b, threshold=0.9): 382 | if threshold <= 0: 383 | return True 384 | elif threshold >= 1.0: 385 | return a == b 386 | 387 | if len(a) < len(b): 388 | a, b = b, a 389 | 390 | alen, blen = len(a), len(b) 391 | 392 | if blen == 0 or alen == 0: 393 | return alen == blen 394 | 395 | if blen == alen and a == b: 396 | return True 397 | 398 | len_ratio = float(blen) / alen 399 | 400 | if threshold > self.upper_bound_similarity(a, b): 401 | return False 402 | else: 403 | # Bad, we can't optimize anything here 404 | return threshold <= difflib.SequenceMatcher(None, a, b).quick_ratio() 405 | 406 | def upper_bound_similarity(self, a, b): 407 | return 2.0*(len(a))/(len(a)+len(b)) 408 | 409 | class CustomHttpService(IHttpService): 410 | def __init__(self, url): 411 | x = urlparse.urlparse(url) 412 | if x.scheme in ("http", "https"): 413 | self._protocol = x.scheme 414 | else: 415 | raise ValueError() 416 | self._host = x.hostname 417 | if not x.hostname: 418 | self._host = "" 419 | self._port = None 420 | if x.port: 421 | self._port = int(x.port) 422 | if not self._port: 423 | if self._protocol == "http": 424 | self._port = 80 425 | elif self._protocol == "https": 426 | self._port = 443 427 | 428 | def getHost(self): 429 | return self._host 430 | 431 | def getPort(self): 432 | return self._port 433 | 434 | def getProtocol(self): 435 | return self._protocol 436 | 437 | def __str__(self): 438 | return CustomHttpService.to_url(self) 439 | 440 | @staticmethod 441 | def to_url(service): 442 | a = FloydsHelpers.u2s(service.getProtocol()) + "://" + FloydsHelpers.u2s(service.getHost()) 443 | if service.getPort(): 444 | a += ":" + str(service.getPort()) 445 | return a + "/" 446 | 447 | 448 | class CustomRequestResponse(IHttpRequestResponse): 449 | # Every call in the code to getRequest or getResponse must be followed by 450 | # callbacks.analyzeRequest or analyze Response OR 451 | # FloydsHelpers.jb2ps OR 452 | # another operation such as len() 453 | 454 | def __init__(self, comment, highlight, service, request, response): 455 | self.com = comment 456 | self.high = highlight 457 | self.setHttpService(service) 458 | self.setRequest(request) 459 | self.setResponse(response) 460 | 461 | def getComment(self): 462 | return self.com 463 | 464 | def getHighlight(self): 465 | return self.high 466 | 467 | def getHttpService(self): 468 | return self.serv 469 | 470 | def getRequest(self): 471 | return self.req 472 | 473 | def getResponse(self): 474 | return self.resp 475 | 476 | def setComment(self, comment): 477 | self.com = comment 478 | 479 | def setHighlight(self, color): 480 | self.high = color 481 | 482 | def setHttpService(self, httpService): 483 | if isinstance(httpService, str): 484 | self.serv = CustomHttpService(httpService) 485 | else: 486 | self.serv = httpService 487 | 488 | def setRequest(self, message): 489 | if isinstance(message, str): 490 | self.req = FloydsHelpers.ps2jb(message) 491 | else: 492 | self.req = message 493 | 494 | def setResponse(self, message): 495 | if isinstance(message, str): 496 | self.resp = FloydsHelpers.ps2jb(message) 497 | else: 498 | self.resp = message 499 | 500 | def serialize(self): 501 | # print type(self.com), type(self.high), type(CustomHttpService.to_url(self.serv)), type(self.req), type(self.resp) 502 | return self.com, self.high, CustomHttpService.to_url(self.serv), FloydsHelpers.jb2ps(self.req), FloydsHelpers.jb2ps(self.resp) 503 | 504 | def deserialize(self, serialized_object): 505 | self.com, self.high, service_url, self.req, self.resp = serialized_object 506 | self.req = FloydsHelpers.ps2jb(self.req) 507 | self.resp = FloydsHelpers.ps2jb(self.resp) 508 | self.serv = CustomHttpService(service_url) 509 | 510 | 511 | # 512 | # extend JTable to handle cell selection 513 | # 514 | 515 | class Table(JTable): 516 | 517 | def __init__(self, extender): 518 | self._extender = extender 519 | self.setModel(extender) 520 | return 521 | 522 | def changeSelection(self, row, col, toggle, extend): 523 | 524 | # show the log entry for the selected row 525 | logEntry = self._extender._log.get(row) 526 | self._extender._requestViewer.setMessage(logEntry._requestResponse.getRequest(), True) 527 | self._extender._responseViewer.setMessage(logEntry._requestResponse.getResponse(), False) 528 | self._extender._currentlyDisplayedItem = logEntry._requestResponse 529 | 530 | JTable.changeSelection(self, row, col, toggle, extend) 531 | return 532 | 533 | # 534 | # class to hold details of each log entry 535 | # 536 | 537 | class LogEntry: 538 | 539 | def __init__(self, tool, requestResponse, url): 540 | self._tool = tool 541 | self._requestResponse = requestResponse 542 | self._url = url 543 | return 544 | 545 | class FloydsHelpers(object): 546 | 547 | @staticmethod 548 | def jb2ps(arr): 549 | """ 550 | Turns Java byte arrays into Python str 551 | :param arr: [65, 65, 65] 552 | :return: 'AAA' 553 | """ 554 | return ''.join(map(lambda x: chr(x % 256), arr)) 555 | 556 | @staticmethod 557 | def ps2jb(arr): 558 | """ 559 | Turns Python str into Java byte arrays 560 | :param arr: 'AAA' 561 | :return: [65, 65, 65] 562 | """ 563 | return [ord(x) if ord(x) < 128 else ord(x) - 256 for x in arr] 564 | 565 | @staticmethod 566 | def u2s(uni): 567 | """ 568 | Turns unicode into str/bytes. Burp might pass invalid Unicode (e.g. Intruder Bit Flipper). 569 | This seems to be the only way to say "give me the raw bytes" 570 | :param uni: u'https://example.org/invalid_unicode/\xc1' 571 | :return: 'https://example.org/invalid_unicode/\xc1' 572 | """ 573 | if isinstance(uni, unicode): 574 | return uni.encode("iso-8859-1", "ignore") 575 | else: 576 | return uni 577 | 578 | --------------------------------------------------------------------------------