|
| 1 | +import os |
| 2 | +from threading import Thread |
| 3 | +from burp import IBurpExtender, ITab |
| 4 | +from javax.swing import JPanel, JButton, JTextField, JFileChooser, JLabel, JOptionPane, JCheckBox, BorderFactory |
| 5 | +from java.awt import GridBagLayout, GridBagConstraints, Insets, Color, Dimension, Font |
| 6 | +from java.net import URL |
| 7 | + |
| 8 | +class BurpExtender(IBurpExtender, ITab): |
| 9 | + def registerExtenderCallbacks(self, callbacks): |
| 10 | + self._callbacks = callbacks |
| 11 | + self._helpers = callbacks.getHelpers() |
| 12 | + |
| 13 | + # Set the extension name |
| 14 | + callbacks.setExtensionName("Obsidian") |
| 15 | + |
| 16 | + # Create and add the custom tab |
| 17 | + self._tab = JPanel(GridBagLayout()) |
| 18 | + self._tab.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)) |
| 19 | + |
| 20 | + constraints = GridBagConstraints() |
| 21 | + constraints.fill = GridBagConstraints.HORIZONTAL |
| 22 | + constraints.insets = Insets(5, 5, 5, 5) |
| 23 | + |
| 24 | + # Add description label |
| 25 | + constraints.gridx = 0 |
| 26 | + constraints.gridy = 0 |
| 27 | + constraints.gridwidth = 3 |
| 28 | + self._descriptionLabel = JLabel("Request to Import to Obsidian") |
| 29 | + self._descriptionLabel.setForeground(Color.WHITE) |
| 30 | + self._descriptionLabel.setFont(Font("Arial", Font.BOLD, 16)) |
| 31 | + self._tab.add(self._descriptionLabel, constraints) |
| 32 | + |
| 33 | + # Add vertical space (empty label) |
| 34 | + constraints.gridx = 0 |
| 35 | + constraints.gridy = 1 |
| 36 | + constraints.gridwidth = 2 |
| 37 | + self._verticalSpace = JLabel(" ") |
| 38 | + self._tab.add(self._verticalSpace, constraints) |
| 39 | + |
| 40 | + # Save folder location label |
| 41 | + constraints.gridx = 0 |
| 42 | + constraints.gridy = 2 |
| 43 | + constraints.gridwidth = 1 |
| 44 | + self._label = JLabel("Save folder location:") |
| 45 | + self._tab.add(self._label, constraints) |
| 46 | + |
| 47 | + # Folder path field |
| 48 | + constraints.gridx = 1 |
| 49 | + constraints.gridy = 2 |
| 50 | + self._folderPathField = JTextField(20) |
| 51 | + self._tab.add(self._folderPathField, constraints) |
| 52 | + |
| 53 | + # Browse button |
| 54 | + constraints.gridx = 2 |
| 55 | + constraints.gridy = 2 |
| 56 | + self._browseButton = JButton("Browse") |
| 57 | + self._browseButton.addActionListener(self._browse) |
| 58 | + self._tab.add(self._browseButton, constraints) |
| 59 | + |
| 60 | + # Create checkboxes for HTTP, HTTPS, and both |
| 61 | + constraints.gridx = 0 |
| 62 | + constraints.gridy = 3 |
| 63 | + self._httpCheckBox = JCheckBox("HTTP") |
| 64 | + self._httpCheckBox.addActionListener(self._updateCheckboxes) |
| 65 | + self._tab.add(self._httpCheckBox, constraints) |
| 66 | + |
| 67 | + constraints.gridx = 1 |
| 68 | + constraints.gridy = 3 |
| 69 | + self._httpsCheckBox = JCheckBox("HTTPS", True) # Default to checked |
| 70 | + self._httpsCheckBox.addActionListener(self._updateCheckboxes) |
| 71 | + self._tab.add(self._httpsCheckBox, constraints) |
| 72 | + |
| 73 | + constraints.gridx = 2 |
| 74 | + constraints.gridy = 3 |
| 75 | + self._httpAndHttpsCheckBox = JCheckBox("HTTP & HTTPS") |
| 76 | + self._httpAndHttpsCheckBox.addActionListener(self._updateCheckboxes) |
| 77 | + self._tab.add(self._httpAndHttpsCheckBox, constraints) |
| 78 | + |
| 79 | + # Add vertical space (empty label) |
| 80 | + constraints.gridx = 0 |
| 81 | + constraints.gridy = 1 |
| 82 | + constraints.gridwidth = 2 |
| 83 | + self._verticalSpace = JLabel(" ") |
| 84 | + self._tab.add(self._verticalSpace, constraints) |
| 85 | + |
| 86 | + # Generate button positioned below the checkboxes |
| 87 | + constraints.gridx = 0 |
| 88 | + constraints.gridy = 4 |
| 89 | + constraints.gridwidth = 1 |
| 90 | + self._generateButton = JButton("Generate") |
| 91 | + self._generateButton.setPreferredSize(Dimension(100, 30)) |
| 92 | + self._descriptionLabel.setForeground(Color(255, 255, 255)) |
| 93 | + self._generateButton.setBackground(Color(238, 103, 0)) # Orange color |
| 94 | + self._generateButton.setOpaque(True) |
| 95 | + self._generateButton.setBorderPainted(False) |
| 96 | + self._generateButton.addActionListener(self._generate) |
| 97 | + self._tab.add(self._generateButton, constraints) |
| 98 | + |
| 99 | + # Loading label |
| 100 | + constraints.gridx = 1 |
| 101 | + constraints.gridy = 4 |
| 102 | + constraints.gridwidth = 2 |
| 103 | + self._loadingLabel = JLabel("") |
| 104 | + self._tab.add(self._loadingLabel, constraints) |
| 105 | + |
| 106 | + # Message label |
| 107 | + constraints.gridx = 0 |
| 108 | + constraints.gridy = 5 |
| 109 | + constraints.gridwidth = 3 |
| 110 | + self._messageLabel = JLabel("") |
| 111 | + self._messageLabel.setForeground(Color(0, 128, 0)) |
| 112 | + self._tab.add(self._messageLabel, constraints) |
| 113 | + |
| 114 | + # Add the custom tab to Burp Suite |
| 115 | + callbacks.addSuiteTab(self) |
| 116 | + |
| 117 | + |
| 118 | + def getTabCaption(self): |
| 119 | + return "Obsidian" |
| 120 | + |
| 121 | + def getUiComponent(self): |
| 122 | + return self._tab |
| 123 | + |
| 124 | + def _browse(self, event): |
| 125 | + # Create a JFileChooser configured for directory selection |
| 126 | + file_chooser = JFileChooser() |
| 127 | + file_chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY) |
| 128 | + result = file_chooser.showSaveDialog(None) |
| 129 | + if result == JFileChooser.APPROVE_OPTION: |
| 130 | + folder = file_chooser.getSelectedFile() |
| 131 | + self._folderPathField.setText(folder.getAbsolutePath()) |
| 132 | + |
| 133 | + def _updateCheckboxes(self, event): |
| 134 | + source = event.getSource() |
| 135 | + if source == self._httpCheckBox: |
| 136 | + if self._httpCheckBox.isSelected(): |
| 137 | + self._httpsCheckBox.setSelected(False) |
| 138 | + self._httpAndHttpsCheckBox.setSelected(False) |
| 139 | + elif source == self._httpsCheckBox: |
| 140 | + if self._httpsCheckBox.isSelected(): |
| 141 | + self._httpCheckBox.setSelected(False) |
| 142 | + self._httpAndHttpsCheckBox.setSelected(False) |
| 143 | + elif source == self._httpAndHttpsCheckBox: |
| 144 | + if self._httpAndHttpsCheckBox.isSelected(): |
| 145 | + self._httpCheckBox.setSelected(False) |
| 146 | + self._httpsCheckBox.setSelected(False) |
| 147 | + |
| 148 | + def _generate(self, event): |
| 149 | + # Show loading message |
| 150 | + self._loadingLabel.setText("Loading...") |
| 151 | + |
| 152 | + # Run the generation process in a new thread |
| 153 | + thread = Thread(target=self._generate_in_thread) |
| 154 | + thread.start() |
| 155 | + |
| 156 | + def _generate_in_thread(self): |
| 157 | + folder_path = self._folderPathField.getText() |
| 158 | + if not folder_path: |
| 159 | + JOptionPane.showMessageDialog(None, "Please specify a folder path.", "Error", JOptionPane.ERROR_MESSAGE) |
| 160 | + self._loadingLabel.setText("") |
| 161 | + return |
| 162 | + |
| 163 | + # Determine selected protocols |
| 164 | + selected_protocols = set() |
| 165 | + if self._httpCheckBox.isSelected(): |
| 166 | + selected_protocols.add("http") |
| 167 | + if self._httpsCheckBox.isSelected(): |
| 168 | + selected_protocols.add("https") |
| 169 | + if self._httpAndHttpsCheckBox.isSelected(): |
| 170 | + selected_protocols.add("http") |
| 171 | + selected_protocols.add("https") |
| 172 | + |
| 173 | + # Ensure at least one protocol is selected |
| 174 | + if not selected_protocols: |
| 175 | + JOptionPane.showMessageDialog(None, "Please select at least one protocol.", "Error", JOptionPane.ERROR_MESSAGE) |
| 176 | + self._loadingLabel.setText("") |
| 177 | + return |
| 178 | + |
| 179 | + # Fetch URLs based on the selected protocols |
| 180 | + if "http" in selected_protocols and "https" in selected_protocols: |
| 181 | + urls = self._get_unique_urls_in_scope() |
| 182 | + else: |
| 183 | + urls = self._get_filtered_urls_in_scope("http" in selected_protocols, "https" in selected_protocols) |
| 184 | + |
| 185 | + if not urls: |
| 186 | + self._messageLabel.setText("No URLs in scope.") |
| 187 | + self._loadingLabel.setText("") |
| 188 | + return |
| 189 | + |
| 190 | + # Process URLs to create folder structure and include request details |
| 191 | + try: |
| 192 | + self._create_folder_structure(folder_path, urls) |
| 193 | + self._messageLabel.setText("Folder structure created in {}".format(folder_path)) |
| 194 | + except Exception as e: |
| 195 | + JOptionPane.showMessageDialog(None, "An error occurred: {}".format(str(e)), "Error", JOptionPane.ERROR_MESSAGE) |
| 196 | + self._messageLabel.setText("Failed to create folder structure.") |
| 197 | + |
| 198 | + self._loadingLabel.setText("") |
| 199 | + |
| 200 | + def _get_filtered_urls_in_scope(self, http_selected, https_selected): |
| 201 | + urls = set() |
| 202 | + http_protocol = "http" |
| 203 | + https_protocol = "https" |
| 204 | + |
| 205 | + # Get the list of IHttpRequestResponse objects in the target scope |
| 206 | + http_messages = self._callbacks.getProxyHistory() |
| 207 | + for message in http_messages: |
| 208 | + url = self._helpers.analyzeRequest(message).getUrl() |
| 209 | + url_str = url.toString() |
| 210 | + |
| 211 | + if self._callbacks.isInScope(url): |
| 212 | + if (http_selected and url.getProtocol() == http_protocol and not https_selected) or \ |
| 213 | + (https_selected and url.getProtocol() == https_protocol): |
| 214 | + urls.add(url_str) |
| 215 | + |
| 216 | + return urls |
| 217 | + |
| 218 | + def _get_unique_urls_in_scope(self): |
| 219 | + urls = set() |
| 220 | + # Get the list of IHttpRequestResponse objects in the target scope |
| 221 | + http_messages = self._callbacks.getProxyHistory() |
| 222 | + for message in http_messages: |
| 223 | + url = self._helpers.analyzeRequest(message).getUrl() |
| 224 | + if self._callbacks.isInScope(url): |
| 225 | + urls.add(url.toString()) |
| 226 | + return urls |
| 227 | + |
| 228 | + def _create_folder_structure(self, base_folder, urls): |
| 229 | + for url_str in urls: |
| 230 | + try: |
| 231 | + url = URL(url_str) |
| 232 | + # Get the path and clean it |
| 233 | + path = url.getPath().strip("/") |
| 234 | + # Replace ':' with empty string in the path |
| 235 | + path = path.replace(":", "") |
| 236 | + |
| 237 | + # Construct folder paths |
| 238 | + tld_folder = url.getHost().split('.')[-2] + '.' + url.getHost().split('.')[-1] |
| 239 | + subfolders = [url.getHost()] + path.split('/') |
| 240 | + |
| 241 | + current_path = os.path.join(base_folder, tld_folder) |
| 242 | + folder_exists = os.path.exists(current_path) |
| 243 | + |
| 244 | + for subfolder in subfolders: |
| 245 | + current_path = os.path.join(current_path, subfolder) |
| 246 | + if not os.path.exists(current_path): |
| 247 | + os.makedirs(current_path) |
| 248 | + folder_exists = False |
| 249 | + |
| 250 | + # Always create root.md in the subdomain folder |
| 251 | + subdomain_path = os.path.join(base_folder, tld_folder, url.getHost()) |
| 252 | + root_md_path = os.path.join(subdomain_path, "root.md") |
| 253 | + if not os.path.exists(root_md_path): |
| 254 | + with open(root_md_path, 'w') as md_file: |
| 255 | + md_file.write(self._get_request(url_str)) |
| 256 | + |
| 257 | + # Only create .md file if the folder did not exist |
| 258 | + if not folder_exists: |
| 259 | + if not path: |
| 260 | + md_file_path = os.path.join(current_path, "root.md") |
| 261 | + else: |
| 262 | + last_segment = subfolders[-1] |
| 263 | + md_file_path = os.path.join(current_path, "{}.md".format(last_segment)) |
| 264 | + |
| 265 | + if not os.path.exists(md_file_path): |
| 266 | + with open(md_file_path, 'w') as md_file: |
| 267 | + md_file.write(self._get_request(url_str)) |
| 268 | + except Exception as e: |
| 269 | + raise RuntimeError("Failed to create folder structure for URL '{}': {}".format(url_str, str(e))) |
| 270 | + |
| 271 | + def _get_request(self, url_str): |
| 272 | + # Get the list of IHttpRequestResponse objects in the target scope |
| 273 | + http_messages = self._callbacks.getProxyHistory() |
| 274 | + for message in http_messages: |
| 275 | + url = self._helpers.analyzeRequest(message).getUrl() |
| 276 | + if url.toString() == url_str: |
| 277 | + request_info = self._helpers.analyzeRequest(message) |
| 278 | + headers = request_info.getHeaders() |
| 279 | + body = message.getRequest()[request_info.getBodyOffset():] |
| 280 | + |
| 281 | + # Format request data |
| 282 | + request_data = "## Request" + "\n\n" + "```\n" + "\n".join(headers) + "\n\n" + self._helpers.bytesToString(body) + "\n" + "```" + "\n\n" + "## Information" |
| 283 | + return request_data |
| 284 | + return "Request data not found for URL: {}".format(url_str) |
0 commit comments