class BurpExtender(IBurpExtender, ITab, IHttpListener, IMessageEditorController, AbstractTableModel, ActionListener, DocumentListener): # # implement IBurpExtender # def registerExtenderCallbacks(self, callbacks): # keep a reference to our callbacks object self._callbacks = callbacks # obtain an extension helpers object self._helpers = callbacks.getHelpers() # set our extension name callbacks.setExtensionName("Response Clusterer") # create the log and a lock on which to synchronize when adding log entries self._log = ArrayList() self._lock = Lock() # main split pane self._main_jtabedpane = JTabbedPane() # The split pane with the log and request/respponse details self._splitpane = JSplitPane(JSplitPane.VERTICAL_SPLIT) # table of log entries logTable = Table(self) scrollPane = JScrollPane(logTable) self._splitpane.setLeftComponent(scrollPane) # List of log entries self._log_entries = [] # tabs with request/response viewers tabs = JTabbedPane() self._requestViewer = callbacks.createMessageEditor(self, False) self._responseViewer = callbacks.createMessageEditor(self, False) tabs.addTab("Request", self._requestViewer.getComponent()) tabs.addTab("Response", self._responseViewer.getComponent()) self._splitpane.setRightComponent(tabs) #Setup the options self._optionsJPanel = JPanel() gridBagLayout = GridBagLayout() gbc = GridBagConstraints() self._optionsJPanel.setLayout(gridBagLayout) self.max_clusters = 500 self.JLabel_max_clusters = JLabel("Maximum amount of clusters: ") gbc.gridy = 0 gbc.gridx = 0 self._optionsJPanel.add(self.JLabel_max_clusters, gbc) self.JTextField_max_clusters = JTextField(str(self.max_clusters), 5) self.JTextField_max_clusters.getDocument().addDocumentListener(self) gbc.gridx = 1 self._optionsJPanel.add(self.JTextField_max_clusters, gbc) callbacks.customizeUiComponent(self.JLabel_max_clusters) callbacks.customizeUiComponent(self.JTextField_max_clusters) self.similarity = 0.95 self.JLabel_similarity = JLabel("Similarity (between 0 and 1)") gbc.gridy = 1 gbc.gridx = 0 self._optionsJPanel.add(self.JLabel_similarity, gbc) self.JTextField_similarity = JTextField(str(self.similarity), 5) self.JTextField_similarity.getDocument().addDocumentListener(self) gbc.gridx = 1 self._optionsJPanel.add(self.JTextField_similarity, gbc) callbacks.customizeUiComponent(self.JLabel_similarity) callbacks.customizeUiComponent(self.JTextField_similarity) self.use_quick_similar = False self.JLabel_use_quick_similar = JLabel( "Use set intersection of space splitted tokens for similarity (default: optimized difflib.SequenceMatcher.quick_ratio)" ) gbc.gridy = 2 gbc.gridx = 0 self._optionsJPanel.add(self.JLabel_use_quick_similar, gbc) self.JCheckBox_use_quick_similar = JCheckBox("") self.JCheckBox_use_quick_similar.addActionListener(self) gbc.gridx = 1 self._optionsJPanel.add(self.JCheckBox_use_quick_similar, gbc) callbacks.customizeUiComponent(self.JCheckBox_use_quick_similar) self.response_max_size = 10 * 1024 #10kb self.JLabel_response_max_size = JLabel("Response max size (bytes)") gbc.gridy = 3 gbc.gridx = 0 self._optionsJPanel.add(self.JLabel_response_max_size, gbc) self.JTextField_response_max_size = JTextField( str(self.response_max_size), 5) self.JTextField_response_max_size.getDocument().addDocumentListener( self) gbc.gridx = 1 self._optionsJPanel.add(self.JTextField_response_max_size, gbc) callbacks.customizeUiComponent(self.JLabel_response_max_size) callbacks.customizeUiComponent(self.JTextField_response_max_size) self.uninteresting_mime_types = ('JPEG', 'CSS', 'GIF', 'script', 'GIF', 'PNG', 'image') self.uninteresting_status_codes = () 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') about = "<html>" about += "Author: floyd, @floyd_ch, http://www.floyd.ch<br>" about += "modzero AG, http://www.modzero.ch<br>" about += "<br>" about += "<h3>Getting an overview of the tested website</h3>" about += "<p style=\"width:500px\">" about += "This plugin clusters all response bodies by similarity and shows a summary, one request/response per cluster. " about += 'Adjust similarity in the options if you get too few or too many entries in the "One member of each cluster" ' about += "tab. The plugin will allow a tester to get an overview of the tested website's responses from all tools (scanner, proxy, etc.). " about += "As similarity comparison " about += "can use a lot of ressources, only small, in-scope responses that have interesting response codes, " about += "file extensions and mime types are processed. " about += "</p>" about += "</html>" self.JLabel_about = JLabel(about) self.JLabel_about.setLayout(GridBagLayout()) self._aboutJPanel = JScrollPane(self.JLabel_about) # customize our UI components callbacks.customizeUiComponent(self._splitpane) callbacks.customizeUiComponent(logTable) callbacks.customizeUiComponent(scrollPane) callbacks.customizeUiComponent(tabs) # add the splitpane and options to the main jtabedpane self._main_jtabedpane.addTab("One member of each cluster", None, self._splitpane, None) self._main_jtabedpane.addTab("Options", None, self._optionsJPanel, None) self._main_jtabedpane.addTab("About & README", None, self._aboutJPanel, None) # clusters will grow up to self.max_clusters response bodies... self._clusters = set() self.Similarity = Similarity() # Now load the already stored with self._lock: log_entries_from_storage = self.load_project_setting("log_entries") if log_entries_from_storage: for toolFlag, req, resp, url in log_entries_from_storage: try: self.add_new_log_entry(toolFlag, req, resp, url) except Exception as e: print "Exception when deserializing a stored log entry", toolFlag, url print e # Important: Do this at the very end (otherwise we could run into troubles locking up entire threads) # add the custom tab to Burp's UI callbacks.addSuiteTab(self) # register ourselves as an HTTP listener callbacks.registerHttpListener(self) # # implement what happens when options are changed # def changedUpdate(self, document): pass def removeUpdate(self, document): self.actionPerformed(None) def insertUpdate(self, document): self.actionPerformed(None) def actionPerformed(self, actionEvent): self.use_quick_similar = self.JCheckBox_use_quick_similar.isSelected() try: self.max_clusters = int(self.JTextField_max_clusters.getText()) except: self.JTextField_max_clusters.setText("200") try: self.similarity = float(self.JTextField_similarity.getText()) if self.similarity > 1.0 or self.similarity < 0.0: self.JTextField_similarity.setText("0.9") except: self.JTextField_similarity.setText("0.9") try: self.response_max_size = float( self.JTextField_response_max_size.getText()) if self.response_max_size < 0.0: self.JTextField_response_max_size.setText(str(10 * 1024)) except: self.JTextField_response_max_size.setText(str(10 * 1024)) print self.JCheckBox_use_quick_similar.isSelected( ), self.JTextField_max_clusters.getText( ), self.JTextField_similarity.getText( ), self.JTextField_response_max_size.getText() # # implement ITab # def getTabCaption(self): return "Response Clusterer" def getUiComponent(self): return self._main_jtabedpane # # implement IHttpListener # def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): if not messageIsRequest: if len(self._clusters) >= self.max_clusters: return resp = messageInfo.getResponse() if len(resp) >= self.response_max_size: print "Message was too long" return iResponseInfo = self._helpers.analyzeResponse(resp) mime_type = iResponseInfo.getStatedMimeType() if mime_type in self.uninteresting_mime_types: print "Mime type", mime_type, "is ignored" return if iResponseInfo.getStatusCode( ) in self.uninteresting_status_codes: print "Status code", iResponseInfo.getStatusCode( ), "is ignored" return req = messageInfo.getRequest() iRequestInfo = self._helpers.analyzeRequest(messageInfo) if not iRequestInfo.getUrl(): print "iRequestInfo.getUrl() returned None, so bailing out of analyzing this request" return if '.' in iRequestInfo.getUrl().getFile() and iRequestInfo.getUrl( ).getFile().split( '.')[-1] in self.uninteresting_url_file_extensions: print iRequestInfo.getUrl().getFile().split( '.')[-1], "is an ignored file extension" return if not self._callbacks.isInScope(iRequestInfo.getUrl()): print iRequestInfo.getUrl(), "is not in scope" return body = resp[iResponseInfo.getBodyOffset():] with self._lock: similarity_func = self.Similarity.similar if self.use_quick_similar: similarity_func = self.Similarity.quick_similar start_time = time.time() for response_code, item in self._clusters: if not response_code == iResponseInfo.getStatusCode(): #Different response codes -> different clusters continue if similarity_func(str(body), str(item), self.similarity): return #break else: #when no break/return occures in the for loop self.add_new_log_entry(toolFlag, req, resp, iRequestInfo.getUrl().toString()) self.save_project_setting("log_entries", self._log_entries) taken_time = time.time() - start_time if taken_time > 0.5: print "Plugin took", taken_time, "seconds to process request... body length:", len( body), "current cluster length:", len(self._clusters) print "URL:", str(iRequestInfo.getUrl()), def add_new_log_entry(self, toolFlag, request, response, service_url): self._log_entries.append((toolFlag, request, response, service_url)) iResponseInfo = self._helpers.analyzeResponse(response) body = response[iResponseInfo.getBodyOffset():] self._clusters.add((iResponseInfo.getStatusCode(), str(body))) row = self._log.size() service = CustomHttpService(service_url) r = CustomRequestResponse(None, None, service, request, response) iRequestInfo = self._helpers.analyzeRequest(r) self._log.add( LogEntry(toolFlag, self._callbacks.saveBuffersToTempFiles(r), iRequestInfo.getUrl())) self.fireTableRowsInserted(row, row) # # extend AbstractTableModel # def getRowCount(self): try: return self._log.size() except: return 0 def getColumnCount(self): return 2 def getColumnName(self, columnIndex): if columnIndex == 0: return "Tool" if columnIndex == 1: return "URL" return "" def getValueAt(self, rowIndex, columnIndex): logEntry = self._log.get(rowIndex) if columnIndex == 0: return self._callbacks.getToolName(logEntry._tool) if columnIndex == 1: return logEntry._url.toString() return "" # # implement IMessageEditorController # this allows our request/response viewers to obtain details about the messages being displayed # def getHttpService(self): return self._currentlyDisplayedItem.getHttpService() def getRequest(self): return self._currentlyDisplayedItem.getRequest() def getResponse(self): return self._currentlyDisplayedItem.getResponse() def save_project_setting(self, name, value): value = pickle.dumps(value).encode("base64") request = "GET /"+name+" HTTP/1.0\r\n\r\n" \ "You can ignore this item in the site map. It was created by the ResponseClusterer extension. The \n" \ "reason is that the Burp API is missing a certain functionality to save settings. \n" \ "TODO Burp API limitation: This is a hackish way to be able to store project-scope settings.\n" \ "We don't want to restore requests/responses of tabs in a totally different Burp project.\n" \ "However, unfortunately there is no saveExtensionProjectSetting in the Burp API :(\n" \ "So we have to abuse the addToSiteMap API to store project-specific things\n" \ "Even when using this hack we currently cannot persist Collaborator interaction checks\n" \ "(IBurpCollaboratorClientContext is not serializable and Threads loose their Python class\n" \ "functionality when unloaded) due to Burp API limitations." response = None if value: response = "HTTP/1.1 200 OK\r\n" + value rr = CustomRequestResponse( name, '', CustomHttpService('http://responseclustererextension.local/'), request, response) self._callbacks.addToSiteMap(rr) def load_project_setting(self, name): rrs = self._callbacks.getSiteMap( 'http://responseclustererextension.local/' + name) if rrs: rr = rrs[0] if rr.getResponse(): val = "\r\n".join( FloydsHelpers.jb2ps(rr.getResponse()).split("\r\n")[1:]) return pickle.loads(val.decode("base64")) else: return None else: return None
class BurpExtender(IBurpExtender, ITab, DocumentListener, ActionListener, IScannerInsertionPointProvider): def registerExtenderCallbacks(self, callbacks): print "Extension loaded!" self._callbacks = callbacks self._helpers = callbacks.getHelpers() callbacks.setExtensionName("HttpFuzzer") #Abusing functionality here :( #I would have prefered to have it implemented as an active scan module callbacks.registerScannerInsertionPointProvider(self) self._newline = "\r\n" #Options: self._random_mutations = 0 self._known_fuzz_string_mutations = 0 self._custom_fuzz_strings = None self._known_fuzz_strings = [ "A" * 256, "A" * 1024, "A" * 4096, "A" * 20000, "A" * 65535, "%x" * 256, "%n" * 256 , "%s" * 256, "%s%n%x%d" * 256, "%s" * 256, "%.1024d", "%.2048d", "%.4096d", "%.8200d", "%99999999999s", "%99999999999d", "%99999999999x", "%99999999999n", "%99999999999s" * 200, "%99999999999d" * 200, "%99999999999x" * 200, "%99999999999n" * 200, "%08x" * 100, "%%20s" * 200, "%%20x" * 200, "%%20n" * 200, "%%20d" * 200, "%#0123456x%08x%x%s%p%n%d%o%u%c%h%l%q%j%z%Z%t%i%e%g%f%a%C%S%08x%%#0123456x%%x%%s%%p%%n%%d%%o%%u%%c%%h%%l%%q%%j%%z%%Z%%t%%i%%e%%g%%f%%a%%C%%S%%08x", "'", "\\", "<", "+", "%", "$", "`" ] #End Options #UI START self._main_jtabedpane = JTabbedPane() #Setup the options self._optionsJPanel = JPanel() gridBagLayout = GridBagLayout(); gbc = GridBagConstraints() self._optionsJPanel.setLayout(gridBagLayout) self.JLabel_random_mutations = JLabel("Number of random bit and byte mutations: ") gbc.gridy += 1 gbc.gridx = 0 self._optionsJPanel.add(self.JLabel_random_mutations, gbc) gbc.gridx = 1 self.JTextField_random_mutations = JTextField(str(self._random_mutations), 6) self.JTextField_random_mutations.getDocument().addDocumentListener(self) self._optionsJPanel.add(self.JTextField_random_mutations, gbc) callbacks.customizeUiComponent(self.JLabel_random_mutations) callbacks.customizeUiComponent(self.JTextField_random_mutations) self.JLabel_known_fuzz_string_mutations = JLabel("Number of tests with known fuzzing strings: ") gbc.gridy += 1 gbc.gridx = 0 self._optionsJPanel.add(self.JLabel_known_fuzz_string_mutations, gbc) gbc.gridx = 1 self.JTextField_known_fuzz_string_mutations = JTextField(str(self._known_fuzz_string_mutations), 6) self.JTextField_known_fuzz_string_mutations.getDocument().addDocumentListener(self) self._optionsJPanel.add(self.JTextField_known_fuzz_string_mutations, gbc) callbacks.customizeUiComponent(self.JLabel_known_fuzz_string_mutations) callbacks.customizeUiComponent(self.JTextField_known_fuzz_string_mutations) self._filepath = '' self.JLabel_filepath = JLabel("Replacement for known fuzzing strings (one per line): ") gbc.gridy += 1 gbc.gridx = 0 gbc.gridwidth = 1 self._optionsJPanel.add(self.JLabel_filepath, gbc) gbc.gridx = 1 self.JTextField_filepath = JTextField(self._filepath, 25) self.JTextField_filepath.getDocument().addDocumentListener(self) self._optionsJPanel.add(self.JTextField_filepath, gbc) gbc.gridx = 2 self.FileChooserButton_filepath = FileChooserButton() self.FileChooserButton_filepath.setup(self.JTextField_filepath, "Choose") self._optionsJPanel.add(self.FileChooserButton_filepath, gbc) callbacks.customizeUiComponent(self.JLabel_filepath) callbacks.customizeUiComponent(self.JTextField_filepath) callbacks.customizeUiComponent(self.FileChooserButton_filepath) about = "<html>" about += "Author: floyd, @floyd_ch, http://www.floyd.ch<br>" about += "<br>" about += "<h3>A simple random fuzzer</h3>" about += "<p style=\"width:500px\">" about += "This plugin adds ActiveScan checks. " about += "Using this fuzzer with any standard HTTP server (Apache, Nginx, etc.) is usually useless, but can be fun. " about += "It can be used to see the different error conditions a server and the web application code can run into. " about += "However, if you are targeting an embedded device HTTP server or anything more exotic you might be more lucky. " about += "The plugin does not do any checks and doesn't add any issues. It is recommended to install the Collect500, " about += "ResponseClusterer, Logger++ and Error Message Checks plugin. Additionally it is recommended to attach a debugger to the " about += "target program on the server (or use strace or another tool of your choice). <br>" about += "In it's default configuration the plugin will not do anything, as it is not considered efficient to fuzz every actively scanned request. " about += "You need to specify a higher value for the number of tests in the options tab to enable fuzzing. " about += "</p>" about += "</html>" self.JLabel_about = JLabel(about) self.JLabel_about.setLayout(GridBagLayout()) self._aboutJPanel = JScrollPane(self.JLabel_about) # customize our UI components callbacks.customizeUiComponent(self._main_jtabedpane) callbacks.customizeUiComponent(self._optionsJPanel) callbacks.customizeUiComponent(self._aboutJPanel) self._main_jtabedpane.addTab("Options", None, self._optionsJPanel, None) self._main_jtabedpane.addTab("About & README", None, self._aboutJPanel, None) # add the custom tab to Burp's UI callbacks.addSuiteTab(self) #UI END print "Extension registered!" # # UI: implement ITab # def getTabCaption(self): return "HttpFuzzer" def getUiComponent(self): return self._main_jtabedpane # # UI: implement what happens when options are changed # def changedUpdate(self, document): pass def removeUpdate(self, document): self.insertUpdate(document) def insertUpdate(self, document): filepath = self.JTextField_filepath.getText().encode("utf-8") if filepath: try: self._custom_fuzz_strings = file(filepath, "rb").readlines() except: print "ERROR: Couldn't read file" self._custom_fuzz_strings = None print filepath try: self._random_mutations = int(self.JTextField_random_mutations.getText()) except: print "Exception,", self.JTextField_random_mutations.getText(), "is not numeric" self._random_mutations = 0 try: self._known_fuzz_string_mutations = int(self.JTextField_known_fuzz_string_mutations.getText()) except: print "Exception,", self.JTextField_known_fuzz_string_mutations.getText(), "is not numeric" self._known_fuzz_string_mutations = 0 print self._random_mutations, self._known_fuzz_string_mutations def actionPerformed(self, actionEvent): self.insertUpdate(None) #TODO: Is there another way to simply say "each active scanned HTTP request once"? #it seems not: https://support.portswigger.net/customer/en/portal/questions/16776337-confusion-on-insertionpoints-active-scan-module?new=16776337 #So we are going to abuse a functionality of Burp called IScannerInsertionPoint #which is by coincidence always called once per request for every actively scanned item (with baseRequestResponse) #this is an ugly hack as the percentage of active scan is simply stuck until this plugin is done def getInsertionPoints(self, baseRequestResponse): self.do_fuzzing(baseRequestResponse) def do_fuzzing(self, baseRequestResponse): req = FloydsHelpers.jb2ps(baseRequestResponse.getRequest()) fuzz_strings = self._custom_fuzz_strings or self._known_fuzz_strings for _ in xrange(0, self._known_fuzz_string_mutations): index = random.choice(xrange(0, len(req))) print "Inserted known fuzz string at byte index", index new_req = req[:index]+random.choice(fuzz_strings)+req[index+1:] try: self._send(baseRequestResponse, new_req) except Exception, e: print "Error occured. Ignoring and simply going on." print e for _ in xrange(0, self._random_mutations): index = random.randint(0, len(req)-1) new_req = req if random.choice((True, False)): #byte change print "At byte index", index, "changed to new byte" new_req = req[:index]+chr(random.randint(0, 255))+req[index+1:] else: #bit change bit_index = random.randint(0, 7) print "At byte index", index, "changed bit", bit_index new_byte = chr(ord(req[index]) ^ (2**bit_index)) new_req = req[:index]+new_byte+req[index+1:] try: self._send(baseRequestResponse, new_req) except Exception, e: print "Error occured. Ignoring and simply going on." print e