From a1376395424ac3fe900c52c8ed0d5b82af1e1255 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Mon, 24 Nov 2025 20:38:44 +0000 Subject: [PATCH 01/24] Updated TagAutomator rules to allow multiple tools per rule --- .../java/burp/hv/HackvertorExtension.java | 2 +- src/main/java/burp/hv/tags/TagAutomator.java | 160 +++++++++++++++--- 2 files changed, 142 insertions(+), 20 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 09d14e7..f6da67d 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -36,7 +36,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.16"; + public static String version = "v2.2.17"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/tags/TagAutomator.java b/src/main/java/burp/hv/tags/TagAutomator.java index 70148de..9743497 100644 --- a/src/main/java/burp/hv/tags/TagAutomator.java +++ b/src/main/java/burp/hv/tags/TagAutomator.java @@ -24,6 +24,7 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.List; import static burp.hv.HackvertorExtension.callbacks; @@ -328,10 +329,18 @@ private static void showCreateEditRuleDialog(boolean isEdit, JSONObject existing JCheckBox requestCheckbox = new JCheckBox("Request"); JCheckBox responseCheckbox = new JCheckBox("Response"); - JLabel toolLabel = new JLabel("Tool:"); + JLabel toolLabel = new JLabel("Tools:"); String[] tools = {"Proxy", "Intruder", "Repeater", "Scanner", "Extensions"}; - JComboBox toolComboBox = new JComboBox<>(tools); - toolComboBox.setSelectedItem("Repeater"); + JPanel toolPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); + JCheckBox[] toolCheckBoxes = new JCheckBox[tools.length]; + + for (int i = 0; i < tools.length; i++) { + toolCheckBoxes[i] = new JCheckBox(tools[i]); + if (tools[i].equals("Repeater")) { + toolCheckBoxes[i].setSelected(true); + } + toolPanel.add(toolCheckBoxes[i]); + } if (isEdit && existingRule != null) { JSONArray contexts = existingRule.getJSONArray("contexts"); @@ -343,8 +352,34 @@ private static void showCreateEditRuleDialog(boolean isEdit, JSONObject existing responseCheckbox.setSelected(true); } } - String tool = existingRule.optString("tool", "Repeater"); - toolComboBox.setSelectedItem(tool); + // Handle both single tool (string) and multiple tools (array) for backwards compatibility + ArrayList selectedTools = new ArrayList<>(); + if (existingRule.has("tool")) { + Object toolValue = existingRule.get("tool"); + if (toolValue instanceof String) { + selectedTools.add((String) toolValue); + } else if (toolValue instanceof JSONArray) { + JSONArray toolsArray = (JSONArray) toolValue; + for (int i = 0; i < toolsArray.length(); i++) { + selectedTools.add(toolsArray.getString(i)); + } + } + } else { + selectedTools.add("Repeater"); + } + // Clear all checkboxes first + for (JCheckBox checkBox : toolCheckBoxes) { + checkBox.setSelected(false); + } + // Set selected tools + for (String selectedTool : selectedTools) { + for (int i = 0; i < tools.length; i++) { + if (tools[i].equals(selectedTool)) { + toolCheckBoxes[i].setSelected(true); + break; + } + } + } } else { requestCheckbox.setSelected(true); } @@ -377,8 +412,34 @@ public void actionPerformed(ActionEvent e) { } } - String tool = example.optString("tool", "Repeater"); - toolComboBox.setSelectedItem(tool); + // Handle both single tool (string) and multiple tools (array) for backwards compatibility + ArrayList selectedTools = new ArrayList<>(); + if (example.has("tool")) { + Object toolValue = example.get("tool"); + if (toolValue instanceof String) { + selectedTools.add((String) toolValue); + } else if (toolValue instanceof JSONArray) { + JSONArray toolsArray = (JSONArray) toolValue; + for (int i = 0; i < toolsArray.length(); i++) { + selectedTools.add(toolsArray.getString(i)); + } + } + } else { + selectedTools.add("Repeater"); + } + // Clear all checkboxes first + for (JCheckBox checkBox : toolCheckBoxes) { + checkBox.setSelected(false); + } + // Set selected tools + for (String selectedTool : selectedTools) { + for (int i = 0; i < tools.length; i++) { + if (tools[i].equals(selectedTool)) { + toolCheckBoxes[i].setSelected(true); + break; + } + } + } } else { typeComboBox.setSelectedItem("Context Menu"); nameField.setText(""); @@ -462,7 +523,7 @@ public void actionPerformed(ActionEvent e) { mainPanel.add(contextPanel, GridbagUtils.createConstraints(1, y++, 1, GridBagConstraints.BOTH, 1, 0, 5, 5, GridBagConstraints.CENTER)); mainPanel.add(toolLabel, GridbagUtils.createConstraints(0, y, 1, GridBagConstraints.BOTH, 0, 0, 5, 5, GridBagConstraints.WEST)); - mainPanel.add(toolComboBox, GridbagUtils.createConstraints(1, y++, 1, GridBagConstraints.BOTH, 1, 0, 5, 5, GridBagConstraints.WEST)); + mainPanel.add(toolPanel, GridbagUtils.createConstraints(1, y++, 1, GridBagConstraints.BOTH, 1, 0, 5, 5, GridBagConstraints.WEST)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); JButton saveButton = new JButton(isEdit ? "Update" : "Create"); @@ -498,9 +559,31 @@ public void actionPerformed(ActionEvent e) { "Validation Error", JOptionPane.ERROR_MESSAGE); return; } - String tool = toolComboBox.getSelectedItem().toString(); - if(!tool.equals("Repeater")) { - int confirm = JOptionPane.showConfirmDialog(null, "Running Python on every "+tool+" request or response can affect Burp's performance, are you sure?"); + List selectedTools = new ArrayList<>(); + for (int i = 0; i < toolCheckBoxes.length; i++) { + if (toolCheckBoxes[i].isSelected()) { + selectedTools.add(tools[i]); + } + } + if(selectedTools.isEmpty()) { + JOptionPane.showMessageDialog(dialog, "At least one tool must be selected", + "Validation Error", JOptionPane.ERROR_MESSAGE); + return; + } + + boolean hasNonRepeaterTool = false; + for(String tool : selectedTools) { + if(!tool.equals("Repeater")) { + hasNonRepeaterTool = true; + break; + } + } + + if(hasNonRepeaterTool) { + String toolsMessage = String.join(", ", selectedTools); + int confirm = JOptionPane.showConfirmDialog(null, + "Running Python on every request or response in " + toolsMessage + + " can affect Burp's performance, are you sure?"); if(confirm != 0) { return; } @@ -521,7 +604,12 @@ public void actionPerformed(ActionEvent e) { rule.put("modification", modification); rule.put("contexts", contexts); rule.put("enabled", enabledCheckbox.isSelected()); - rule.put("tool", (String) toolComboBox.getSelectedItem()); + // Save tools as array + JSONArray toolsArray = new JSONArray(); + for(String tool : selectedTools) { + toolsArray.put(tool); + } + rule.put("tool", toolsArray); if (isEdit) { for (int i = 0; i < rules.length(); i++) { @@ -593,7 +681,26 @@ public static boolean shouldApplyRules(String context, String tool, String type) } if(!rule.getString("type").equals(type)) continue; JSONArray contexts = rule.getJSONArray("contexts"); - String ruleTool = rule.optString("tool", "Repeater"); + + // Handle both single tool (string) and multiple tools (array) for backwards compatibility + boolean toolMatches = false; + if (rule.has("tool")) { + Object toolValue = rule.get("tool"); + if (toolValue instanceof String) { + toolMatches = ((String) toolValue).equalsIgnoreCase(tool); + } else if (toolValue instanceof JSONArray) { + JSONArray toolsArray = (JSONArray) toolValue; + for (int k = 0; k < toolsArray.length(); k++) { + if (toolsArray.getString(k).equalsIgnoreCase(tool)) { + toolMatches = true; + break; + } + } + } + } else { + // Default to Repeater for backwards compatibility + toolMatches = "Repeater".equalsIgnoreCase(tool); + } boolean appliesTo = false; for (int j = 0; j < contexts.length(); j++) { @@ -603,8 +710,6 @@ public static boolean shouldApplyRules(String context, String tool, String type) } } - boolean toolMatches = ruleTool.equalsIgnoreCase(tool); - if (appliesTo && toolMatches) { return true; } @@ -633,7 +738,26 @@ public static String applyRules(String content, String context, String tool, Str } JSONArray contexts = rule.getJSONArray("contexts"); - String ruleTool = rule.optString("tool", "Repeater"); + + // Handle both single tool (string) and multiple tools (array) for backwards compatibility + boolean toolMatches = false; + if (rule.has("tool")) { + Object toolValue = rule.get("tool"); + if (toolValue instanceof String) { + toolMatches = ((String) toolValue).equalsIgnoreCase(tool); + } else if (toolValue instanceof JSONArray) { + JSONArray toolsArray = (JSONArray) toolValue; + for (int k = 0; k < toolsArray.length(); k++) { + if (toolsArray.getString(k).equalsIgnoreCase(tool)) { + toolMatches = true; + break; + } + } + } + } else { + // Default to Repeater for backwards compatibility + toolMatches = "Repeater".equalsIgnoreCase(tool); + } boolean appliesTo = false; for (int j = 0; j < contexts.length(); j++) { @@ -642,9 +766,7 @@ public static String applyRules(String content, String context, String tool, Str break; } } - - boolean toolMatches = ruleTool.equalsIgnoreCase(tool); - + if (appliesTo && toolMatches) { String analysis = rule.getString("analysis"); String modification = rule.getString("modification"); From 82a9bf5aa51d0daf2fbbc039421b772215816fb3 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Mon, 24 Nov 2025 21:52:52 +0000 Subject: [PATCH 02/24] Added multi encoder window --- .../java/burp/hv/HackvertorExtension.java | 36 +- .../burp/hv/ui/HackvertorContextMenu.java | 28 + .../java/burp/hv/ui/MultiEncoderWindow.java | 499 ++++++++++++++++++ 3 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 src/main/java/burp/hv/ui/MultiEncoderWindow.java diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index f6da67d..a20d19e 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -10,7 +10,6 @@ import burp.api.montoya.ui.hotkey.HotKey; import burp.api.montoya.ui.hotkey.HotKeyContext; import burp.api.montoya.ui.hotkey.HotKeyHandler; -import burp.api.montoya.utilities.CompressionType; import burp.hv.settings.Settings; import burp.hv.tags.CustomTags; import burp.hv.tags.Tag; @@ -36,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.17"; + public static String version = "v2.2.18"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; @@ -228,7 +227,8 @@ private void registerAllHotkeys(MontoyaApi montoyaApi, Burp burp) { new HotkeyDefinition("Tag Automator", "Ctrl+Alt+A", event -> TagAutomator.showRulesDialog()), new HotkeyDefinition("Settings", "Ctrl+Alt+S", event -> Settings.showSettingsWindow()), new HotkeyDefinition("Smart decode", "Ctrl+Alt+D", createAutoDecodeHandler()), - new HotkeyDefinition("Show tag store", "Ctrl+Alt+T", event -> TagStore.showTagStore()) + new HotkeyDefinition("Show tag store", "Ctrl+Alt+T", event -> TagStore.showTagStore()), + new HotkeyDefinition("Multi Encoder", "Ctrl+Alt+M", createMultiEncoderHandler(montoyaApi)) ); for (HotkeyDefinition hotkey : hotkeys) { @@ -291,4 +291,34 @@ private HotKeyHandler createAutoDecodeHandler() { } }; } + + private HotKeyHandler createMultiEncoderHandler(MontoyaApi montoyaApi) { + return event -> { + if (event.messageEditorRequestResponse().isEmpty()) { + return; + } + MessageEditorHttpRequestResponse requestResponse = event.messageEditorRequestResponse().get(); + if(requestResponse.selectionOffsets().isPresent() && + requestResponse.selectionContext().toString().equalsIgnoreCase("request")) { + String request = requestResponse.requestResponse().request().toString(); + int start = requestResponse.selectionOffsets().get().startIndexInclusive(); + int end = requestResponse.selectionOffsets().get().endIndexExclusive(); + + if (start != end) { + String selectedText = request.substring(start, end); + ArrayList tags = HackvertorExtension.hackvertor.getTags(); + + // Show the Multi Encoder window + MultiEncoderWindow multiEncoderWindow = new MultiEncoderWindow( + montoyaApi, + selectedText, + tags, + requestResponse, + requestResponse.requestResponse() + ); + multiEncoderWindow.show(); + } + } + }; + } } diff --git a/src/main/java/burp/hv/ui/HackvertorContextMenu.java b/src/main/java/burp/hv/ui/HackvertorContextMenu.java index 95dc6dc..7561cb0 100644 --- a/src/main/java/burp/hv/ui/HackvertorContextMenu.java +++ b/src/main/java/burp/hv/ui/HackvertorContextMenu.java @@ -243,6 +243,34 @@ public List provideMenuItems(ContextMenuEvent event) { } }); menu.add(autodecodeConvert); + + // Multi Encoder feature + JMenuItem multiEncoder = new JMenuItem("Multi Encoder (Ctrl+Alt+M)"); + multiEncoder.setEnabled(start != end); + multiEncoder.addActionListener(e -> { + if (event.invocationType() == InvocationType.MESSAGE_EDITOR_REQUEST || event.invocationType() == InvocationType.MESSAGE_VIEWER_REQUEST) { + if(event.messageEditorRequestResponse().isPresent()) { + HttpRequest request = event.messageEditorRequestResponse().get().requestResponse().request(); + String requestStr = request.toString(); + String selectedText = requestStr.substring(start, end); + + // Get all available tags + ArrayList tags = HackvertorExtension.hackvertor.getTags(); + + // Show the Multi Encoder window + MultiEncoderWindow multiEncoderWindow = new MultiEncoderWindow( + montoyaApi, + selectedText, + tags, + event.messageEditorRequestResponse().get(), + event.messageEditorRequestResponse().get().requestResponse() + ); + multiEncoderWindow.show(); + } + } + }); + menu.add(multiEncoder); + menu.addSeparator(); CustomTags.loadCustomTags(); if(allowTagCount) { diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java new file mode 100644 index 0000000..6b59890 --- /dev/null +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -0,0 +1,499 @@ +package burp.hv.ui; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; +import burp.hv.Convertors; +import burp.hv.HackvertorExtension; +import burp.hv.tags.Tag; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.geom.RoundRectangle2D; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static burp.hv.HackvertorExtension.montoyaApi; + +public class MultiEncoderWindow { + private static final int DEFAULT_WIDTH = 900; + private static final int DEFAULT_HEIGHT = 600; + private static final int CORNER_RADIUS = 20; + private static final int COLUMN_COUNT = 3; + + private final MontoyaApi montoyaApi; + private final String selectedText; + private final ArrayList tags; + private final Map tagCheckboxes; + private final MessageEditorHttpRequestResponse messageEditor; + private final HttpRequestResponse baseRequestResponse; + private JTextArea previewArea; + private JWindow window; + private ArrayList selectedTags; + private JComboBox modeComboBox; + + public MultiEncoderWindow(MontoyaApi montoyaApi, String selectedText, ArrayList tags, + MessageEditorHttpRequestResponse messageEditor, HttpRequestResponse baseRequestResponse) { + this.montoyaApi = montoyaApi; + this.selectedText = selectedText; + this.tags = tags; + this.tagCheckboxes = new HashMap<>(); + this.selectedTags = new ArrayList<>(); + this.messageEditor = messageEditor; + this.baseRequestResponse = baseRequestResponse; + } + + public void show() { + SwingUtilities.invokeLater(() -> { + // Create window + window = new JWindow(montoyaApi.userInterface().swingUtils().suiteFrame()); + window.setLayout(new BorderLayout()); + window.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); + + // Apply rounded corners + Runnable applyRoundedCorners = () -> { + try { + window.setBackground(new Color(0, 0, 0, 0)); + SwingUtilities.invokeLater(() -> { + Shape shape = new RoundRectangle2D.Float(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT, + CORNER_RADIUS, CORNER_RADIUS); + window.setShape(shape); + }); + } catch (UnsupportedOperationException ignored) {} + }; + + // Main panel with title + JPanel mainPanel = new JPanel(new BorderLayout()); + mainPanel.setBorder(new EmptyBorder(14, 14, 14, 14)); + montoyaApi.userInterface().applyThemeToComponent(mainPanel); + + // Add title panel + JPanel titlePanel = new JPanel(new BorderLayout()); + JLabel titleLabel = new JLabel("Multi Encoder"); + titleLabel.setFont(new Font("Inter", Font.BOLD, 16)); + titlePanel.add(titleLabel, BorderLayout.WEST); + titlePanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + montoyaApi.userInterface().applyThemeToComponent(titlePanel); + montoyaApi.userInterface().applyThemeToComponent(titleLabel); + + // Create search and tags panel + JPanel topPanel = new JPanel(new BorderLayout()); + topPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0)); + montoyaApi.userInterface().applyThemeToComponent(topPanel); + + // Search field + JTextField searchField = new JTextField(); + searchField.setFont(new Font("Monospaced", Font.PLAIN, 14)); + searchField.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); + montoyaApi.userInterface().applyThemeToComponent(searchField); + + JLabel searchLabel = new JLabel("Search tags: "); + searchLabel.setFont(new Font("Inter", Font.PLAIN, 13)); + montoyaApi.userInterface().applyThemeToComponent(searchLabel); + + JPanel searchPanel = new JPanel(new BorderLayout()); + searchPanel.add(searchLabel, BorderLayout.WEST); + searchPanel.add(searchField, BorderLayout.CENTER); + montoyaApi.userInterface().applyThemeToComponent(searchPanel); + + topPanel.add(searchPanel, BorderLayout.NORTH); + + // Tags panel with checkboxes + JPanel tagsPanel = new JPanel(new GridBagLayout()); + montoyaApi.userInterface().applyThemeToComponent(tagsPanel); + + JScrollPane tagsScrollPane = new JScrollPane(tagsPanel); + tagsScrollPane.setPreferredSize(new Dimension(DEFAULT_WIDTH - 50, 200)); + tagsScrollPane.setBorder(BorderFactory.createTitledBorder("Select Tags")); + tagsScrollPane.getVerticalScrollBar().setUnitIncrement(16); + montoyaApi.userInterface().applyThemeToComponent(tagsScrollPane); + + topPanel.add(tagsScrollPane, BorderLayout.CENTER); + + // Preview section + JPanel previewPanel = new JPanel(new BorderLayout()); + previewPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); + + previewArea = new JTextArea(); + previewArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); + previewArea.setEditable(false); + montoyaApi.userInterface().applyThemeToComponent(previewArea); + + JScrollPane previewScrollPane = new JScrollPane(previewArea); + previewScrollPane.setPreferredSize(new Dimension(DEFAULT_WIDTH - 50, 200)); + previewScrollPane.setBorder(BorderFactory.createTitledBorder("Preview")); + montoyaApi.userInterface().applyThemeToComponent(previewScrollPane); + + previewPanel.add(previewScrollPane, BorderLayout.CENTER); + + // Button panel + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + montoyaApi.userInterface().applyThemeToComponent(buttonPanel); + + // Add mode selection combo box + JLabel modeLabel = new JLabel("Mode:"); + modeLabel.setFont(new Font("Inter", Font.PLAIN, 12)); + montoyaApi.userInterface().applyThemeToComponent(modeLabel); + + String[] modes = {"Convert", "Add tags"}; + modeComboBox = new JComboBox<>(modes); + modeComboBox.setSelectedItem("Convert"); + modeComboBox.addActionListener(e -> updatePreview()); + montoyaApi.userInterface().applyThemeToComponent(modeComboBox); + + JButton previewButton = new JButton("Update Preview"); + previewButton.addActionListener(e -> updatePreview()); + montoyaApi.userInterface().applyThemeToComponent(previewButton); + + JButton sendToRepeaterButton = new JButton("Send to Repeater"); + sendToRepeaterButton.addActionListener(e -> sendToRepeater()); + montoyaApi.userInterface().applyThemeToComponent(sendToRepeaterButton); + + JButton sendToIntruderButton = new JButton("Send to Intruder"); + sendToIntruderButton.addActionListener(e -> sendToIntruder()); + montoyaApi.userInterface().applyThemeToComponent(sendToIntruderButton); + + JButton cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> window.dispose()); + montoyaApi.userInterface().applyThemeToComponent(cancelButton); + + buttonPanel.add(modeLabel); + buttonPanel.add(modeComboBox); + buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); + buttonPanel.add(previewButton); + buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); + buttonPanel.add(sendToRepeaterButton); + buttonPanel.add(sendToIntruderButton); + buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); + buttonPanel.add(cancelButton); + + // Helper: Filter tags + Function> filterTags = searchText -> { + ArrayList filtered = new ArrayList<>(); + String lowerSearch = searchText.toLowerCase(); + for (Tag tag : tags) { + if (lowerSearch.isEmpty() || + tag.name.toLowerCase().contains(lowerSearch) || + tag.category.toString().toLowerCase().contains(lowerSearch) || + (tag.tooltip != null && tag.tooltip.toLowerCase().contains(lowerSearch))) { + filtered.add(tag); + } + } + filtered.sort((a, b) -> a.name.compareToIgnoreCase(b.name)); + return filtered; + }; + + // Helper: Create tag checkbox + Function createTagCheckbox = tag -> { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); + JCheckBox checkbox = new JCheckBox(tag.name); + checkbox.setToolTipText(tag.tooltip != null ? tag.tooltip : tag.category.toString()); + checkbox.setFont(new Font("Inter", Font.PLAIN, 12)); + checkbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + montoyaApi.userInterface().applyThemeToComponent(checkbox); + montoyaApi.userInterface().applyThemeToComponent(panel); + + tagCheckboxes.put(tag.name, checkbox); + + checkbox.addActionListener(e -> { + if (checkbox.isSelected()) { + if (!selectedTags.contains(tag)) { + selectedTags.add(tag); + } + } else { + selectedTags.remove(tag); + } + updatePreview(); + }); + + panel.add(checkbox); + return panel; + }; + + // Update tags display + Runnable updateTags = () -> { + tagsPanel.removeAll(); + tagCheckboxes.clear(); + + ArrayList filteredTags = filterTags.apply(searchField.getText()); + + if (!filteredTags.isEmpty()) { + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(3, 3, 3, 3); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridy = 0; + + int currentColumn = 0; + for (Tag tag : filteredTags) { + JPanel tagPanel = createTagCheckbox.apply(tag); + gbc.gridx = currentColumn; + gbc.weightx = 1.0 / COLUMN_COUNT; + tagsPanel.add(tagPanel, gbc); + + if (++currentColumn >= COLUMN_COUNT) { + currentColumn = 0; + gbc.gridy++; + } + } + } else { + JLabel noResultsLabel = new JLabel("No tags found matching: " + searchField.getText()); + noResultsLabel.setFont(new Font("Inter", Font.PLAIN, 13)); + montoyaApi.userInterface().applyThemeToComponent(noResultsLabel); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = COLUMN_COUNT; + tagsPanel.add(noResultsLabel, gbc); + } + + tagsPanel.revalidate(); + tagsPanel.repaint(); + }; + + // Event listeners + searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { + public void insertUpdate(javax.swing.event.DocumentEvent e) { updateTags.run(); } + public void removeUpdate(javax.swing.event.DocumentEvent e) { updateTags.run(); } + public void changedUpdate(javax.swing.event.DocumentEvent e) { updateTags.run(); } + }); + + searchField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + window.dispose(); + } + } + }); + + // Add window focus listener - dispose when focus is lost + window.addWindowFocusListener(new java.awt.event.WindowAdapter() { + @Override + public void windowLostFocus(java.awt.event.WindowEvent e) { + window.dispose(); + } + }); + + // Assemble UI + JPanel contentPanel = new JPanel(new BorderLayout()); + contentPanel.add(titlePanel, BorderLayout.NORTH); + contentPanel.add(topPanel, BorderLayout.CENTER); + montoyaApi.userInterface().applyThemeToComponent(contentPanel); + + mainPanel.add(contentPanel, BorderLayout.NORTH); + mainPanel.add(previewPanel, BorderLayout.CENTER); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + // Initialize window + applyRoundedCorners.run(); + window.add(mainPanel); + + // Apply theme to the window's content pane if available + montoyaApi.userInterface().applyThemeToComponent(window.getContentPane()); + window.setLocationRelativeTo(montoyaApi.userInterface().swingUtils().suiteFrame()); + + // Initialize content and show + updateTags.run(); + window.setVisible(true); + searchField.requestFocusInWindow(); + }); + } + + private void updatePreview() { + if (selectedTags.isEmpty()) { + previewArea.setText("No tags selected. Please select at least one tag."); + return; + } + + StringBuilder preview = new StringBuilder(); + preview.append("Selected text: ").append(selectedText).append("\n"); + preview.append("Mode: ").append(modeComboBox.getSelectedItem()).append("\n"); + preview.append("=====================================\n\n"); + + boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); + + for (Tag tag : selectedTags) { + String[] tagStartEnd = Convertors.generateTagStartEnd(tag); + String tagStart = tagStartEnd[0]; + String tagEnd = tagStartEnd[1]; + String taggedText = tagStart + selectedText + tagEnd; + + String result; + if (shouldConvert) { + // Process through Hackvertor to get actual encoded result + try { + result = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); + } catch (Exception ex) { + result = "Error: " + ex.getMessage(); + } + } else { + // Just add tags without converting + result = taggedText; + } + + preview.append("Tag: ").append(tag.name).append("\n"); + if (!shouldConvert) { + preview.append("Tagged: ").append(result).append("\n"); + } else { + preview.append("Input: ").append(taggedText).append("\n"); + preview.append("Result: ").append(result).append("\n"); + } + preview.append("-------------------------------------\n"); + } + + previewArea.setText(preview.toString()); + previewArea.setCaretPosition(0); + } + + private void sendToRepeater() { + if (selectedTags.isEmpty()) { + JOptionPane.showMessageDialog(window, + "Please select at least one tag before sending to Repeater.", + "No Tags Selected", + JOptionPane.WARNING_MESSAGE); + return; + } + + if (messageEditor == null || baseRequestResponse == null) { + JOptionPane.showMessageDialog(window, + "Unable to access the original request.", + "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + + HttpRequest baseRequest = baseRequestResponse.request(); + String requestStr = baseRequest.toString(); + boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); + + for (Tag tag : selectedTags) { + String[] tagStartEnd = Convertors.generateTagStartEnd(tag); + String tagStart = tagStartEnd[0]; + String tagEnd = tagStartEnd[1]; + String taggedText = tagStart + selectedText + tagEnd; + + String replacementText; + if (shouldConvert) { + // Process through Hackvertor to get actual encoded result + try { + replacementText = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); + } catch (Exception ex) { + replacementText = selectedText; // Fallback to original if error + } + } else { + // Just add tags without converting + replacementText = taggedText; + } + + // Replace the selected text with the result + String modifiedRequestStr = requestStr.replace(selectedText, replacementText); + HttpRequest modifiedRequest = HttpRequest.httpRequest(modifiedRequestStr); + + // Send to Repeater with a descriptive tab name + String modePrefix = shouldConvert ? "HV-" : "HVT-"; + String tabName = modePrefix + tag.name + "-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); + montoyaApi.repeater().sendToRepeater(modifiedRequest, tabName); + } + + JOptionPane.showMessageDialog(window, + "Sent " + selectedTags.size() + " requests to Repeater.", + "Success", + JOptionPane.INFORMATION_MESSAGE); + + window.dispose(); + } + + private void sendToIntruder() { + if (selectedTags.isEmpty()) { + JOptionPane.showMessageDialog(window, + "Please select at least one tag before sending to Intruder.", + "No Tags Selected", + JOptionPane.WARNING_MESSAGE); + return; + } + + if (messageEditor == null || baseRequestResponse == null) { + JOptionPane.showMessageDialog(window, + "Unable to access the original request.", + "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + + HttpRequest baseRequest = baseRequestResponse.request(); + String requestStr = baseRequest.toString(); + + // Find the position of the selected text in the request + int startPos = requestStr.indexOf(selectedText); + int endPos = startPos + selectedText.length(); + + if (startPos == -1) { + JOptionPane.showMessageDialog(window, + "Could not find the selected text in the request.", + "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + + // Create a payload marker for Intruder + String beforeMarker = requestStr.substring(0, startPos); + String afterMarker = requestStr.substring(endPos); + String markedRequest = beforeMarker + "§" + selectedText + "§" + afterMarker; + + HttpRequest intruderRequest = HttpRequest.httpRequest(markedRequest); + + // Send to Intruder + montoyaApi.intruder().sendToIntruder(intruderRequest); + + // Generate payload list for Intruder + StringBuilder payloads = new StringBuilder(); + payloads.append("Hackvertor Multi-Encoder Payloads:\n"); + + boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); + payloads.append("Mode: ").append(modeComboBox.getSelectedItem()).append("\n"); + payloads.append("Copy these payloads to use in Intruder:\n\n"); + + for (Tag tag : selectedTags) { + String[] tagStartEnd = Convertors.generateTagStartEnd(tag); + String tagStart = tagStartEnd[0]; + String tagEnd = tagStartEnd[1]; + String taggedText = tagStart + selectedText + tagEnd; + + String payloadResult; + if (shouldConvert) { + // Process through Hackvertor to get actual encoded result + try { + payloadResult = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); + } catch (Exception ex) { + payloadResult = selectedText; // Fallback to original if error + } + } else { + // Just add tags without converting + payloadResult = taggedText; + } + + payloads.append(payloadResult).append("\n"); + } + + // Show payloads in a window + JTextArea payloadArea = new JTextArea(payloads.toString()); + payloadArea.setEditable(false); + payloadArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); + JScrollPane scrollPane = new JScrollPane(payloadArea); + scrollPane.setPreferredSize(new Dimension(600, 400)); + + JOptionPane.showMessageDialog(window, + scrollPane, + "Payloads for Intruder", + JOptionPane.INFORMATION_MESSAGE); + + window.dispose(); + } +} \ No newline at end of file From 02ed32f1597036c616b2baa384224f4edccd5483 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 12:49:12 +0000 Subject: [PATCH 03/24] Added websockets setting and websocket handler --- .../java/burp/hv/HackvertorExtension.java | 3 +- .../burp/hv/HackvertorMessageHandler.java | 31 +++++++++++++++++++ .../burp/hv/HackvertorWebSocketHandler.java | 12 +++++++ src/main/java/burp/hv/utils/Utils.java | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/main/java/burp/hv/HackvertorMessageHandler.java create mode 100644 src/main/java/burp/hv/HackvertorWebSocketHandler.java diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index a20d19e..10f49cd 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.18"; + public static String version = "v2.2.19"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; @@ -185,6 +185,7 @@ public void initialize(MontoyaApi montoyaApi) { montoyaApi.userInterface().menuBar().registerMenu(Utils.generateHackvertorMenuBar()); Burp burp = new Burp(montoyaApi.burpSuite().version()); montoyaApi.http().registerHttpHandler(new HackvertorHttpHandler()); + montoyaApi.websockets().registerWebSocketCreatedHandler(new HackvertorWebSocketHandler()); montoyaApi.userInterface().registerContextMenuItemsProvider(new HackvertorContextMenu()); if(burp.hasCapability(Burp.Capability.REGISTER_HOTKEY)) { diff --git a/src/main/java/burp/hv/HackvertorMessageHandler.java b/src/main/java/burp/hv/HackvertorMessageHandler.java new file mode 100644 index 0000000..ea09770 --- /dev/null +++ b/src/main/java/burp/hv/HackvertorMessageHandler.java @@ -0,0 +1,31 @@ +package burp.hv; + +import burp.api.montoya.websocket.*; +import burp.hv.settings.InvalidTypeSettingException; +import burp.hv.settings.UnregisteredSettingException; + +public class HackvertorMessageHandler implements MessageHandler { + + @Override + public TextMessageAction handleTextMessage(TextMessage textMessage) { + boolean tagsInWebSockets; + try { + tagsInWebSockets = HackvertorExtension.generalSettings.getBoolean("tagsInWebSockets"); + } catch (UnregisteredSettingException | InvalidTypeSettingException e) { + HackvertorExtension.callbacks.printError("Error loading settings:" + e); + throw new RuntimeException(e); + } + if(tagsInWebSockets) { + if (textMessage.payload().contains("<@")) { + String converted = HackvertorExtension.hackvertor.convert(textMessage.payload(), HackvertorExtension.hackvertor); + return TextMessageAction.continueWith(converted); + } + } + return TextMessageAction.continueWith(textMessage); + } + + @Override + public BinaryMessageAction handleBinaryMessage(BinaryMessage binaryMessage) { + return BinaryMessageAction.continueWith(binaryMessage); + } +} diff --git a/src/main/java/burp/hv/HackvertorWebSocketHandler.java b/src/main/java/burp/hv/HackvertorWebSocketHandler.java new file mode 100644 index 0000000..999de59 --- /dev/null +++ b/src/main/java/burp/hv/HackvertorWebSocketHandler.java @@ -0,0 +1,12 @@ +package burp.hv; + +import burp.api.montoya.websocket.WebSocketCreated; +import burp.api.montoya.websocket.WebSocketCreatedHandler; + +public class HackvertorWebSocketHandler implements WebSocketCreatedHandler { + + @Override + public void handleWebSocketCreated(WebSocketCreated webSocketCreated) { + webSocketCreated.webSocket().registerMessageHandler(new HackvertorMessageHandler()); + } +} diff --git a/src/main/java/burp/hv/utils/Utils.java b/src/main/java/burp/hv/utils/Utils.java index ece2910..fef13ed 100644 --- a/src/main/java/burp/hv/utils/Utils.java +++ b/src/main/java/burp/hv/utils/Utils.java @@ -74,6 +74,7 @@ public static void registerGeneralSettings(Settings settings) { settings.registerBooleanSetting("tagsInScanner", true, "Allow tags in Scanner", "Tag permissions", null); settings.registerBooleanSetting("tagsInExtensions", true, "Allow tags in Extensions", "Tag permissions", null); settings.registerBooleanSetting("tagsInResponse", false, "Allow tags in HTTP response", "Tag permissions", null); + settings.registerBooleanSetting("tagsInWebSockets", false, "Allow tags in WebSockets", "Tag permissions", null); settings.registerBooleanSetting("codeExecutionTagsEnabled", false, "Allow code execution tags", "Tag permissions", "Using code execution tags on untrusted requests can compromise your system, are you sure?"); settings.registerBooleanSetting("autoUpdateContentLength", true, "Auto update content length", "Requests", null); settings.registerIntegerSetting("maxBodyLength", 3 * 1024 * 1024, "Maximum body length", "Requests"); From d42d544a2cbd706d44355b3893850666d4922404 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 19:51:49 +0000 Subject: [PATCH 04/24] Added copy to clipboard button, clear button and select all checkbox to the MultiEncoderWindow. --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 93 ++++++++++++++++++- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 10f49cd..0ddc4f4 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.19"; + public static String version = "v2.2.20"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index 6b59890..a2fdf1f 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -11,6 +11,8 @@ import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.geom.RoundRectangle2D; @@ -102,7 +104,18 @@ public void show() { searchPanel.add(searchField, BorderLayout.CENTER); montoyaApi.userInterface().applyThemeToComponent(searchPanel); - topPanel.add(searchPanel, BorderLayout.NORTH); + JCheckBox selectAllCheckbox = new JCheckBox("Select all"); + selectAllCheckbox.setFont(new Font("Inter", Font.PLAIN, 12)); + selectAllCheckbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); + selectAllCheckbox.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 0)); + montoyaApi.userInterface().applyThemeToComponent(selectAllCheckbox); + + JPanel searchAndSelectAllPanel = new JPanel(new BorderLayout()); + searchAndSelectAllPanel.add(searchPanel, BorderLayout.CENTER); + searchAndSelectAllPanel.add(selectAllCheckbox, BorderLayout.SOUTH); + montoyaApi.userInterface().applyThemeToComponent(searchAndSelectAllPanel); + + topPanel.add(searchAndSelectAllPanel, BorderLayout.NORTH); // Tags panel with checkboxes JPanel tagsPanel = new JPanel(new GridBagLayout()); @@ -163,6 +176,50 @@ public void show() { cancelButton.addActionListener(e -> window.dispose()); montoyaApi.userInterface().applyThemeToComponent(cancelButton); + JButton clearButton = new JButton("Clear"); + clearButton.addActionListener(e -> { + for (JCheckBox checkbox : tagCheckboxes.values()) { + checkbox.setSelected(false); + } + selectedTags.clear(); + selectAllCheckbox.setSelected(false); + previewArea.setText(""); + }); + montoyaApi.userInterface().applyThemeToComponent(clearButton); + + JButton copyButton = new JButton("Copy"); + copyButton.addActionListener(e -> { + if (selectedTags.isEmpty()) { + return; + } + StringBuilder output = new StringBuilder(); + boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); + for (Tag tag : selectedTags) { + String[] tagStartEnd = Convertors.generateTagStartEnd(tag); + String tagStart = tagStartEnd[0]; + String tagEnd = tagStartEnd[1]; + String taggedText = tagStart + selectedText + tagEnd; + String result; + if (shouldConvert) { + try { + result = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); + } catch (Exception ex) { + result = "Error: " + ex.getMessage(); + } + } else { + result = taggedText; + } + output.append(result).append("\n"); + } + StringSelection selection = new StringSelection(output.toString().trim()); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(selection, null); + }); + montoyaApi.userInterface().applyThemeToComponent(copyButton); + + buttonPanel.add(clearButton); + buttonPanel.add(copyButton); + buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); buttonPanel.add(modeLabel); buttonPanel.add(modeComboBox); buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); @@ -257,11 +314,39 @@ public void show() { tagsPanel.repaint(); }; + selectAllCheckbox.addActionListener(e -> { + ArrayList filteredTags = filterTags.apply(searchField.getText()); + boolean selectAll = selectAllCheckbox.isSelected(); + for (Tag tag : filteredTags) { + JCheckBox checkbox = tagCheckboxes.get(tag.name); + if (checkbox != null && checkbox.isSelected() != selectAll) { + checkbox.setSelected(selectAll); + if (selectAll) { + if (!selectedTags.contains(tag)) { + selectedTags.add(tag); + } + } else { + selectedTags.remove(tag); + } + } + } + updatePreview(); + }); + // Event listeners searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - public void insertUpdate(javax.swing.event.DocumentEvent e) { updateTags.run(); } - public void removeUpdate(javax.swing.event.DocumentEvent e) { updateTags.run(); } - public void changedUpdate(javax.swing.event.DocumentEvent e) { updateTags.run(); } + public void insertUpdate(javax.swing.event.DocumentEvent e) { + selectAllCheckbox.setSelected(false); + updateTags.run(); + } + public void removeUpdate(javax.swing.event.DocumentEvent e) { + selectAllCheckbox.setSelected(false); + updateTags.run(); + } + public void changedUpdate(javax.swing.event.DocumentEvent e) { + selectAllCheckbox.setSelected(false); + updateTags.run(); + } }); searchField.addKeyListener(new KeyAdapter() { From 047842b796129c50456e7fa99ecdff6604dab6c6 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 20:15:43 +0000 Subject: [PATCH 05/24] Fixed send to intruder --- src/main/java/burp/hv/ui/MultiEncoderWindow.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index a2fdf1f..2638296 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -3,6 +3,8 @@ import burp.api.montoya.MontoyaApi; import burp.api.montoya.http.message.HttpRequestResponse; import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.core.Range; +import burp.api.montoya.intruder.HttpRequestTemplate; import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; import burp.hv.Convertors; import burp.hv.HackvertorExtension; @@ -17,6 +19,7 @@ import java.awt.event.KeyEvent; import java.awt.geom.RoundRectangle2D; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -527,15 +530,10 @@ private void sendToIntruder() { return; } - // Create a payload marker for Intruder - String beforeMarker = requestStr.substring(0, startPos); - String afterMarker = requestStr.substring(endPos); - String markedRequest = beforeMarker + "§" + selectedText + "§" + afterMarker; - - HttpRequest intruderRequest = HttpRequest.httpRequest(markedRequest); - - // Send to Intruder - montoyaApi.intruder().sendToIntruder(intruderRequest); + Range insertionPoint = Range.range(startPos, endPos); + HttpRequestTemplate intruderTemplate = HttpRequestTemplate.httpRequestTemplate(baseRequest, Collections.singletonList(insertionPoint)); + String tabName = "HV-Multi-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); + montoyaApi.intruder().sendToIntruder(baseRequestResponse.request().httpService(), intruderTemplate, tabName); // Generate payload list for Intruder StringBuilder payloads = new StringBuilder(); From bbe570bc045f4b0f214cc2ae5b0f6a3a3b9f01fa Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 20:32:36 +0000 Subject: [PATCH 06/24] Added layers to MultiEncoderWindow --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 633 +++++++++--------- 2 files changed, 333 insertions(+), 302 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 0ddc4f4..4ab9adb 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.20"; + public static String version = "v2.2.21"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index 2638296..be1f276 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -24,8 +24,6 @@ import java.util.Map; import java.util.function.Function; -import static burp.hv.HackvertorExtension.montoyaApi; - public class MultiEncoderWindow { private static final int DEFAULT_WIDTH = 900; private static final int DEFAULT_HEIGHT = 600; @@ -35,33 +33,49 @@ public class MultiEncoderWindow { private final MontoyaApi montoyaApi; private final String selectedText; private final ArrayList tags; - private final Map tagCheckboxes; private final MessageEditorHttpRequestResponse messageEditor; private final HttpRequestResponse baseRequestResponse; + private final ArrayList layers; private JTextArea previewArea; private JWindow window; - private ArrayList selectedTags; private JComboBox modeComboBox; + private JTabbedPane layerTabbedPane; + private int layerCounter = 1; + + private class Layer { + final Map tagCheckboxes; + final ArrayList selectedTags; + final JPanel tagsPanel; + final JTextField searchField; + final JCheckBox selectAllCheckbox; + final Runnable updateTags; + + Layer(JPanel tagsPanel, JTextField searchField, JCheckBox selectAllCheckbox, Runnable updateTags) { + this.tagCheckboxes = new HashMap<>(); + this.selectedTags = new ArrayList<>(); + this.tagsPanel = tagsPanel; + this.searchField = searchField; + this.selectAllCheckbox = selectAllCheckbox; + this.updateTags = updateTags; + } + } public MultiEncoderWindow(MontoyaApi montoyaApi, String selectedText, ArrayList tags, MessageEditorHttpRequestResponse messageEditor, HttpRequestResponse baseRequestResponse) { this.montoyaApi = montoyaApi; this.selectedText = selectedText; this.tags = tags; - this.tagCheckboxes = new HashMap<>(); - this.selectedTags = new ArrayList<>(); this.messageEditor = messageEditor; this.baseRequestResponse = baseRequestResponse; + this.layers = new ArrayList<>(); } public void show() { SwingUtilities.invokeLater(() -> { - // Create window window = new JWindow(montoyaApi.userInterface().swingUtils().suiteFrame()); window.setLayout(new BorderLayout()); window.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); - // Apply rounded corners Runnable applyRoundedCorners = () -> { try { window.setBackground(new Color(0, 0, 0, 0)); @@ -73,12 +87,10 @@ public void show() { } catch (UnsupportedOperationException ignored) {} }; - // Main panel with title JPanel mainPanel = new JPanel(new BorderLayout()); mainPanel.setBorder(new EmptyBorder(14, 14, 14, 14)); montoyaApi.userInterface().applyThemeToComponent(mainPanel); - // Add title panel JPanel titlePanel = new JPanel(new BorderLayout()); JLabel titleLabel = new JLabel("Multi Encoder"); titleLabel.setFont(new Font("Inter", Font.BOLD, 16)); @@ -87,52 +99,30 @@ public void show() { montoyaApi.userInterface().applyThemeToComponent(titlePanel); montoyaApi.userInterface().applyThemeToComponent(titleLabel); - // Create search and tags panel JPanel topPanel = new JPanel(new BorderLayout()); topPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0)); montoyaApi.userInterface().applyThemeToComponent(topPanel); - // Search field - JTextField searchField = new JTextField(); - searchField.setFont(new Font("Monospaced", Font.PLAIN, 14)); - searchField.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); - montoyaApi.userInterface().applyThemeToComponent(searchField); - - JLabel searchLabel = new JLabel("Search tags: "); - searchLabel.setFont(new Font("Inter", Font.PLAIN, 13)); - montoyaApi.userInterface().applyThemeToComponent(searchLabel); - - JPanel searchPanel = new JPanel(new BorderLayout()); - searchPanel.add(searchLabel, BorderLayout.WEST); - searchPanel.add(searchField, BorderLayout.CENTER); - montoyaApi.userInterface().applyThemeToComponent(searchPanel); + layerTabbedPane = new JTabbedPane(); + layerTabbedPane.setPreferredSize(new Dimension(DEFAULT_WIDTH - 50, 220)); + montoyaApi.userInterface().applyThemeToComponent(layerTabbedPane); - JCheckBox selectAllCheckbox = new JCheckBox("Select all"); - selectAllCheckbox.setFont(new Font("Inter", Font.PLAIN, 12)); - selectAllCheckbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); - selectAllCheckbox.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 0)); - montoyaApi.userInterface().applyThemeToComponent(selectAllCheckbox); + JPanel layerButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton addLayerButton = new JButton("+ Add Layer"); + addLayerButton.addActionListener(e -> addLayer()); + montoyaApi.userInterface().applyThemeToComponent(addLayerButton); - JPanel searchAndSelectAllPanel = new JPanel(new BorderLayout()); - searchAndSelectAllPanel.add(searchPanel, BorderLayout.CENTER); - searchAndSelectAllPanel.add(selectAllCheckbox, BorderLayout.SOUTH); - montoyaApi.userInterface().applyThemeToComponent(searchAndSelectAllPanel); + JButton removeLayerButton = new JButton("- Remove Layer"); + removeLayerButton.addActionListener(e -> removeCurrentLayer()); + montoyaApi.userInterface().applyThemeToComponent(removeLayerButton); - topPanel.add(searchAndSelectAllPanel, BorderLayout.NORTH); + layerButtonPanel.add(addLayerButton); + layerButtonPanel.add(removeLayerButton); + montoyaApi.userInterface().applyThemeToComponent(layerButtonPanel); - // Tags panel with checkboxes - JPanel tagsPanel = new JPanel(new GridBagLayout()); - montoyaApi.userInterface().applyThemeToComponent(tagsPanel); + topPanel.add(layerButtonPanel, BorderLayout.NORTH); + topPanel.add(layerTabbedPane, BorderLayout.CENTER); - JScrollPane tagsScrollPane = new JScrollPane(tagsPanel); - tagsScrollPane.setPreferredSize(new Dimension(DEFAULT_WIDTH - 50, 200)); - tagsScrollPane.setBorder(BorderFactory.createTitledBorder("Select Tags")); - tagsScrollPane.getVerticalScrollBar().setUnitIncrement(16); - montoyaApi.userInterface().applyThemeToComponent(tagsScrollPane); - - topPanel.add(tagsScrollPane, BorderLayout.CENTER); - - // Preview section JPanel previewPanel = new JPanel(new BorderLayout()); previewPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); @@ -142,17 +132,15 @@ public void show() { montoyaApi.userInterface().applyThemeToComponent(previewArea); JScrollPane previewScrollPane = new JScrollPane(previewArea); - previewScrollPane.setPreferredSize(new Dimension(DEFAULT_WIDTH - 50, 200)); + previewScrollPane.setPreferredSize(new Dimension(DEFAULT_WIDTH - 50, 180)); previewScrollPane.setBorder(BorderFactory.createTitledBorder("Preview")); montoyaApi.userInterface().applyThemeToComponent(previewScrollPane); previewPanel.add(previewScrollPane, BorderLayout.CENTER); - // Button panel JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); montoyaApi.userInterface().applyThemeToComponent(buttonPanel); - // Add mode selection combo box JLabel modeLabel = new JLabel("Mode:"); modeLabel.setFont(new Font("Inter", Font.PLAIN, 12)); montoyaApi.userInterface().applyThemeToComponent(modeLabel); @@ -181,43 +169,19 @@ public void show() { JButton clearButton = new JButton("Clear"); clearButton.addActionListener(e -> { - for (JCheckBox checkbox : tagCheckboxes.values()) { - checkbox.setSelected(false); + for (Layer layer : layers) { + for (JCheckBox checkbox : layer.tagCheckboxes.values()) { + checkbox.setSelected(false); + } + layer.selectedTags.clear(); + layer.selectAllCheckbox.setSelected(false); } - selectedTags.clear(); - selectAllCheckbox.setSelected(false); previewArea.setText(""); }); montoyaApi.userInterface().applyThemeToComponent(clearButton); JButton copyButton = new JButton("Copy"); - copyButton.addActionListener(e -> { - if (selectedTags.isEmpty()) { - return; - } - StringBuilder output = new StringBuilder(); - boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - for (Tag tag : selectedTags) { - String[] tagStartEnd = Convertors.generateTagStartEnd(tag); - String tagStart = tagStartEnd[0]; - String tagEnd = tagStartEnd[1]; - String taggedText = tagStart + selectedText + tagEnd; - String result; - if (shouldConvert) { - try { - result = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); - } catch (Exception ex) { - result = "Error: " + ex.getMessage(); - } - } else { - result = taggedText; - } - output.append(result).append("\n"); - } - StringSelection selection = new StringSelection(output.toString().trim()); - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - clipboard.setContents(selection, null); - }); + copyButton.addActionListener(e -> copyToClipboard()); montoyaApi.userInterface().applyThemeToComponent(copyButton); buttonPanel.add(clearButton); @@ -233,170 +197,295 @@ public void show() { buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); buttonPanel.add(cancelButton); - // Helper: Filter tags - Function> filterTags = searchText -> { - ArrayList filtered = new ArrayList<>(); - String lowerSearch = searchText.toLowerCase(); - for (Tag tag : tags) { - if (lowerSearch.isEmpty() || - tag.name.toLowerCase().contains(lowerSearch) || - tag.category.toString().toLowerCase().contains(lowerSearch) || - (tag.tooltip != null && tag.tooltip.toLowerCase().contains(lowerSearch))) { - filtered.add(tag); - } + window.addWindowFocusListener(new java.awt.event.WindowAdapter() { + @Override + public void windowLostFocus(java.awt.event.WindowEvent e) { + window.dispose(); } - filtered.sort((a, b) -> a.name.compareToIgnoreCase(b.name)); - return filtered; - }; + }); - // Helper: Create tag checkbox - Function createTagCheckbox = tag -> { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - JCheckBox checkbox = new JCheckBox(tag.name); - checkbox.setToolTipText(tag.tooltip != null ? tag.tooltip : tag.category.toString()); - checkbox.setFont(new Font("Inter", Font.PLAIN, 12)); - checkbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); + JPanel contentPanel = new JPanel(new BorderLayout()); + contentPanel.add(titlePanel, BorderLayout.NORTH); + contentPanel.add(topPanel, BorderLayout.CENTER); + montoyaApi.userInterface().applyThemeToComponent(contentPanel); - montoyaApi.userInterface().applyThemeToComponent(checkbox); - montoyaApi.userInterface().applyThemeToComponent(panel); + mainPanel.add(contentPanel, BorderLayout.NORTH); + mainPanel.add(previewPanel, BorderLayout.CENTER); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); - tagCheckboxes.put(tag.name, checkbox); + applyRoundedCorners.run(); + window.add(mainPanel); - checkbox.addActionListener(e -> { - if (checkbox.isSelected()) { - if (!selectedTags.contains(tag)) { - selectedTags.add(tag); - } - } else { - selectedTags.remove(tag); - } - updatePreview(); - }); + montoyaApi.userInterface().applyThemeToComponent(window.getContentPane()); + window.setLocationRelativeTo(montoyaApi.userInterface().swingUtils().suiteFrame()); - panel.add(checkbox); - return panel; - }; + addLayer(); - // Update tags display - Runnable updateTags = () -> { - tagsPanel.removeAll(); - tagCheckboxes.clear(); - - ArrayList filteredTags = filterTags.apply(searchField.getText()); - - if (!filteredTags.isEmpty()) { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(3, 3, 3, 3); - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.gridy = 0; - - int currentColumn = 0; - for (Tag tag : filteredTags) { - JPanel tagPanel = createTagCheckbox.apply(tag); - gbc.gridx = currentColumn; - gbc.weightx = 1.0 / COLUMN_COUNT; - tagsPanel.add(tagPanel, gbc); - - if (++currentColumn >= COLUMN_COUNT) { - currentColumn = 0; - gbc.gridy++; - } + window.setVisible(true); + if (!layers.isEmpty()) { + layers.get(0).searchField.requestFocusInWindow(); + } + }); + } + + private void addLayer() { + JPanel layerPanel = new JPanel(new BorderLayout()); + layerPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + montoyaApi.userInterface().applyThemeToComponent(layerPanel); + + JTextField searchField = new JTextField(); + searchField.setFont(new Font("Monospaced", Font.PLAIN, 14)); + searchField.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); + montoyaApi.userInterface().applyThemeToComponent(searchField); + + JLabel searchLabel = new JLabel("Search tags: "); + searchLabel.setFont(new Font("Inter", Font.PLAIN, 13)); + montoyaApi.userInterface().applyThemeToComponent(searchLabel); + + JPanel searchPanel = new JPanel(new BorderLayout()); + searchPanel.add(searchLabel, BorderLayout.WEST); + searchPanel.add(searchField, BorderLayout.CENTER); + montoyaApi.userInterface().applyThemeToComponent(searchPanel); + + JCheckBox selectAllCheckbox = new JCheckBox("Select all"); + selectAllCheckbox.setFont(new Font("Inter", Font.PLAIN, 12)); + selectAllCheckbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); + selectAllCheckbox.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 0)); + montoyaApi.userInterface().applyThemeToComponent(selectAllCheckbox); + + JPanel searchAndSelectAllPanel = new JPanel(new BorderLayout()); + searchAndSelectAllPanel.add(searchPanel, BorderLayout.CENTER); + searchAndSelectAllPanel.add(selectAllCheckbox, BorderLayout.SOUTH); + montoyaApi.userInterface().applyThemeToComponent(searchAndSelectAllPanel); + + JPanel tagsPanel = new JPanel(new GridBagLayout()); + montoyaApi.userInterface().applyThemeToComponent(tagsPanel); + + JScrollPane tagsScrollPane = new JScrollPane(tagsPanel); + tagsScrollPane.setBorder(BorderFactory.createTitledBorder("Select Tags")); + tagsScrollPane.getVerticalScrollBar().setUnitIncrement(16); + montoyaApi.userInterface().applyThemeToComponent(tagsScrollPane); + + layerPanel.add(searchAndSelectAllPanel, BorderLayout.NORTH); + layerPanel.add(tagsScrollPane, BorderLayout.CENTER); + + Layer layer = new Layer(tagsPanel, searchField, selectAllCheckbox, null); + + Function> filterTags = searchText -> { + ArrayList filtered = new ArrayList<>(); + String lowerSearch = searchText.toLowerCase(); + for (Tag tag : tags) { + if (lowerSearch.isEmpty() || + tag.name.toLowerCase().contains(lowerSearch) || + tag.category.toString().toLowerCase().contains(lowerSearch) || + (tag.tooltip != null && tag.tooltip.toLowerCase().contains(lowerSearch))) { + filtered.add(tag); + } + } + filtered.sort((a, b) -> a.name.compareToIgnoreCase(b.name)); + return filtered; + }; + + Function createTagCheckbox = tag -> { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); + JCheckBox checkbox = new JCheckBox(tag.name); + checkbox.setToolTipText(tag.tooltip != null ? tag.tooltip : tag.category.toString()); + checkbox.setFont(new Font("Inter", Font.PLAIN, 12)); + checkbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + montoyaApi.userInterface().applyThemeToComponent(checkbox); + montoyaApi.userInterface().applyThemeToComponent(panel); + + layer.tagCheckboxes.put(tag.name, checkbox); + + checkbox.addActionListener(e -> { + if (checkbox.isSelected()) { + if (!layer.selectedTags.contains(tag)) { + layer.selectedTags.add(tag); } } else { - JLabel noResultsLabel = new JLabel("No tags found matching: " + searchField.getText()); - noResultsLabel.setFont(new Font("Inter", Font.PLAIN, 13)); - montoyaApi.userInterface().applyThemeToComponent(noResultsLabel); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridwidth = COLUMN_COUNT; - tagsPanel.add(noResultsLabel, gbc); + layer.selectedTags.remove(tag); } + updatePreview(); + }); - tagsPanel.revalidate(); - tagsPanel.repaint(); - }; + panel.add(checkbox); + return panel; + }; + + Runnable updateTags = () -> { + tagsPanel.removeAll(); + layer.tagCheckboxes.clear(); - selectAllCheckbox.addActionListener(e -> { - ArrayList filteredTags = filterTags.apply(searchField.getText()); - boolean selectAll = selectAllCheckbox.isSelected(); + ArrayList filteredTags = filterTags.apply(searchField.getText()); + + if (!filteredTags.isEmpty()) { + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(3, 3, 3, 3); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridy = 0; + + int currentColumn = 0; for (Tag tag : filteredTags) { - JCheckBox checkbox = tagCheckboxes.get(tag.name); - if (checkbox != null && checkbox.isSelected() != selectAll) { - checkbox.setSelected(selectAll); - if (selectAll) { - if (!selectedTags.contains(tag)) { - selectedTags.add(tag); - } - } else { - selectedTags.remove(tag); - } + JPanel tagPanel = createTagCheckbox.apply(tag); + gbc.gridx = currentColumn; + gbc.weightx = 1.0 / COLUMN_COUNT; + tagsPanel.add(tagPanel, gbc); + + if (++currentColumn >= COLUMN_COUNT) { + currentColumn = 0; + gbc.gridy++; } } - updatePreview(); - }); + } else { + JLabel noResultsLabel = new JLabel("No tags found matching: " + searchField.getText()); + noResultsLabel.setFont(new Font("Inter", Font.PLAIN, 13)); + montoyaApi.userInterface().applyThemeToComponent(noResultsLabel); - // Event listeners - searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - public void insertUpdate(javax.swing.event.DocumentEvent e) { - selectAllCheckbox.setSelected(false); - updateTags.run(); - } - public void removeUpdate(javax.swing.event.DocumentEvent e) { - selectAllCheckbox.setSelected(false); - updateTags.run(); - } - public void changedUpdate(javax.swing.event.DocumentEvent e) { - selectAllCheckbox.setSelected(false); - updateTags.run(); - } - }); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = COLUMN_COUNT; + tagsPanel.add(noResultsLabel, gbc); + } - searchField.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { - window.dispose(); + tagsPanel.revalidate(); + tagsPanel.repaint(); + }; + + selectAllCheckbox.addActionListener(e -> { + ArrayList filteredTags = filterTags.apply(searchField.getText()); + boolean selectAll = selectAllCheckbox.isSelected(); + for (Tag tag : filteredTags) { + JCheckBox checkbox = layer.tagCheckboxes.get(tag.name); + if (checkbox != null && checkbox.isSelected() != selectAll) { + checkbox.setSelected(selectAll); + if (selectAll) { + if (!layer.selectedTags.contains(tag)) { + layer.selectedTags.add(tag); + } + } else { + layer.selectedTags.remove(tag); } } - }); + } + updatePreview(); + }); - // Add window focus listener - dispose when focus is lost - window.addWindowFocusListener(new java.awt.event.WindowAdapter() { - @Override - public void windowLostFocus(java.awt.event.WindowEvent e) { + searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { + public void insertUpdate(javax.swing.event.DocumentEvent e) { + selectAllCheckbox.setSelected(false); + updateTags.run(); + } + public void removeUpdate(javax.swing.event.DocumentEvent e) { + selectAllCheckbox.setSelected(false); + updateTags.run(); + } + public void changedUpdate(javax.swing.event.DocumentEvent e) { + selectAllCheckbox.setSelected(false); + updateTags.run(); + } + }); + + searchField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { window.dispose(); } - }); + } + }); - // Assemble UI - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.add(titlePanel, BorderLayout.NORTH); - contentPanel.add(topPanel, BorderLayout.CENTER); - montoyaApi.userInterface().applyThemeToComponent(contentPanel); + layers.add(layer); + String tabTitle = "Layer " + layerCounter++; + layerTabbedPane.addTab(tabTitle, layerPanel); + layerTabbedPane.setSelectedIndex(layerTabbedPane.getTabCount() - 1); - mainPanel.add(contentPanel, BorderLayout.NORTH); - mainPanel.add(previewPanel, BorderLayout.CENTER); - mainPanel.add(buttonPanel, BorderLayout.SOUTH); + updateTags.run(); + } - // Initialize window - applyRoundedCorners.run(); - window.add(mainPanel); + private void removeCurrentLayer() { + if (layers.size() <= 1) { + return; + } + int selectedIndex = layerTabbedPane.getSelectedIndex(); + if (selectedIndex >= 0 && selectedIndex < layers.size()) { + layers.remove(selectedIndex); + layerTabbedPane.removeTabAt(selectedIndex); + updatePreview(); + } + } - // Apply theme to the window's content pane if available - montoyaApi.userInterface().applyThemeToComponent(window.getContentPane()); - window.setLocationRelativeTo(montoyaApi.userInterface().swingUtils().suiteFrame()); + private ArrayList> getAllLayerTags() { + ArrayList> allLayerTags = new ArrayList<>(); + for (Layer layer : layers) { + if (!layer.selectedTags.isEmpty()) { + allLayerTags.add(new ArrayList<>(layer.selectedTags)); + } + } + return allLayerTags; + } - // Initialize content and show - updateTags.run(); - window.setVisible(true); - searchField.requestFocusInWindow(); - }); + private String applyLayeredTags(String input, boolean shouldConvert) { + ArrayList> allLayerTags = getAllLayerTags(); + if (allLayerTags.isEmpty()) { + return input; + } + + StringBuilder tagStart = new StringBuilder(); + StringBuilder tagEnd = new StringBuilder(); + + for (int i = allLayerTags.size() - 1; i >= 0; i--) { + ArrayList layerTags = allLayerTags.get(i); + for (Tag tag : layerTags) { + String[] tagStartEnd = Convertors.generateTagStartEnd(tag); + tagStart.append(tagStartEnd[0]); + tagEnd.insert(0, tagStartEnd[1]); + } + } + + String taggedText = tagStart.toString() + input + tagEnd; + + if (shouldConvert) { + try { + return HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); + } catch (Exception ex) { + return "Error: " + ex.getMessage(); + } + } + return taggedText; + } + + private String getLayersSummary() { + StringBuilder summary = new StringBuilder(); + ArrayList> allLayerTags = getAllLayerTags(); + for (int i = 0; i < allLayerTags.size(); i++) { + ArrayList layerTags = allLayerTags.get(i); + summary.append("Layer ").append(i + 1).append(": "); + for (int j = 0; j < layerTags.size(); j++) { + if (j > 0) summary.append(", "); + summary.append(layerTags.get(j).name); + } + summary.append("\n"); + } + return summary.toString(); + } + + private void copyToClipboard() { + ArrayList> allLayerTags = getAllLayerTags(); + if (allLayerTags.isEmpty()) { + return; + } + boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); + String result = applyLayeredTags(selectedText, shouldConvert); + StringSelection selection = new StringSelection(result); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(selection, null); } private void updatePreview() { - if (selectedTags.isEmpty()) { - previewArea.setText("No tags selected. Please select at least one tag."); + ArrayList> allLayerTags = getAllLayerTags(); + if (allLayerTags.isEmpty()) { + previewArea.setText("No tags selected. Please select at least one tag in any layer."); return; } @@ -404,36 +493,18 @@ private void updatePreview() { preview.append("Selected text: ").append(selectedText).append("\n"); preview.append("Mode: ").append(modeComboBox.getSelectedItem()).append("\n"); preview.append("=====================================\n\n"); + preview.append(getLayersSummary()); + preview.append("=====================================\n\n"); boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - - for (Tag tag : selectedTags) { - String[] tagStartEnd = Convertors.generateTagStartEnd(tag); - String tagStart = tagStartEnd[0]; - String tagEnd = tagStartEnd[1]; - String taggedText = tagStart + selectedText + tagEnd; - - String result; - if (shouldConvert) { - // Process through Hackvertor to get actual encoded result - try { - result = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); - } catch (Exception ex) { - result = "Error: " + ex.getMessage(); - } - } else { - // Just add tags without converting - result = taggedText; - } - - preview.append("Tag: ").append(tag.name).append("\n"); - if (!shouldConvert) { - preview.append("Tagged: ").append(result).append("\n"); - } else { - preview.append("Input: ").append(taggedText).append("\n"); - preview.append("Result: ").append(result).append("\n"); - } - preview.append("-------------------------------------\n"); + String taggedText = applyLayeredTags(selectedText, false); + String result = applyLayeredTags(selectedText, shouldConvert); + + if (!shouldConvert) { + preview.append("Tagged: ").append(result).append("\n"); + } else { + preview.append("Input: ").append(taggedText).append("\n"); + preview.append("Result: ").append(result).append("\n"); } previewArea.setText(preview.toString()); @@ -441,7 +512,8 @@ private void updatePreview() { } private void sendToRepeater() { - if (selectedTags.isEmpty()) { + ArrayList> allLayerTags = getAllLayerTags(); + if (allLayerTags.isEmpty()) { JOptionPane.showMessageDialog(window, "Please select at least one tag before sending to Repeater.", "No Tags Selected", @@ -461,37 +533,16 @@ private void sendToRepeater() { String requestStr = baseRequest.toString(); boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - for (Tag tag : selectedTags) { - String[] tagStartEnd = Convertors.generateTagStartEnd(tag); - String tagStart = tagStartEnd[0]; - String tagEnd = tagStartEnd[1]; - String taggedText = tagStart + selectedText + tagEnd; - - String replacementText; - if (shouldConvert) { - // Process through Hackvertor to get actual encoded result - try { - replacementText = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); - } catch (Exception ex) { - replacementText = selectedText; // Fallback to original if error - } - } else { - // Just add tags without converting - replacementText = taggedText; - } - - // Replace the selected text with the result - String modifiedRequestStr = requestStr.replace(selectedText, replacementText); - HttpRequest modifiedRequest = HttpRequest.httpRequest(modifiedRequestStr); + String replacementText = applyLayeredTags(selectedText, shouldConvert); + String modifiedRequestStr = requestStr.replace(selectedText, replacementText); + HttpRequest modifiedRequest = HttpRequest.httpRequest(modifiedRequestStr); - // Send to Repeater with a descriptive tab name - String modePrefix = shouldConvert ? "HV-" : "HVT-"; - String tabName = modePrefix + tag.name + "-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); - montoyaApi.repeater().sendToRepeater(modifiedRequest, tabName); - } + String modePrefix = shouldConvert ? "HV-" : "HVT-"; + String tabName = modePrefix + "Layers-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); + montoyaApi.repeater().sendToRepeater(modifiedRequest, tabName); JOptionPane.showMessageDialog(window, - "Sent " + selectedTags.size() + " requests to Repeater.", + "Sent request to Repeater.", "Success", JOptionPane.INFORMATION_MESSAGE); @@ -499,7 +550,8 @@ private void sendToRepeater() { } private void sendToIntruder() { - if (selectedTags.isEmpty()) { + ArrayList> allLayerTags = getAllLayerTags(); + if (allLayerTags.isEmpty()) { JOptionPane.showMessageDialog(window, "Please select at least one tag before sending to Intruder.", "No Tags Selected", @@ -518,7 +570,6 @@ private void sendToIntruder() { HttpRequest baseRequest = baseRequestResponse.request(); String requestStr = baseRequest.toString(); - // Find the position of the selected text in the request int startPos = requestStr.indexOf(selectedText); int endPos = startPos + selectedText.length(); @@ -532,40 +583,20 @@ private void sendToIntruder() { Range insertionPoint = Range.range(startPos, endPos); HttpRequestTemplate intruderTemplate = HttpRequestTemplate.httpRequestTemplate(baseRequest, Collections.singletonList(insertionPoint)); - String tabName = "HV-Multi-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); + String tabName = "HV-Layers-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); montoyaApi.intruder().sendToIntruder(baseRequestResponse.request().httpService(), intruderTemplate, tabName); - // Generate payload list for Intruder StringBuilder payloads = new StringBuilder(); - payloads.append("Hackvertor Multi-Encoder Payloads:\n"); + payloads.append("Hackvertor Multi-Encoder Layered Payloads:\n"); boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); payloads.append("Mode: ").append(modeComboBox.getSelectedItem()).append("\n"); - payloads.append("Copy these payloads to use in Intruder:\n\n"); - - for (Tag tag : selectedTags) { - String[] tagStartEnd = Convertors.generateTagStartEnd(tag); - String tagStart = tagStartEnd[0]; - String tagEnd = tagStartEnd[1]; - String taggedText = tagStart + selectedText + tagEnd; + payloads.append(getLayersSummary()); + payloads.append("Copy this payload to use in Intruder:\n\n"); - String payloadResult; - if (shouldConvert) { - // Process through Hackvertor to get actual encoded result - try { - payloadResult = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); - } catch (Exception ex) { - payloadResult = selectedText; // Fallback to original if error - } - } else { - // Just add tags without converting - payloadResult = taggedText; - } - - payloads.append(payloadResult).append("\n"); - } + String payloadResult = applyLayeredTags(selectedText, shouldConvert); + payloads.append(payloadResult).append("\n"); - // Show payloads in a window JTextArea payloadArea = new JTextArea(payloads.toString()); payloadArea.setEditable(false); payloadArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); @@ -579,4 +610,4 @@ private void sendToIntruder() { window.dispose(); } -} \ No newline at end of file +} From cf78864f193b7e2f069ffd08518eb8860a8465c9 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 20:51:52 +0000 Subject: [PATCH 07/24] Added MultiEncoderWindow to the HackvertorExtension panel and added sendToHackvertor button --- .../java/burp/hv/HackvertorExtension.java | 2 +- src/main/java/burp/hv/ui/HackvertorPanel.java | 31 +++++++++ .../java/burp/hv/ui/MultiEncoderWindow.java | 63 ++++++++++++++++--- 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 4ab9adb..0e2cd51 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.21"; + public static String version = "v2.2.22"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/HackvertorPanel.java b/src/main/java/burp/hv/ui/HackvertorPanel.java index dea6ac4..252d04b 100644 --- a/src/main/java/burp/hv/ui/HackvertorPanel.java +++ b/src/main/java/burp/hv/ui/HackvertorPanel.java @@ -446,6 +446,37 @@ public void actionPerformed(ActionEvent evt) { finderWindow.show(); } }); + + inputArea.getInputMap().put(KeyStroke.getKeyStroke("control alt M"), "multiEncoder"); + inputArea.getActionMap().put("multiEncoder", new AbstractAction("multiEncoder") { + public void actionPerformed(ActionEvent evt) { + String selectedText = inputArea.getSelectedText(); + boolean hasSelection = selectedText != null && !selectedText.isEmpty(); + if (!hasSelection) { + selectedText = inputArea.getText(); + } + if (selectedText == null || selectedText.isEmpty()) { + return; + } + ArrayList tags = hackvertor.getTags(); + String textToEncode = selectedText; + boolean replaceSelection = hasSelection; + MultiEncoderWindow encoderWindow = new MultiEncoderWindow( + HackvertorExtension.montoyaApi, + textToEncode, + tags, + result -> { + if (replaceSelection) { + inputArea.replaceSelection(result); + } else { + inputArea.setText(result); + } + } + ); + encoderWindow.show(); + } + }); + decode.addActionListener(smartDecodeAction); JButton rehydrateTagExecutionKey = new JButton("Rehydrate Tags"); rehydrateTagExecutionKey.setToolTipText("Replace tag execution keys in selected text with your current key"); diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index be1f276..24fb083 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; public class MultiEncoderWindow { @@ -36,6 +37,7 @@ public class MultiEncoderWindow { private final MessageEditorHttpRequestResponse messageEditor; private final HttpRequestResponse baseRequestResponse; private final ArrayList layers; + private final Consumer hackvertorCallback; private JTextArea previewArea; private JWindow window; private JComboBox modeComboBox; @@ -62,11 +64,23 @@ private class Layer { public MultiEncoderWindow(MontoyaApi montoyaApi, String selectedText, ArrayList tags, MessageEditorHttpRequestResponse messageEditor, HttpRequestResponse baseRequestResponse) { + this(montoyaApi, selectedText, tags, messageEditor, baseRequestResponse, null); + } + + public MultiEncoderWindow(MontoyaApi montoyaApi, String selectedText, ArrayList tags, + Consumer hackvertorCallback) { + this(montoyaApi, selectedText, tags, null, null, hackvertorCallback); + } + + private MultiEncoderWindow(MontoyaApi montoyaApi, String selectedText, ArrayList tags, + MessageEditorHttpRequestResponse messageEditor, HttpRequestResponse baseRequestResponse, + Consumer hackvertorCallback) { this.montoyaApi = montoyaApi; this.selectedText = selectedText; this.tags = tags; this.messageEditor = messageEditor; this.baseRequestResponse = baseRequestResponse; + this.hackvertorCallback = hackvertorCallback; this.layers = new ArrayList<>(); } @@ -155,14 +169,6 @@ public void show() { previewButton.addActionListener(e -> updatePreview()); montoyaApi.userInterface().applyThemeToComponent(previewButton); - JButton sendToRepeaterButton = new JButton("Send to Repeater"); - sendToRepeaterButton.addActionListener(e -> sendToRepeater()); - montoyaApi.userInterface().applyThemeToComponent(sendToRepeaterButton); - - JButton sendToIntruderButton = new JButton("Send to Intruder"); - sendToIntruderButton.addActionListener(e -> sendToIntruder()); - montoyaApi.userInterface().applyThemeToComponent(sendToIntruderButton); - JButton cancelButton = new JButton("Cancel"); cancelButton.addActionListener(e -> window.dispose()); montoyaApi.userInterface().applyThemeToComponent(cancelButton); @@ -192,8 +198,25 @@ public void show() { buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); buttonPanel.add(previewButton); buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); - buttonPanel.add(sendToRepeaterButton); - buttonPanel.add(sendToIntruderButton); + + if (hackvertorCallback != null) { + JButton sendToHackvertorButton = new JButton("Send to Hackvertor"); + sendToHackvertorButton.addActionListener(e -> sendToHackvertor()); + montoyaApi.userInterface().applyThemeToComponent(sendToHackvertorButton); + buttonPanel.add(sendToHackvertorButton); + } else { + JButton sendToRepeaterButton = new JButton("Send to Repeater"); + sendToRepeaterButton.addActionListener(e -> sendToRepeater()); + montoyaApi.userInterface().applyThemeToComponent(sendToRepeaterButton); + + JButton sendToIntruderButton = new JButton("Send to Intruder"); + sendToIntruderButton.addActionListener(e -> sendToIntruder()); + montoyaApi.userInterface().applyThemeToComponent(sendToIntruderButton); + + buttonPanel.add(sendToRepeaterButton); + buttonPanel.add(sendToIntruderButton); + } + buttonPanel.add(new JSeparator(SwingConstants.VERTICAL)); buttonPanel.add(cancelButton); @@ -511,6 +534,26 @@ private void updatePreview() { previewArea.setCaretPosition(0); } + private void sendToHackvertor() { + ArrayList> allLayerTags = getAllLayerTags(); + if (allLayerTags.isEmpty()) { + JOptionPane.showMessageDialog(window, + "Please select at least one tag before sending to Hackvertor.", + "No Tags Selected", + JOptionPane.WARNING_MESSAGE); + return; + } + + boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); + String result = applyLayeredTags(selectedText, shouldConvert); + + if (hackvertorCallback != null) { + hackvertorCallback.accept(result); + } + + window.dispose(); + } + private void sendToRepeater() { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { From b975061d2185ebed809f394de53b245629a3c28a Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 21:07:04 +0000 Subject: [PATCH 08/24] Fixed the layers to work correctly. The layers now apply the nesting --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 117 +++++++++++------- 2 files changed, 74 insertions(+), 45 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 0e2cd51..c32fe3c 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.22"; + public static String version = "v2.2.23"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index 24fb083..0dadb4d 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -448,34 +448,40 @@ private ArrayList> getAllLayerTags() { return allLayerTags; } - private String applyLayeredTags(String input, boolean shouldConvert) { + private ArrayList generateAllVariants(String input, boolean shouldConvert) { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { - return input; + ArrayList result = new ArrayList<>(); + result.add(input); + return result; } - StringBuilder tagStart = new StringBuilder(); - StringBuilder tagEnd = new StringBuilder(); - - for (int i = allLayerTags.size() - 1; i >= 0; i--) { - ArrayList layerTags = allLayerTags.get(i); - for (Tag tag : layerTags) { - String[] tagStartEnd = Convertors.generateTagStartEnd(tag); - tagStart.append(tagStartEnd[0]); - tagEnd.insert(0, tagStartEnd[1]); + ArrayList currentVariants = new ArrayList<>(); + currentVariants.add(input); + + for (ArrayList layerTags : allLayerTags) { + ArrayList newVariants = new ArrayList<>(); + for (String variant : currentVariants) { + for (Tag tag : layerTags) { + String[] tagStartEnd = Convertors.generateTagStartEnd(tag); + String taggedText = tagStartEnd[0] + variant + tagStartEnd[1]; + + if (shouldConvert) { + try { + String converted = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); + newVariants.add(converted); + } catch (Exception ex) { + newVariants.add("Error: " + ex.getMessage()); + } + } else { + newVariants.add(taggedText); + } + } } + currentVariants = newVariants; } - String taggedText = tagStart.toString() + input + tagEnd; - - if (shouldConvert) { - try { - return HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); - } catch (Exception ex) { - return "Error: " + ex.getMessage(); - } - } - return taggedText; + return currentVariants; } private String getLayersSummary() { @@ -499,8 +505,13 @@ private void copyToClipboard() { return; } boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - String result = applyLayeredTags(selectedText, shouldConvert); - StringSelection selection = new StringSelection(result); + ArrayList variants = generateAllVariants(selectedText, shouldConvert); + StringBuilder output = new StringBuilder(); + for (int i = 0; i < variants.size(); i++) { + if (i > 0) output.append("\n"); + output.append(variants.get(i)); + } + StringSelection selection = new StringSelection(output.toString()); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(selection, null); } @@ -517,17 +528,23 @@ private void updatePreview() { preview.append("Mode: ").append(modeComboBox.getSelectedItem()).append("\n"); preview.append("=====================================\n\n"); preview.append(getLayersSummary()); - preview.append("=====================================\n\n"); boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - String taggedText = applyLayeredTags(selectedText, false); - String result = applyLayeredTags(selectedText, shouldConvert); - - if (!shouldConvert) { - preview.append("Tagged: ").append(result).append("\n"); - } else { - preview.append("Input: ").append(taggedText).append("\n"); - preview.append("Result: ").append(result).append("\n"); + ArrayList taggedVariants = generateAllVariants(selectedText, false); + ArrayList resultVariants = generateAllVariants(selectedText, shouldConvert); + + preview.append("Total variants: ").append(resultVariants.size()).append("\n"); + preview.append("=====================================\n\n"); + + for (int i = 0; i < resultVariants.size(); i++) { + preview.append("Variant ").append(i + 1).append(":\n"); + if (!shouldConvert) { + preview.append("Tagged: ").append(resultVariants.get(i)).append("\n"); + } else { + preview.append("Input: ").append(taggedVariants.get(i)).append("\n"); + preview.append("Result: ").append(resultVariants.get(i)).append("\n"); + } + preview.append("-------------------------------------\n"); } previewArea.setText(preview.toString()); @@ -545,10 +562,15 @@ private void sendToHackvertor() { } boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - String result = applyLayeredTags(selectedText, shouldConvert); + ArrayList variants = generateAllVariants(selectedText, shouldConvert); if (hackvertorCallback != null) { - hackvertorCallback.accept(result); + StringBuilder output = new StringBuilder(); + for (int i = 0; i < variants.size(); i++) { + if (i > 0) output.append("\n"); + output.append(variants.get(i)); + } + hackvertorCallback.accept(output.toString()); } window.dispose(); @@ -575,17 +597,20 @@ private void sendToRepeater() { HttpRequest baseRequest = baseRequestResponse.request(); String requestStr = baseRequest.toString(); boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - - String replacementText = applyLayeredTags(selectedText, shouldConvert); - String modifiedRequestStr = requestStr.replace(selectedText, replacementText); - HttpRequest modifiedRequest = HttpRequest.httpRequest(modifiedRequestStr); + ArrayList variants = generateAllVariants(selectedText, shouldConvert); String modePrefix = shouldConvert ? "HV-" : "HVT-"; - String tabName = modePrefix + "Layers-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); - montoyaApi.repeater().sendToRepeater(modifiedRequest, tabName); + int variantNum = 1; + for (String variant : variants) { + String modifiedRequestStr = requestStr.replace(selectedText, variant); + HttpRequest modifiedRequest = HttpRequest.httpRequest(modifiedRequestStr); + String tabName = modePrefix + "V" + variantNum + "-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); + montoyaApi.repeater().sendToRepeater(modifiedRequest, tabName); + variantNum++; + } JOptionPane.showMessageDialog(window, - "Sent request to Repeater.", + "Sent " + variants.size() + " variant(s) to Repeater.", "Success", JOptionPane.INFORMATION_MESSAGE); @@ -635,10 +660,14 @@ private void sendToIntruder() { boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); payloads.append("Mode: ").append(modeComboBox.getSelectedItem()).append("\n"); payloads.append(getLayersSummary()); - payloads.append("Copy this payload to use in Intruder:\n\n"); - String payloadResult = applyLayeredTags(selectedText, shouldConvert); - payloads.append(payloadResult).append("\n"); + ArrayList variants = generateAllVariants(selectedText, shouldConvert); + payloads.append("Total variants: ").append(variants.size()).append("\n"); + payloads.append("Copy these payloads to use in Intruder:\n\n"); + + for (String variant : variants) { + payloads.append(variant).append("\n"); + } JTextArea payloadArea = new JTextArea(payloads.toString()); payloadArea.setEditable(false); From f5bd46ca5a6fb157c92a8f5b05482a230f67aa26 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 21:15:30 +0000 Subject: [PATCH 09/24] Added limits to MultiEncoderWindow --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index c32fe3c..6ad3647 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.23"; + public static String version = "v2.2.24"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index 0dadb4d..dcc00cb 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.*; import java.util.function.Consumer; import java.util.function.Function; @@ -30,6 +31,8 @@ public class MultiEncoderWindow { private static final int DEFAULT_HEIGHT = 600; private static final int CORNER_RADIUS = 20; private static final int COLUMN_COUNT = 3; + private static final int MAX_VARIANTS_DISPLAY = 100; + private static final int CONVERT_TIMEOUT_SECONDS = 20; private final MontoyaApi montoyaApi; private final String selectedText; @@ -448,6 +451,24 @@ private ArrayList> getAllLayerTags() { return allLayerTags; } + private String convertWithTimeout(String taggedText) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> + HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor) + ); + + try { + return future.get(CONVERT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (TimeoutException e) { + future.cancel(true); + return "Error: Conversion timed out after " + CONVERT_TIMEOUT_SECONDS + " seconds"; + } catch (Exception e) { + return "Error: " + e.getMessage(); + } finally { + executor.shutdownNow(); + } + } + private ArrayList generateAllVariants(String input, boolean shouldConvert) { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { @@ -467,12 +488,8 @@ private ArrayList generateAllVariants(String input, boolean shouldConver String taggedText = tagStartEnd[0] + variant + tagStartEnd[1]; if (shouldConvert) { - try { - String converted = HackvertorExtension.hackvertor.convert(taggedText, HackvertorExtension.hackvertor); - newVariants.add(converted); - } catch (Exception ex) { - newVariants.add("Error: " + ex.getMessage()); - } + String converted = convertWithTimeout(taggedText); + newVariants.add(converted); } else { newVariants.add(taggedText); } @@ -534,9 +551,13 @@ private void updatePreview() { ArrayList resultVariants = generateAllVariants(selectedText, shouldConvert); preview.append("Total variants: ").append(resultVariants.size()).append("\n"); + if (resultVariants.size() > MAX_VARIANTS_DISPLAY) { + preview.append("Showing first ").append(MAX_VARIANTS_DISPLAY).append(" variants\n"); + } preview.append("=====================================\n\n"); - for (int i = 0; i < resultVariants.size(); i++) { + int displayLimit = Math.min(resultVariants.size(), MAX_VARIANTS_DISPLAY); + for (int i = 0; i < displayLimit; i++) { preview.append("Variant ").append(i + 1).append(":\n"); if (!shouldConvert) { preview.append("Tagged: ").append(resultVariants.get(i)).append("\n"); @@ -547,6 +568,11 @@ private void updatePreview() { preview.append("-------------------------------------\n"); } + if (resultVariants.size() > MAX_VARIANTS_DISPLAY) { + preview.append("\n... ").append(resultVariants.size() - MAX_VARIANTS_DISPLAY) + .append(" more variants not shown ...\n"); + } + previewArea.setText(preview.toString()); previewArea.setCaretPosition(0); } From d64ae0559167f46d872b878589123429b2ac4401 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Tue, 25 Nov 2025 21:19:35 +0000 Subject: [PATCH 10/24] Updated changelog --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 4c356eb..a2fb6f5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,18 @@ Tags also support arguments. The find tag allows you to find a string by regex a # Changelog +## Version v2.2.24 (2025-11-25) + +- Updated TagAutomator rules to allow multiple tools per rule +- Added multi encoder window +- Added websockets setting and websocket handler +- Added copy to clipboard button, clear button and select all checkbox to the MultiEncoderWindow. +- Fixed send to intruder +- Added layers to MultiEncoderWindow +- Added MultiEncoderWindow to the HackvertorExtension panel and added sendToHackvertor button +- Fixed the layers to work correctly. The layers now apply the nesting +- Added limits to MultiEncoderWindow + ## Version v2.2.16 (2025-11-20) - Changed HTTP handler to allow interception when there are Tag Automation rules From baeff83335b9fcdefd0732a4e1f61ec1d2031b20 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Wed, 26 Nov 2025 13:10:11 +0000 Subject: [PATCH 11/24] Fixed deflate, fixed auto decoding of deflate and base32. Added converter and ui tests to cover them --- src/main/java/burp/hv/Convertors.java | 186 ++++++++++++------ src/main/java/burp/hv/Hackvertor.java | 7 +- .../java/burp/hv/HackvertorExtension.java | 2 +- src/test/java/burp/ConvertorTests.java | 78 ++++++++ src/test/java/burp/ui/HackvertorUiTest.java | 102 ++++++++++ 5 files changed, 306 insertions(+), 69 deletions(-) diff --git a/src/main/java/burp/hv/Convertors.java b/src/main/java/burp/hv/Convertors.java index dc2ebf6..fe2a5b5 100644 --- a/src/main/java/burp/hv/Convertors.java +++ b/src/main/java/burp/hv/Convertors.java @@ -321,9 +321,9 @@ private static void initializeTagRegistry() { TAG_REGISTRY.put("bzip2_compress", (output, args, vars, custom, hv) -> bzip2_compress(output)); TAG_REGISTRY.put("bzip2_decompress", (output, args, vars, custom, hv) -> bzip2_decompress(output)); TAG_REGISTRY.put("deflate_compress", (output, args, vars, custom, hv) -> - deflate_compress(output, getBoolean(args, 0))); + deflate_compress(output, getString(args, 0))); TAG_REGISTRY.put("deflate_decompress", (output, args, vars, custom, hv) -> - deflate_decompress(output, getBoolean(args, 0))); + deflate_decompress(output)); // SAML operations TAG_REGISTRY.put("saml", (output, args, vars, custom, hv) -> saml(output)); @@ -1219,60 +1219,58 @@ static String bzip2_decompress(String input) { } } - static String deflate_compress(String input, Boolean includeHeader) { - // Use Montoya API if available - if (HackvertorExtension.montoyaApi != null) { - try { - byte[] inputBytes = input.getBytes(); - ByteArray compressed = HackvertorExtension.montoyaApi.utilities().compressionUtils() - .compress(ByteArray.byteArray(inputBytes), CompressionType.DEFLATE); - return helpers.bytesToString(compressed.getBytes()); - } catch (Exception e) { - return "Error compressing DEFLATE: " + e.toString(); - } - } - // Fallback to legacy implementation for tests - ByteArrayOutputStream bos = new ByteArrayOutputStream(input.length()); - CompressorOutputStream cos; - DeflateParameters params = new DeflateParameters(); - params.setWithZlibHeader(includeHeader); - cos = new DeflateCompressorOutputStream(bos, params); + static String deflate_compress(String input, String compressionType) { try { - cos.write(input.getBytes()); - cos.close(); - byte[] compressed = bos.toByteArray(); + byte[] inputBytes = helpers.stringToBytes(input); + java.util.zip.Deflater deflater; + + if ("store".equalsIgnoreCase(compressionType)) { + deflater = new java.util.zip.Deflater(java.util.zip.Deflater.NO_COMPRESSION); + } else if ("fixed".equalsIgnoreCase(compressionType)) { + deflater = new java.util.zip.Deflater(java.util.zip.Deflater.BEST_SPEED); + deflater.setStrategy(java.util.zip.Deflater.HUFFMAN_ONLY); + } else { + deflater = new java.util.zip.Deflater(java.util.zip.Deflater.BEST_COMPRESSION); + } + + deflater.setInput(inputBytes); + deflater.finish(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(inputBytes.length); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + bos.write(buffer, 0, count); + } + deflater.end(); bos.close(); - return helpers.bytesToString(compressed); - } catch (IOException e) { + + return helpers.bytesToString(bos.toByteArray()); + } catch (Exception e) { return "Error:" + e; } } - static String deflate_decompress(String input, Boolean includeHeader) { - // Use Montoya API if available - if (HackvertorExtension.montoyaApi != null) { - try { - byte[] inputBytes = helpers.stringToBytes(input); - ByteArray decompressed = HackvertorExtension.montoyaApi.utilities().compressionUtils() - .decompress(ByteArray.byteArray(inputBytes), CompressionType.DEFLATE); - return new String(decompressed.getBytes()); - } catch (Exception e) { - return "Error decompressing DEFLATE: " + e.toString(); - } - } - // Fallback to legacy implementation for tests - ByteArrayInputStream bis = new ByteArrayInputStream(helpers.stringToBytes(input)); - DeflateCompressorInputStream cis; - byte[] bytes; + static String deflate_decompress(String input) { try { - DeflateParameters params = new DeflateParameters(); - params.setWithZlibHeader(includeHeader); - cis = new DeflateCompressorInputStream(bis, params); - bytes = IOUtils.toByteArray(cis); - cis.close(); - bis.close(); - return new String(bytes); - } catch (IOException e) { + byte[] inputBytes = helpers.stringToBytes(input); + java.util.zip.Inflater inflater = new java.util.zip.Inflater(); + inflater.setInput(inputBytes); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(inputBytes.length); + byte[] buffer = new byte[1024]; + while (!inflater.finished()) { + int count = inflater.inflate(buffer); + if (count == 0 && inflater.needsInput()) { + break; + } + bos.write(buffer, 0, count); + } + inflater.end(); + bos.close(); + + return helpers.bytesToString(bos.toByteArray()); + } catch (Exception e) { return "Error:" + e; } } @@ -1337,14 +1335,61 @@ static String base64urlEncode(String str) { } static String saml(String input) { - return urlencode(base64Encode(deflate_compress(input, false))); + return urlencode(base64Encode(deflate_compress_raw(input))); } + static String d_saml(String input) { String decodedUrl = decode_url(input); if(isBase64(decodedUrl, true)) { - return deflate_decompress(decode_base64(decodedUrl), false); + return deflate_decompress_raw(decode_base64(decodedUrl)); } else { - return deflate_decompress(decode_base64(input), false); + return deflate_decompress_raw(decode_base64(input)); + } + } + + static String deflate_compress_raw(String input) { + try { + byte[] inputBytes = helpers.stringToBytes(input); + java.util.zip.Deflater deflater = new java.util.zip.Deflater(java.util.zip.Deflater.DEFAULT_COMPRESSION, true); + deflater.setInput(inputBytes); + deflater.finish(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(inputBytes.length); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + bos.write(buffer, 0, count); + } + deflater.end(); + bos.close(); + + return helpers.bytesToString(bos.toByteArray()); + } catch (Exception e) { + return "Error:" + e; + } + } + + static String deflate_decompress_raw(String input) { + try { + byte[] inputBytes = helpers.stringToBytes(input); + java.util.zip.Inflater inflater = new java.util.zip.Inflater(true); + inflater.setInput(inputBytes); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(inputBytes.length); + byte[] buffer = new byte[1024]; + while (!inflater.finished()) { + int count = inflater.inflate(buffer); + if (count == 0 && inflater.needsInput()) { + break; + } + bos.write(buffer, 0, count); + } + inflater.end(); + bos.close(); + + return helpers.bytesToString(bos.toByteArray()); + } catch (Exception e) { + return "Error:" + e; } } @@ -3330,13 +3375,21 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { do { String startStr = str; matched = false; - int tagNo = new Random().nextInt(10000); if (Pattern.compile("^\\x1f\\x8b\\x08").matcher(str).find()) { str = gzip_decompress(str); matched = true; encodingOpeningTags.append("<@gzip_compress>"); encodingClosingTags.insert(0, ""); } + if (Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(str).find()) { + test = deflate_decompress(str); + if (!test.startsWith("Error:") && Pattern.compile("^[\\x00-\\x7f]+$").matcher(test).find()) { + str = test; + matched = true; + encodingOpeningTags.append("<@deflate_compress>"); + encodingClosingTags.insert(0, ""); + } + } if (Pattern.compile("[01]{4,}\\s+[01]{4,}").matcher(str).find()) { str = bin2ascii(str); matched = true; @@ -3431,23 +3484,28 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { return d_jwt_get_header(str) + "\n" + d_jwt_get_payload(str) + "\n" + decode_base64url(parts[2]); } } - if (isBase64(str, true)) { - test = decode_base64(str); - if (Pattern.compile("^[\\x00-\\x7f]+$", Pattern.CASE_INSENSITIVE).matcher(test).find()) { + if (Pattern.compile("^[A-Z2-7]+=*$").matcher(str).find() && str.length() % 8 == 0) { + test = decode_base32(str); + boolean isAscii = Pattern.compile("^[\\x00-\\x7f]+$").matcher(test).find(); + boolean isGzip = Pattern.compile("^\\x1f\\x8b\\x08").matcher(test).find(); + boolean isDeflate = Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(test).find(); + if (isAscii || isGzip || isDeflate) { str = test; matched = true; - encodingOpeningTags.append("<@base64>"); - encodingClosingTags.insert(0, ""); + encodingOpeningTags.append("<@base32>"); + encodingClosingTags.insert(0, ""); } } - - if (Pattern.compile("[A-Z0-9+/]{4,}=*$", Pattern.CASE_INSENSITIVE).matcher(str).find() && str.length() % 4 == 0 && !matched) { - test = decode_base32(str); - if (Pattern.compile("^[\\x00-\\x7f]+$", Pattern.CASE_INSENSITIVE).matcher(test).find()) { + if (isBase64(str, true) && !matched) { + test = decode_base64(str); + boolean isAscii = Pattern.compile("^[\\x00-\\x7f]+$").matcher(test).find(); + boolean isGzip = Pattern.compile("^\\x1f\\x8b\\x08").matcher(test).find(); + boolean isDeflate = Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(test).find(); + if (isAscii || isGzip || isDeflate) { str = test; matched = true; - encodingOpeningTags.append("<@base32>"); - encodingClosingTags.insert(0, ""); + encodingOpeningTags.append("<@base64>"); + encodingClosingTags.insert(0, ""); } } if (decrypt) { diff --git a/src/main/java/burp/hv/Hackvertor.java b/src/main/java/burp/hv/Hackvertor.java index bc8a5b3..7da0cd8 100644 --- a/src/main/java/burp/hv/Hackvertor.java +++ b/src/main/java/burp/hv/Hackvertor.java @@ -351,11 +351,10 @@ private void initCompressionTags() { addTag(Tag.Category.Compression, "bzip2_compress", true, "bzip2_compress(String str)"); addTag(Tag.Category.Compression, "bzip2_decompress", true, "bzip2_decompress(String str)"); addTag(Tag.Category.Compression, "deflate_compress", true, - "deflate_compress(String str, Boolean includeHeader)", - "boolean", "true"); + "deflate_compress(String str, String compressionType)//fixed, store, dynamic", + "string", "fixed"); addTag(Tag.Category.Compression, "deflate_decompress", true, - "deflate_decompress(String str, Boolean includeHeader)", - "boolean", "true"); + "deflate_decompress(String str)"); } private void initDateTags() { diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 6ad3647..637d2b6 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.24"; + public static String version = "v2.2.25"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/test/java/burp/ConvertorTests.java b/src/test/java/burp/ConvertorTests.java index 7c0982d..379d104 100644 --- a/src/test/java/burp/ConvertorTests.java +++ b/src/test/java/burp/ConvertorTests.java @@ -504,4 +504,82 @@ void testMultipleSameTags() { String converted = hackvertor.convert(input, hackvertor); assertEquals("SGVsbG8= V29ybGQ=", converted); } + + @Test + void testAutoDecodeGzipBase64() { + String encoded = hackvertor.convert("<@gzip_compress><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base64>foobar", decoded); + } + + @Test + void testAutoDecodeDeflateBase32() { + String encoded = hackvertor.convert("<@deflate_compress><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@deflate_compress><@base32>foobar", decoded); + } + + @Test + void testAutoDecodeBase32ThenDeflate() { + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>PCOALAFBBEAAAAGCN3KWAHPYP4MIHZQCBCVQE6Q=", hackvertor); + assertEquals("<@base32><@deflate_compress>foobar", decoded); + } + + @Test + void testAutoDecodeBase32NotMisidentifiedAsBase64() { + String base32Encoded = hackvertor.convert("<@base32>test", hackvertor); + assertEquals("ORSXG5A=", base32Encoded); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + base32Encoded + "", hackvertor); + assertEquals("<@base32>test", decoded); + } + + @Test + void testAutoDecodeBase64() { + String base64Encoded = hackvertor.convert("<@base64>foobar", hackvertor); + assertEquals("Zm9vYmFy", base64Encoded); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + base64Encoded + "", hackvertor); + assertEquals("<@base64>foobar", decoded); + } + + @Test + void testDeflateCompressDecompressDynamic() { + String input = "foobar"; + String compressed = hackvertor.convert("<@deflate_compress('dynamic')>" + input + "", hackvertor); + String decompressed = hackvertor.convert("<@deflate_decompress>" + compressed + "", hackvertor); + assertEquals(input, decompressed); + } + + @Test + void testDeflateCompressDecompressFixed() { + String input = "foobar"; + String compressed = hackvertor.convert("<@deflate_compress('fixed')>" + input + "", hackvertor); + String decompressed = hackvertor.convert("<@deflate_decompress>" + compressed + "", hackvertor); + assertEquals(input, decompressed); + } + + @Test + void testDeflateCompressDecompressStore() { + String input = "foobar"; + String compressed = hackvertor.convert("<@deflate_compress('store')>" + input + "", hackvertor); + String decompressed = hackvertor.convert("<@deflate_decompress>" + compressed + "", hackvertor); + assertEquals(input, decompressed); + } + + @Test + void testDeflateCompressDynamicBase32Output() { + String encoded = hackvertor.convert("<@base32><@deflate_compress('dynamic')>foobar", hackvertor); + assertEquals("PDNEXS6PJ5FCYAQABCVQE6Q=", encoded); + } + + @Test + void testDeflateCompressFixedBase32Output() { + String encoded = hackvertor.convert("<@base32><@deflate_compress('fixed')>foobar", hackvertor); + assertEquals("PAAUXS6PJ5FCYAQABCVQE6Q=", encoded); + } + + @Test + void testDeflateCompressStoreBase32Output() { + String encoded = hackvertor.convert("<@base32><@deflate_compress('store')>foobar", hackvertor); + assertEquals("PAAQCBQA7H7WM33PMJQXECFLAJ5A====", encoded); + } } \ No newline at end of file diff --git a/src/test/java/burp/ui/HackvertorUiTest.java b/src/test/java/burp/ui/HackvertorUiTest.java index f3b342b..d415db2 100644 --- a/src/test/java/burp/ui/HackvertorUiTest.java +++ b/src/test/java/burp/ui/HackvertorUiTest.java @@ -1474,6 +1474,108 @@ void testUrlencodeAllTag() throws Exception { Assertions.assertEquals("%74%65%73%74", outputText, "Output should contain all characters URL encoded"); } + @Test + void testAutoDecodeGzipBase64Ui() throws Exception { + window.robot().waitForIdle(); + + Component[] allTextAreas = window.robot().finder() + .findAll(window.target(), component -> component instanceof JTextArea) + .toArray(new Component[0]); + + JTextArea inputArea = null; + JTextArea outputArea = null; + int hackvertorInputCount = 0; + + for (Component component : allTextAreas) { + if (component.getClass().getName().equals("burp.hv.ui.HackvertorInput")) { + if (hackvertorInputCount == 0) { + inputArea = (JTextArea) component; + } else if (hackvertorInputCount == 1) { + outputArea = (JTextArea) component; + } + hackvertorInputCount++; + } + } + + Assertions.assertNotNull(inputArea, "Input area should be found"); + Assertions.assertNotNull(outputArea, "Output area should be found"); + + window.robot().click(inputArea); + window.robot().waitForIdle(); + + final JTextArea finalInputArea = inputArea; + final JTextArea finalOutputArea = outputArea; + GuiActionRunner.execute(() -> finalInputArea.setText("<@gzip_compress><@base64>foobar")); + window.robot().waitForIdle(); + + Thread.sleep(300); + window.robot().waitForIdle(); + + String encodedOutput = outputArea.getText(); + Assertions.assertFalse(encodedOutput.isEmpty(), "Output should contain gzip+base64 encoded data"); + + GuiActionRunner.execute(() -> finalInputArea.setText("<@auto_decode_no_decrypt>" + finalOutputArea.getText() + "")); + window.robot().waitForIdle(); + + Thread.sleep(300); + window.robot().waitForIdle(); + + String decodedOutput = outputArea.getText(); + Assertions.assertEquals("<@gzip_compress><@base64>foobar", decodedOutput, + "auto_decode_no_decrypt should produce correct gzip+base64 encoding tags"); + } + + @Test + void testAutoDecodeDeflateBase32Ui() throws Exception { + window.robot().waitForIdle(); + + Component[] allTextAreas = window.robot().finder() + .findAll(window.target(), component -> component instanceof JTextArea) + .toArray(new Component[0]); + + JTextArea inputArea = null; + JTextArea outputArea = null; + int hackvertorInputCount = 0; + + for (Component component : allTextAreas) { + if (component.getClass().getName().equals("burp.hv.ui.HackvertorInput")) { + if (hackvertorInputCount == 0) { + inputArea = (JTextArea) component; + } else if (hackvertorInputCount == 1) { + outputArea = (JTextArea) component; + } + hackvertorInputCount++; + } + } + + Assertions.assertNotNull(inputArea, "Input area should be found"); + Assertions.assertNotNull(outputArea, "Output area should be found"); + + window.robot().click(inputArea); + window.robot().waitForIdle(); + + final JTextArea finalInputArea = inputArea; + final JTextArea finalOutputArea = outputArea; + GuiActionRunner.execute(() -> finalInputArea.setText("<@deflate_compress><@base32>foobar")); + window.robot().waitForIdle(); + + Thread.sleep(300); + window.robot().waitForIdle(); + + String encodedOutput = outputArea.getText(); + Assertions.assertFalse(encodedOutput.isEmpty(), "Output should contain deflate+base32 encoded data"); + + GuiActionRunner.execute(() -> finalInputArea.setText("<@auto_decode_no_decrypt>" + finalOutputArea.getText() + "")); + window.robot().waitForIdle(); + + Thread.sleep(300); + window.robot().waitForIdle(); + + String decodedOutput = outputArea.getText(); + Assertions.assertEquals("<@deflate_compress><@base32>foobar", decodedOutput, + "auto_decode_no_decrypt should produce correct deflate+base32 encoding tags"); + } + @AfterEach void checkForUncaughtExceptions() { // Check for uncaught exceptions and fail the test if any occurred From 6307b383150e6b76b1be9477a62871222b8eb714 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Wed, 26 Nov 2025 13:15:14 +0000 Subject: [PATCH 12/24] Updated README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a2fb6f5..573bb26 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ Tags also support arguments. The find tag allows you to find a string by regex a # Changelog +## Version v2.2.24 (2025-11-26) + +- Fixed deflate, base32 detection +- Improved auto decoder (smart decoding) + ## Version v2.2.24 (2025-11-25) - Updated TagAutomator rules to allow multiple tools per rule From e167e604e1390973a55f1be205de1e61f8ab1bfa Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Wed, 26 Nov 2025 14:30:13 +0000 Subject: [PATCH 13/24] Fixed dialog problems. Added limits for multiencoder --- README.md | 5 + .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 185 ++++++++++++------ 3 files changed, 128 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 573bb26..730ee78 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ Tags also support arguments. The find tag allows you to find a string by regex a # Changelog +## Version v2.2.26 (2025-11-26) + +- Fixed dialog problems +- Added limits for multiencoder + ## Version v2.2.24 (2025-11-26) - Fixed deflate, base32 detection diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 637d2b6..676a882 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.25"; + public static String version = "v2.2.26"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index dcc00cb..89a2109 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -32,6 +32,8 @@ public class MultiEncoderWindow { private static final int CORNER_RADIUS = 20; private static final int COLUMN_COUNT = 3; private static final int MAX_VARIANTS_DISPLAY = 100; + private static final int MAX_VARIANTS_TOTAL = 10000; + private static final int MAX_TAGS_PER_LAYER = 50; private static final int CONVERT_TIMEOUT_SECONDS = 20; private final MontoyaApi montoyaApi; @@ -46,6 +48,8 @@ public class MultiEncoderWindow { private JComboBox modeComboBox; private JTabbedPane layerTabbedPane; private int layerCounter = 1; + private JLabel statusLabel; + private Timer statusClearTimer; private class Layer { final Map tagCheckboxes; @@ -155,6 +159,12 @@ public void show() { previewPanel.add(previewScrollPane, BorderLayout.CENTER); + statusLabel = new JLabel(" "); + statusLabel.setFont(new Font("Inter", Font.BOLD, 12)); + statusLabel.setHorizontalAlignment(SwingConstants.CENTER); + statusLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); + montoyaApi.userInterface().applyThemeToComponent(statusLabel); + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); montoyaApi.userInterface().applyThemeToComponent(buttonPanel); @@ -235,9 +245,14 @@ public void windowLostFocus(java.awt.event.WindowEvent e) { contentPanel.add(topPanel, BorderLayout.CENTER); montoyaApi.userInterface().applyThemeToComponent(contentPanel); + JPanel southPanel = new JPanel(new BorderLayout()); + southPanel.add(statusLabel, BorderLayout.NORTH); + southPanel.add(buttonPanel, BorderLayout.CENTER); + montoyaApi.userInterface().applyThemeToComponent(southPanel); + mainPanel.add(contentPanel, BorderLayout.NORTH); mainPanel.add(previewPanel, BorderLayout.CENTER); - mainPanel.add(buttonPanel, BorderLayout.SOUTH); + mainPanel.add(southPanel, BorderLayout.SOUTH); applyRoundedCorners.run(); window.add(mainPanel); @@ -378,24 +393,38 @@ private void addLayer() { tagsPanel.repaint(); }; - selectAllCheckbox.addActionListener(e -> { - ArrayList filteredTags = filterTags.apply(searchField.getText()); - boolean selectAll = selectAllCheckbox.isSelected(); - for (Tag tag : filteredTags) { - JCheckBox checkbox = layer.tagCheckboxes.get(tag.name); - if (checkbox != null && checkbox.isSelected() != selectAll) { - checkbox.setSelected(selectAll); - if (selectAll) { - if (!layer.selectedTags.contains(tag)) { - layer.selectedTags.add(tag); + java.awt.event.ActionListener selectAllListener = new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent e) { + ArrayList filteredTags = filterTags.apply(searchField.getText()); + boolean selectAll = selectAllCheckbox.isSelected(); + + if (selectAll && filteredTags.size() > MAX_TAGS_PER_LAYER) { + showWarningMessage("Too many tags (" + filteredTags.size() + "). Max is " + MAX_TAGS_PER_LAYER + ". Use search to filter."); + } + + int count = 0; + for (Tag tag : filteredTags) { + if (selectAll && count >= MAX_TAGS_PER_LAYER) { + break; + } + JCheckBox checkbox = layer.tagCheckboxes.get(tag.name); + if (checkbox != null && checkbox.isSelected() != selectAll) { + checkbox.setSelected(selectAll); + if (selectAll) { + if (!layer.selectedTags.contains(tag)) { + layer.selectedTags.add(tag); + } + } else { + layer.selectedTags.remove(tag); } - } else { - layer.selectedTags.remove(tag); } + count++; } + updatePreview(); } - updatePreview(); - }); + }; + selectAllCheckbox.addActionListener(selectAllListener); searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { public void insertUpdate(javax.swing.event.DocumentEvent e) { @@ -451,6 +480,32 @@ private ArrayList> getAllLayerTags() { return allLayerTags; } + private void showStatusMessage(String message, Color color) { + if (statusClearTimer != null && statusClearTimer.isRunning()) { + statusClearTimer.stop(); + } + statusLabel.setText(message); + statusLabel.setForeground(color); + statusClearTimer = new Timer(5000, e -> { + statusLabel.setText(" "); + statusClearTimer.stop(); + }); + statusClearTimer.setRepeats(false); + statusClearTimer.start(); + } + + private void showWarningMessage(String message) { + showStatusMessage("⚠ " + message, new Color(255, 165, 0)); + } + + private void showInfoMessage(String message) { + showStatusMessage("✓ " + message, new Color(0, 128, 0)); + } + + private void showErrorMessage(String message) { + showStatusMessage("✗ " + message, new Color(200, 0, 0)); + } + private String convertWithTimeout(String taggedText) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = executor.submit(() -> @@ -469,6 +524,22 @@ private String convertWithTimeout(String taggedText) { } } + private int calculateTotalVariants() { + ArrayList> allLayerTags = getAllLayerTags(); + if (allLayerTags.isEmpty()) { + return 1; + } + int total = 1; + for (ArrayList layerTags : allLayerTags) { + int layerSize = Math.min(layerTags.size(), MAX_TAGS_PER_LAYER); + if (total > MAX_VARIANTS_TOTAL / layerSize) { + return MAX_VARIANTS_TOTAL + 1; + } + total *= layerSize; + } + return total; + } + private ArrayList generateAllVariants(String input, boolean shouldConvert) { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { @@ -477,13 +548,27 @@ private ArrayList generateAllVariants(String input, boolean shouldConver return result; } + int estimatedTotal = calculateTotalVariants(); + if (estimatedTotal > MAX_VARIANTS_TOTAL) { + ArrayList result = new ArrayList<>(); + result.add("Error: Too many variants (" + estimatedTotal + "+). Maximum allowed is " + MAX_VARIANTS_TOTAL + ". Please reduce tag selection."); + return result; + } + ArrayList currentVariants = new ArrayList<>(); currentVariants.add(input); for (ArrayList layerTags : allLayerTags) { ArrayList newVariants = new ArrayList<>(); + int tagCount = 0; for (String variant : currentVariants) { for (Tag tag : layerTags) { + if (tagCount >= MAX_TAGS_PER_LAYER) { + break; + } + if (newVariants.size() >= MAX_VARIANTS_TOTAL) { + break; + } String[] tagStartEnd = Convertors.generateTagStartEnd(tag); String taggedText = tagStartEnd[0] + variant + tagStartEnd[1]; @@ -493,7 +578,16 @@ private ArrayList generateAllVariants(String input, boolean shouldConver } else { newVariants.add(taggedText); } + tagCount++; } + if (newVariants.size() >= MAX_VARIANTS_TOTAL) { + break; + } + tagCount = 0; + } + if (newVariants.size() >= MAX_VARIANTS_TOTAL) { + currentVariants = newVariants; + break; } currentVariants = newVariants; } @@ -580,10 +674,7 @@ private void updatePreview() { private void sendToHackvertor() { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { - JOptionPane.showMessageDialog(window, - "Please select at least one tag before sending to Hackvertor.", - "No Tags Selected", - JOptionPane.WARNING_MESSAGE); + showWarningMessage("Please select at least one tag before sending to Hackvertor."); return; } @@ -605,18 +696,12 @@ private void sendToHackvertor() { private void sendToRepeater() { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { - JOptionPane.showMessageDialog(window, - "Please select at least one tag before sending to Repeater.", - "No Tags Selected", - JOptionPane.WARNING_MESSAGE); + showWarningMessage("Please select at least one tag before sending to Repeater."); return; } if (messageEditor == null || baseRequestResponse == null) { - JOptionPane.showMessageDialog(window, - "Unable to access the original request.", - "Error", - JOptionPane.ERROR_MESSAGE); + showErrorMessage("Unable to access the original request."); return; } @@ -635,29 +720,19 @@ private void sendToRepeater() { variantNum++; } - JOptionPane.showMessageDialog(window, - "Sent " + variants.size() + " variant(s) to Repeater.", - "Success", - JOptionPane.INFORMATION_MESSAGE); - + showInfoMessage("Sent " + variants.size() + " variant(s) to Repeater."); window.dispose(); } private void sendToIntruder() { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { - JOptionPane.showMessageDialog(window, - "Please select at least one tag before sending to Intruder.", - "No Tags Selected", - JOptionPane.WARNING_MESSAGE); + showWarningMessage("Please select at least one tag before sending to Intruder."); return; } if (messageEditor == null || baseRequestResponse == null) { - JOptionPane.showMessageDialog(window, - "Unable to access the original request.", - "Error", - JOptionPane.ERROR_MESSAGE); + showErrorMessage("Unable to access the original request."); return; } @@ -668,10 +743,7 @@ private void sendToIntruder() { int endPos = startPos + selectedText.length(); if (startPos == -1) { - JOptionPane.showMessageDialog(window, - "Could not find the selected text in the request.", - "Error", - JOptionPane.ERROR_MESSAGE); + showErrorMessage("Could not find the selected text in the request."); return; } @@ -680,32 +752,19 @@ private void sendToIntruder() { String tabName = "HV-Layers-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); montoyaApi.intruder().sendToIntruder(baseRequestResponse.request().httpService(), intruderTemplate, tabName); - StringBuilder payloads = new StringBuilder(); - payloads.append("Hackvertor Multi-Encoder Layered Payloads:\n"); - boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - payloads.append("Mode: ").append(modeComboBox.getSelectedItem()).append("\n"); - payloads.append(getLayersSummary()); - ArrayList variants = generateAllVariants(selectedText, shouldConvert); - payloads.append("Total variants: ").append(variants.size()).append("\n"); - payloads.append("Copy these payloads to use in Intruder:\n\n"); + StringBuilder payloadList = new StringBuilder(); for (String variant : variants) { - payloads.append(variant).append("\n"); + payloadList.append(variant).append("\n"); } - JTextArea payloadArea = new JTextArea(payloads.toString()); - payloadArea.setEditable(false); - payloadArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); - JScrollPane scrollPane = new JScrollPane(payloadArea); - scrollPane.setPreferredSize(new Dimension(600, 400)); - - JOptionPane.showMessageDialog(window, - scrollPane, - "Payloads for Intruder", - JOptionPane.INFORMATION_MESSAGE); + StringSelection selection = new StringSelection(payloadList.toString()); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(selection, null); + JOptionPane.showMessageDialog(window, "Sent to Intruder. " + variants.size() + " payload(s) copied to clipboard.", "Success", JOptionPane.INFORMATION_MESSAGE); window.dispose(); } } From f7799af27957ef30a0f92ad004dfc96da1c239a7 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Wed, 26 Nov 2025 15:40:35 +0000 Subject: [PATCH 14/24] Added more test coverage and a small refactor --- src/main/java/burp/hv/Convertors.java | 43 +- src/test/java/burp/AutoDecodeNestedTests.java | 477 ++++++++++++++++++ 2 files changed, 504 insertions(+), 16 deletions(-) create mode 100644 src/test/java/burp/AutoDecodeNestedTests.java diff --git a/src/main/java/burp/hv/Convertors.java b/src/main/java/burp/hv/Convertors.java index fe2a5b5..e136199 100644 --- a/src/main/java/burp/hv/Convertors.java +++ b/src/main/java/burp/hv/Convertors.java @@ -3357,6 +3357,23 @@ static > Map sortByValuesAsc(final Map ma static Boolean isBase64(String str, Boolean checkStart) { return Pattern.compile((checkStart ? "^" : "") + "[a-zA-Z0-9+/]{4,}=*$", Pattern.CASE_INSENSITIVE).matcher(str).find() && str.length() % 4 == 0; } + + private static boolean isAscii(String str) { + return Pattern.compile("^[\\x00-\\x7f]+$").matcher(str).find(); + } + + private static boolean isPrintableAscii(String str) { + return Pattern.compile("^[\\x09-\\x7f]+$").matcher(str).find(); + } + + private static boolean isGzip(String str) { + return Pattern.compile("^\\x1f\\x8b\\x08").matcher(str).find(); + } + + private static boolean isDeflate(String str) { + return Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(str).find(); + } + static String auto_decode(String str) { return auto_decode_decrypt(str, true); } @@ -3375,15 +3392,15 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { do { String startStr = str; matched = false; - if (Pattern.compile("^\\x1f\\x8b\\x08").matcher(str).find()) { + if (isGzip(str)) { str = gzip_decompress(str); matched = true; encodingOpeningTags.append("<@gzip_compress>"); encodingClosingTags.insert(0, ""); } - if (Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(str).find()) { + if (isDeflate(str)) { test = deflate_decompress(str); - if (!test.startsWith("Error:") && Pattern.compile("^[\\x00-\\x7f]+$").matcher(test).find()) { + if (!test.startsWith("Error:") && isAscii(test)) { str = test; matched = true; encodingOpeningTags.append("<@deflate_compress>"); @@ -3398,7 +3415,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } if (Pattern.compile("(?:[0-9a-fA-F]{2}[\\s,\\-]?){3,}").matcher(str).find()) { test = hex2ascii(str); - if (Pattern.compile("^[\\x09-\\x7f]+$", Pattern.CASE_INSENSITIVE).matcher(test).find()) { + if (isAscii(test) || isGzip(test) || isDeflate(test)) { str = test; encodingOpeningTags.append("<@ascii2hex(\" \")>"); encodingClosingTags.insert(0, ""); @@ -3420,7 +3437,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } if (Pattern.compile("(?:\\\\0{0,4}[0-9a-fA-F]{2}[\\s,\\-]?){3,}").matcher(str).find()) { test = decode_css_escapes(str); - if (Pattern.compile("^[\\x09-\\x7f]+$", Pattern.CASE_INSENSITIVE).matcher(test).find()) { + if (isPrintableAscii(test)) { str = test; matched = true; encodingOpeningTags.append("<@css_escapes>"); @@ -3429,7 +3446,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } if (Pattern.compile("\\\\x[0-9a-f]{2}", Pattern.CASE_INSENSITIVE).matcher(str).find()) { test = decode_js_string(str); - if (Pattern.compile("^[\\x09-\\x7f]+$", Pattern.CASE_INSENSITIVE).matcher(test).find()) { + if (isPrintableAscii(test)) { str = test; matched = true; encodingOpeningTags.append("<@hex_escapes>"); @@ -3438,7 +3455,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } if (Pattern.compile("\\\\[0-9]{1,3}").matcher(str).find()) { test = decode_js_string(str); - if (Pattern.compile("^[\\x09-\\x7f]+$", Pattern.CASE_INSENSITIVE).matcher(test).find()) { + if (isPrintableAscii(test)) { str = test; matched = true; encodingOpeningTags.append("<@octal_escapes>"); @@ -3447,7 +3464,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } if (Pattern.compile("\\\\u[0-9a-f]{4}", Pattern.CASE_INSENSITIVE).matcher(str).find()) { test = decode_js_string(str); - if (Pattern.compile("^[\\x09-\\x7f]+$", Pattern.CASE_INSENSITIVE).matcher(test).find()) { + if (isPrintableAscii(test)) { str = test; matched = true; encodingOpeningTags.append("<@unicode_escapes>"); @@ -3486,10 +3503,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } if (Pattern.compile("^[A-Z2-7]+=*$").matcher(str).find() && str.length() % 8 == 0) { test = decode_base32(str); - boolean isAscii = Pattern.compile("^[\\x00-\\x7f]+$").matcher(test).find(); - boolean isGzip = Pattern.compile("^\\x1f\\x8b\\x08").matcher(test).find(); - boolean isDeflate = Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(test).find(); - if (isAscii || isGzip || isDeflate) { + if (isAscii(test) || isGzip(test) || isDeflate(test)) { str = test; matched = true; encodingOpeningTags.append("<@base32>"); @@ -3498,10 +3512,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } if (isBase64(str, true) && !matched) { test = decode_base64(str); - boolean isAscii = Pattern.compile("^[\\x00-\\x7f]+$").matcher(test).find(); - boolean isGzip = Pattern.compile("^\\x1f\\x8b\\x08").matcher(test).find(); - boolean isDeflate = Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(test).find(); - if (isAscii || isGzip || isDeflate) { + if (isAscii(test) || isGzip(test) || isDeflate(test)) { str = test; matched = true; encodingOpeningTags.append("<@base64>"); diff --git a/src/test/java/burp/AutoDecodeNestedTests.java b/src/test/java/burp/AutoDecodeNestedTests.java new file mode 100644 index 0000000..2c1eb70 --- /dev/null +++ b/src/test/java/burp/AutoDecodeNestedTests.java @@ -0,0 +1,477 @@ +package burp; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class AutoDecodeNestedTests extends BaseHackvertorTest { + + @Test + void testAsciiHexSpaceToCssEscapesToBase64_3Levels() { + String input = "5c 35 41 5c 36 44 5c 33 39 5c 37 36 5c 35 39 5c 36 44 5c 34 36 5c 37 39"; + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + input + "", hackvertor); + assertEquals("<@ascii2hex(\" \")><@css_escapes><@base64>foobar", decoded); + } + + @Test + void testUnicodeEscapesToBase32_2Levels() { + String input = "\\u004D\\u005A\\u0058\\u0057\\u0036\\u0059\\u0054\\u0042\\u004F\\u0049\\u003D\\u003D\\u003D\\u003D\\u003D\\u003D"; + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + input + "", hackvertor); + assertEquals("<@unicode_escapes><@base32>foobar", decoded); + } + + @Test + void testGzipToBase64_2Levels() { + String encoded = hackvertor.convert("<@gzip_compress><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base64>foobar", decoded); + } + + @Test + void testDeflateToBase64_2Levels() { + String encoded = hackvertor.convert("<@deflate_compress><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@deflate_compress><@base64>foobar", decoded); + } + + @Test + void testBase32ToBase64_2Levels() { + String encoded = hackvertor.convert("<@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@base64>foobar", decoded); + } + + @Test + void testUrlencodeToBase64_2Levels() { + String encoded = hackvertor.convert("<@urlencode_all><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64>foobar", decoded); + } + + @Test + void testHexEscapesToBase64_2Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base64>foobar", decoded); + } + + @Test + void testOctalEscapesToBase64_2Levels() { + String encoded = hackvertor.convert("<@octal_escapes><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@octal_escapes><@base64>foobar", decoded); + } + + @Test + void testHexEntitiesToBase64_2Levels() { + String encoded = hackvertor.convert("<@hex_entities><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@base64>foobar", decoded); + } + + @Test + void testAsciiHexWithSeparatorToBase64_2Levels() { + String encoded = hackvertor.convert("<@ascii2hex(' ')><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@ascii2hex(\" \")><@base64>foobar", decoded); + } + + @Test + void testCharcodeToBase64_2Levels() { + String encoded = hackvertor.convert("<@to_charcode><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@to_charcode><@base64>foobar", decoded); + } + + @Test + void testCssEscapesToBase64_2Levels() { + String encoded = hackvertor.convert("<@css_escapes><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@css_escapes><@base64>foobar", decoded); + } + + @Test + void testUrlencodeToHexEscapesToBase64_3Levels() { + String encoded = hackvertor.convert("<@urlencode_all><@hex_escapes><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@hex_escapes><@base64>foobar", decoded); + } + + @Test + void testBase64ToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testBase32ToBase32ToBase32_3Levels() { + String encoded = hackvertor.convert("<@base32><@base32><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@base32><@base32>foobar", decoded); + } + + @Test + void testUnicodeEscapesToHexEscapesToBase64_3Levels() { + String encoded = hackvertor.convert("<@unicode_escapes><@hex_escapes><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@unicode_escapes><@hex_escapes><@base64>foobar", decoded); + } + + @Test + void testHexEntitiesToUrlencodeToBase64_3Levels() { + String encoded = hackvertor.convert("<@hex_entities><@urlencode_all><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@urlencode_not_plus><@base64>foobar", decoded); + } + + @Test + void testGzipToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@gzip_compress><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base64><@base64>foobar", decoded); + } + + @Test + void testDeflateToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@deflate_compress><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@deflate_compress><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64ToBase64ToBase64ToBase64_4Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64x5_5Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64x6_6Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64x7_7Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64x8_8Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64x9_9Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64x10_10Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testAsciiHexSpaceToBase64_2Levels() { + String encoded = hackvertor.convert("<@ascii2hex(' ')><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@ascii2hex(\" \")><@base64>foobar", decoded); + } + + @Test + void testAsciiHexSpaceToGzip_2Levels() { + String encoded = hackvertor.convert("<@ascii2hex(' ')><@gzip_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@ascii2hex(\" \")><@gzip_compress>foobar", decoded); + } + + @Test + void testAsciiHexSpaceToDeflate_2Levels() { + String encoded = hackvertor.convert("<@ascii2hex(' ')><@deflate_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@ascii2hex(\" \")><@deflate_compress>foobar", decoded); + } + + @Test + void testBase32ToGzip_2Levels() { + String encoded = hackvertor.convert("<@base32><@gzip_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@gzip_compress>foobar", decoded); + } + + @Test + void testBase32ToDeflate_2Levels() { + String encoded = hackvertor.convert("<@base32><@deflate_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@deflate_compress>foobar", decoded); + } + + @Test + void testCssEscapesToBase32_2Levels() { + String encoded = hackvertor.convert("<@css_escapes><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@css_escapes><@base32>foobar", decoded); + } + + @Test + void testHexEscapesToBase32_2Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base32>foobar", decoded); + } + + @Test + void testOctalEscapesToBase32_2Levels() { + String encoded = hackvertor.convert("<@octal_escapes><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@octal_escapes><@base32>foobar", decoded); + } + + @Test + void testHexEntitiesToBase32_2Levels() { + String encoded = hackvertor.convert("<@hex_entities><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@base32>foobar", decoded); + } + + @Test + void testUrlencodeToBase32_2Levels() { + String encoded = hackvertor.convert("<@urlencode_all><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base32>foobar", decoded); + } + + @Test + void testOctalEscapesToHexEscapesToBase64_3Levels() { + String encoded = hackvertor.convert("<@octal_escapes><@hex_escapes><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@octal_escapes><@hex_escapes><@base64>foobar", decoded); + } + + @Test + void testHexEntitiesToHexEntitiesToBase64_3Levels() { + String encoded = hackvertor.convert("<@hex_entities><@hex_entities><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@hex_entities><@base64>foobar", decoded); + } + + @Test + void testBase32ToDeflateToBase64_3Levels() { + String encoded = hackvertor.convert("<@base32><@deflate_compress><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@deflate_compress><@base64>foobar", decoded); + } + + @Test + void testAsciiHexSpaceToBase32ToGzip_3Levels() { + String encoded = hackvertor.convert("<@ascii2hex(' ')><@base32><@gzip_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@ascii2hex(\" \")><@base32><@gzip_compress>foobar", decoded); + } + + @Test + void testHexEscapesToBase64ToBase32ToGzip_4Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@base64><@base32><@gzip_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base64><@base32><@gzip_compress>foobar", decoded); + } + + @Test + void testGzipToBase32_2Levels() { + String encoded = hackvertor.convert("<@gzip_compress><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base32>foobar", decoded); + } + + @Test + void testDeflateToBase32_2Levels() { + String encoded = hackvertor.convert("<@deflate_compress><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@deflate_compress><@base32>foobar", decoded); + } + + @Test + void testCharcodeToBase32_2Levels() { + String encoded = hackvertor.convert("<@to_charcode><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@to_charcode><@base32>foobar", decoded); + } + + @Test + void testBase32ToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@base32><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@base64><@base64>foobar", decoded); + } + + @Test + void testBase32ToGzipToBase64_3Levels() { + String encoded = hackvertor.convert("<@base32><@gzip_compress><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@gzip_compress><@base64>foobar", decoded); + } + + @Test + void testUrlencodeToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@urlencode_all><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64><@base64>foobar", decoded); + } + + @Test + void testHexEntitiesToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@hex_entities><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@base64><@base64>foobar", decoded); + } + + @Test + void testHexEscapesToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base64><@base64>foobar", decoded); + } + + @Test + void testOctalEscapesToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@octal_escapes><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@octal_escapes><@base64><@base64>foobar", decoded); + } + + @Test + void testUnicodeEscapesToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@unicode_escapes><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@unicode_escapes><@base64><@base64>foobar", decoded); + } + + @Test + void testCssEscapesToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@css_escapes><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@css_escapes><@base64><@base64>foobar", decoded); + } + + @Test + void testBase64ToBase32ToBase64_3Levels() { + String encoded = hackvertor.convert("<@base64><@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base32><@base64>foobar", decoded); + } + + @Test + void testBase32ToBase64ToBase32_3Levels() { + String encoded = hackvertor.convert("<@base32><@base64><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@base64><@base32>foobar", decoded); + } + + @Test + void testBase64ToBase64ToBase32_3Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base32>foobar", decoded); + } + + @Test + void testBase32ToBase32ToBase64_3Levels() { + String encoded = hackvertor.convert("<@base32><@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@base32><@base64>foobar", decoded); + } + + @Test + void testHexEntitiesToBase32ToBase64_3Levels() { + String encoded = hackvertor.convert("<@hex_entities><@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@base32><@base64>foobar", decoded); + } + + @Test + void testUrlencodeToBase32ToBase64_3Levels() { + String encoded = hackvertor.convert("<@urlencode_all><@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base32><@base64>foobar", decoded); + } + + @Test + void testHexEscapesToBase32ToBase64_3Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base32><@base64>foobar", decoded); + } + + @Test + void testGzipToBase32ToBase64_3Levels() { + String encoded = hackvertor.convert("<@gzip_compress><@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base32><@base64>foobar", decoded); + } + + @Test + void testDeflateToBase32ToBase64_3Levels() { + String encoded = hackvertor.convert("<@deflate_compress><@base32><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@deflate_compress><@base32><@base64>foobar", decoded); + } + + @Test + void testBase64ToBase64ToBase64ToBase32_4Levels() { + String encoded = hackvertor.convert("<@base64><@base64><@base64><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64><@base64><@base32>foobar", decoded); + } + + @Test + void testBase32ToBase64ToBase64ToBase64_4Levels() { + String encoded = hackvertor.convert("<@base32><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testGzipToBase64ToBase64ToBase64_4Levels() { + String encoded = hackvertor.convert("<@gzip_compress><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testDeflateToBase64ToBase64ToBase64_4Levels() { + String encoded = hackvertor.convert("<@deflate_compress><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@deflate_compress><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testHexEntitiesToBase64ToBase64ToBase64_4Levels() { + String encoded = hackvertor.convert("<@hex_entities><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@base64><@base64><@base64>foobar", decoded); + } + + @Test + void testUrlencodeToBase64ToBase64ToBase64_4Levels() { + String encoded = hackvertor.convert("<@urlencode_all><@base64><@base64><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64><@base64><@base64>foobar", decoded); + } +} From 9dd69ef5d072b54ad2e826d94922a7675bfe8c25 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Wed, 26 Nov 2025 20:31:55 +0000 Subject: [PATCH 15/24] Improved AutoDecoder, fixed tests, added more realistic AutoDecodeNestedTests --- src/main/java/burp/hv/Convertors.java | 208 +++++--- .../java/burp/hv/HackvertorExtension.java | 2 +- src/test/java/burp/AutoDecodeNestedTests.java | 454 ++++++++++++++++++ src/test/java/burp/ConvertorTests.java | 13 + src/test/java/burp/stubs/StubCallbacks.java | 2 +- .../java/burp/stubs/StubExtensionHelpers.java | 2 +- 6 files changed, 598 insertions(+), 83 deletions(-) diff --git a/src/main/java/burp/hv/Convertors.java b/src/main/java/burp/hv/Convertors.java index e136199..5e45463 100644 --- a/src/main/java/burp/hv/Convertors.java +++ b/src/main/java/burp/hv/Convertors.java @@ -3358,20 +3358,51 @@ static Boolean isBase64(String str, Boolean checkStart) { return Pattern.compile((checkStart ? "^" : "") + "[a-zA-Z0-9+/]{4,}=*$", Pattern.CASE_INSENSITIVE).matcher(str).find() && str.length() % 4 == 0; } + private static final Pattern ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7f]+$"); + private static final Pattern PRINTABLE_ASCII_PATTERN = Pattern.compile("^[\\x09-\\x7f]+$"); + private static final Pattern GZIP_PATTERN = Pattern.compile("^\\x1f\\x8b\\x08"); + private static final Pattern DEFLATE_PATTERN = Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]"); + private static final Pattern BINARY_PATTERN = Pattern.compile("[01]{4,}\\s+[01]{4,}"); + private static final Pattern HEX_SPACED_PATTERN = Pattern.compile("(?:[0-9a-fA-F]{2}[\\s,\\-]?){3,}"); + private static final Pattern HEX_PATTERN = Pattern.compile("^[0-9a-fA-F]+$"); + private static final Pattern CHARCODE_PATTERN = Pattern.compile("\\d+[,\\s]+"); + private static final Pattern NON_CHARCODE_PATTERN = Pattern.compile("[^\\d,\\s]"); + private static final Pattern CSS_ESCAPE_PATTERN = Pattern.compile("(?:\\\\0{0,4}[0-9a-fA-F]{2}[\\s,\\-]?){3,}"); + private static final Pattern HEX_ESCAPE_PATTERN = Pattern.compile("\\\\x[0-9a-f]{2}", Pattern.CASE_INSENSITIVE); + private static final Pattern OCTAL_ESCAPE_PATTERN = Pattern.compile("\\\\[0-9]{1,3}"); + private static final Pattern UNICODE_ESCAPE_PATTERN = Pattern.compile("\\\\u[0-9a-f]{4}", Pattern.CASE_INSENSITIVE); + private static final Pattern HTML_ENTITY_PATTERN = Pattern.compile("&[a-zA-Z]+;", Pattern.CASE_INSENSITIVE); + private static final Pattern HEX_ENTITY_PATTERN = Pattern.compile("&#x?[0-9a-f]+;?", Pattern.CASE_INSENSITIVE); + private static final Pattern URL_ENCODE_PATTERN = Pattern.compile("%[0-9a-f]{2}", Pattern.CASE_INSENSITIVE); + private static final Pattern JWT_PATTERN = Pattern.compile("^[a-zA-Z0-9\\-_.]+$", Pattern.CASE_INSENSITIVE); + private static final Pattern BASE32_PATTERN = Pattern.compile("^[A-Z2-7]+=*$"); + private static final Pattern WORDS_PATTERN = Pattern.compile("(?:[a-zA-Z]+[\\s,-]){2,}"); + private static final Pattern LOWERCASE_WORDS_PATTERN = Pattern.compile("(?:[a-z]+[\\s,-]){2,}"); + private static final Pattern LOWERCASE_ONLY_PATTERN = Pattern.compile("^[a-z]{10,}$"); + private static final Pattern BYTE_PATTERN = Pattern.compile("^[\\x00-\\xff]+$", Pattern.CASE_INSENSITIVE); + private static final Pattern BASE58_PATTERN = Pattern.compile("^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$"); + private static final Pattern BASE64URL_PATTERN = Pattern.compile("^[A-Za-z0-9_-]+$"); + private static final Pattern QUOTED_PRINTABLE_PATTERN = Pattern.compile("=[0-9A-Fa-f]{2}"); + private static final Pattern UTF7_PATTERN = Pattern.compile("\\+[A-Za-z0-9+/]*-"); + private static boolean isAscii(String str) { - return Pattern.compile("^[\\x00-\\x7f]+$").matcher(str).find(); + return ASCII_PATTERN.matcher(str).find(); } private static boolean isPrintableAscii(String str) { - return Pattern.compile("^[\\x09-\\x7f]+$").matcher(str).find(); + return PRINTABLE_ASCII_PATTERN.matcher(str).find(); } private static boolean isGzip(String str) { - return Pattern.compile("^\\x1f\\x8b\\x08").matcher(str).find(); + return GZIP_PATTERN.matcher(str).find(); } private static boolean isDeflate(String str) { - return Pattern.compile("^\\x78[\\x01\\x5e\\x9c\\xda]").matcher(str).find(); + return DEFLATE_PATTERN.matcher(str).find(); + } + + private static boolean isAsciiOrCompressed(String str) { + return isAscii(str) || isGzip(str) || isDeflate(str); } static String auto_decode(String str) { @@ -3387,140 +3418,152 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { int repeat = 0; boolean matched; String test; - StringBuilder encodingOpeningTags = new StringBuilder(); - StringBuilder encodingClosingTags = new StringBuilder(); + StringBuilder openTags = new StringBuilder(); + StringBuilder closeTags = new StringBuilder(); do { String startStr = str; matched = false; if (isGzip(str)) { str = gzip_decompress(str); matched = true; - encodingOpeningTags.append("<@gzip_compress>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "gzip_compress"); } if (isDeflate(str)) { test = deflate_decompress(str); if (!test.startsWith("Error:") && isAscii(test)) { str = test; matched = true; - encodingOpeningTags.append("<@deflate_compress>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "deflate_compress"); } } - if (Pattern.compile("[01]{4,}\\s+[01]{4,}").matcher(str).find()) { + if (BINARY_PATTERN.matcher(str).find()) { str = bin2ascii(str); matched = true; - encodingOpeningTags.append("<@ascii2bin>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "ascii2bin"); } - if (Pattern.compile("(?:[0-9a-fA-F]{2}[\\s,\\-]?){3,}").matcher(str).find()) { + if (HEX_SPACED_PATTERN.matcher(str).find()) { test = hex2ascii(str); - if (isAscii(test) || isGzip(test) || isDeflate(test)) { + if (isAsciiOrCompressed(test)) { str = test; - encodingOpeningTags.append("<@ascii2hex(\" \")>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "ascii2hex(\" \")", "ascii2hex"); repeat++; continue; } } - if (Pattern.compile("^[0-9a-fA-F]+$").matcher(str).find() && str.length() % 2 == 0) { + if (HEX_PATTERN.matcher(str).find() && str.length() % 2 == 0) { str = hex2ascii(str); matched = true; - encodingOpeningTags.append("<@ascii2hex(\"\")>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "ascii2hex(\"\")", "ascii2hex"); } - if (!Pattern.compile("[^\\d,\\s]").matcher(str).find() && Pattern.compile("\\d+[,\\s]+").matcher(str).find()) { + if (!NON_CHARCODE_PATTERN.matcher(str).find() && CHARCODE_PATTERN.matcher(str).find()) { str = from_charcode(str); matched = true; - encodingOpeningTags.append("<@to_charcode>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "to_charcode"); } - if (Pattern.compile("(?:\\\\0{0,4}[0-9a-fA-F]{2}[\\s,\\-]?){3,}").matcher(str).find()) { + if (CSS_ESCAPE_PATTERN.matcher(str).find()) { test = decode_css_escapes(str); if (isPrintableAscii(test)) { str = test; matched = true; - encodingOpeningTags.append("<@css_escapes>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "css_escapes"); } } - if (Pattern.compile("\\\\x[0-9a-f]{2}", Pattern.CASE_INSENSITIVE).matcher(str).find()) { + if (HEX_ESCAPE_PATTERN.matcher(str).find()) { test = decode_js_string(str); if (isPrintableAscii(test)) { str = test; matched = true; - encodingOpeningTags.append("<@hex_escapes>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "hex_escapes"); } } - if (Pattern.compile("\\\\[0-9]{1,3}").matcher(str).find()) { + if (OCTAL_ESCAPE_PATTERN.matcher(str).find()) { test = decode_js_string(str); if (isPrintableAscii(test)) { str = test; matched = true; - encodingOpeningTags.append("<@octal_escapes>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "octal_escapes"); } } - if (Pattern.compile("\\\\u[0-9a-f]{4}", Pattern.CASE_INSENSITIVE).matcher(str).find()) { + if (UNICODE_ESCAPE_PATTERN.matcher(str).find()) { test = decode_js_string(str); if (isPrintableAscii(test)) { str = test; matched = true; - encodingOpeningTags.append("<@unicode_escapes>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "unicode_escapes"); } } - if (Pattern.compile("&[a-zA-Z]+;", Pattern.CASE_INSENSITIVE).matcher(str).find()) { + if (HTML_ENTITY_PATTERN.matcher(str).find()) { str = decode_html5_entities(str); matched = true; - encodingOpeningTags.append("<@html_entities>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "html_entities"); } - if (Pattern.compile("&#x?[0-9a-f]+;?", Pattern.CASE_INSENSITIVE).matcher(str).find()) { + if (HEX_ENTITY_PATTERN.matcher(str).find()) { str = decode_html5_entities(str); matched = true; - encodingOpeningTags.append("<@hex_entities>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "hex_entities"); } - if (Pattern.compile("%[0-9a-f]{2}", Pattern.CASE_INSENSITIVE).matcher(str).find()) { + if (URL_ENCODE_PATTERN.matcher(str).find()) { boolean plus = str.contains("+"); str = decode_url(str); matched = true; - if (plus) { - encodingOpeningTags.append("<@urlencode>"); - encodingClosingTags.insert(0, ""); - } else { - encodingOpeningTags.append("<@urlencode_not_plus>"); - encodingClosingTags.insert(0, ""); - } + appendTags(openTags, closeTags, plus ? "urlencode" : "urlencode_not_plus"); } - if (Pattern.compile("^[a-zA-Z0-9\\-_.]+$", Pattern.CASE_INSENSITIVE).matcher(str).find()) { + if (JWT_PATTERN.matcher(str).find()) { String[] parts = str.split("\\."); if (parts.length == 3 && !d_jwt_get_header(str).equals("Invalid token")) { return d_jwt_get_header(str) + "\n" + d_jwt_get_payload(str) + "\n" + decode_base64url(parts[2]); } } - if (Pattern.compile("^[A-Z2-7]+=*$").matcher(str).find() && str.length() % 8 == 0) { + if (BASE32_PATTERN.matcher(str).find() && str.length() % 8 == 0) { test = decode_base32(str); - if (isAscii(test) || isGzip(test) || isDeflate(test)) { + if (isAsciiOrCompressed(test)) { str = test; matched = true; - encodingOpeningTags.append("<@base32>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "base32"); } } if (isBase64(str, true) && !matched) { test = decode_base64(str); - if (isAscii(test) || isGzip(test) || isDeflate(test)) { + if (isAsciiOrCompressed(test)) { + str = test; + matched = true; + appendTags(openTags, closeTags, "base64"); + } + } + if (BASE64URL_PATTERN.matcher(str).find() && str.length() >= 4 && !matched) { + test = decode_base64url(str); + if (isAsciiOrCompressed(test)) { + str = test; + matched = true; + appendTags(openTags, closeTags, "base64url"); + } + } + if (BASE58_PATTERN.matcher(str).find() && str.length() >= 4 && !matched) { + test = decode_base58(str); + if (isPrintableAscii(test)) { str = test; matched = true; - encodingOpeningTags.append("<@base64>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "base58"); + } + } + if (QUOTED_PRINTABLE_PATTERN.matcher(str).find() && !matched) { + test = d_quoted_printable(str); + if (!test.startsWith("Error") && isPrintableAscii(test)) { + str = test; + matched = true; + appendTags(openTags, closeTags, "quoted_printable"); + } + } + if (UTF7_PATTERN.matcher(str).find() && !matched) { + test = utf7Decode(str); + if (isPrintableAscii(test) && !test.equals(str)) { + str = test; + matched = true; + appendTags(openTags, closeTags, "utf7", "utf7"); } } if (decrypt) { - if (Pattern.compile("(?:[a-zA-Z]+[\\s,-]){2,}").matcher(str).find()) { + if (WORDS_PATTERN.matcher(str).find()) { double total = 0; double bestScore = -9999999; int n = 0; @@ -3533,7 +3576,7 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { n = i; } } - double average = (total / 25); + double average = total / 25; if ((((average - bestScore) / average) * 100) > 20) { String originalString = str; str = rotN(str, n); @@ -3544,11 +3587,11 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { break; } } - encodingOpeningTags.append("<@rotN(").append(n).append(")>"); - encodingClosingTags.insert(0, ""); + openTags.append("<@rotN(").append(n).append(")>"); + closeTags.insert(0, ""); } } - if (Pattern.compile("(?:[a-z]+[\\s,-]){2,}").matcher(str).find()) { + if (LOWERCASE_WORDS_PATTERN.matcher(str).find()) { double total = 0; double bestScore = -9999999; int key1 = 0; @@ -3567,25 +3610,23 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } } } - double average = (total / 25); + double average = total / 25; if ((((average - bestScore) / average) * 100) > 60 && (key1 != 1 && key2 != 0)) { str = affine_decrypt(str, key1, key2); matched = true; - encodingOpeningTags.append("<@affine_encrypt(").append(key1).append(",").append(key2).append(")>"); - encodingClosingTags.insert(0, ""); + openTags.append("<@affine_encrypt(").append(key1).append(",").append(key2).append(")>"); + closeTags.insert(0, ""); } } - - if (Pattern.compile("(?:[a-z]+[\\s,-]){2,}").matcher(str).find()) { + if (LOWERCASE_WORDS_PATTERN.matcher(str).find()) { String plaintext = atbash_decrypt(str); if (is_like_english(plaintext) - is_like_english(str) >= 200) { str = plaintext; matched = true; - encodingOpeningTags.append("<@atbash_encrypt>"); - encodingClosingTags.insert(0, ""); + appendTags(openTags, closeTags, "atbash_encrypt"); } } - if (Pattern.compile("^[a-z]{10,}$").matcher(str).find()) { + if (LOWERCASE_ONLY_PATTERN.matcher(str).find()) { double total = 0; double bestScore = -9999999; int n = 0; @@ -3599,27 +3640,25 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { n = i; } } - double average = (total / max - 1); + double average = total / (max - 1); if ((((average - bestScore) / average) * 100) > 20) { str = rail_fence_decrypt(str, n); matched = true; - encodingOpeningTags.append("<@rail_fence_encrypt(").append(n).append(")>"); - encodingClosingTags.insert(0, ""); + openTags.append("<@rail_fence_encrypt(").append(n).append(")>"); + closeTags.insert(0, ""); } } - - if (Pattern.compile("^[\\x00-\\xff]+$", Pattern.CASE_INSENSITIVE).matcher(str).find()) { + if (BYTE_PATTERN.matcher(str).find()) { int lenGuess = guess_key_length(str); test = xor_decrypt(str, lenGuess, false); int alphaCount = test.replaceAll("[^a-zA-Z0-9]+", "").length(); - int strLen = str.length(); - float percent = (((float) alphaCount / strLen) * 100); + float percent = ((float) alphaCount / str.length()) * 100; if (is_like_english(test) < is_like_english(str) && percent > 20) { String key = xor_decrypt(str, lenGuess, true).replaceAll("\"", "\\\""); str = test; matched = true; - encodingOpeningTags.append("<@xor(\"").append(key).append("\")>"); - encodingClosingTags.insert(0, ""); + openTags.append("<@xor(\"").append(key).append("\")>"); + closeTags.insert(0, ""); } } } @@ -3628,7 +3667,16 @@ static String auto_decode_decrypt(String str, Boolean decrypt) { } repeat++; } while (repeat < repeats); - return encodingOpeningTags + str + encodingClosingTags; + return openTags + str + closeTags; + } + + private static void appendTags(StringBuilder openTags, StringBuilder closeTags, String tagName) { + appendTags(openTags, closeTags, tagName, tagName); + } + + private static void appendTags(StringBuilder openTags, StringBuilder closeTags, String openTagName, String closeTagName) { + openTags.append("<@").append(openTagName).append(">"); + closeTags.insert(0, ""); } static String range(String str, int from, int to, int step) { diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 676a882..390cc2a 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.26"; + public static String version = "v2.2.27"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/test/java/burp/AutoDecodeNestedTests.java b/src/test/java/burp/AutoDecodeNestedTests.java index 2c1eb70..7c94269 100644 --- a/src/test/java/burp/AutoDecodeNestedTests.java +++ b/src/test/java/burp/AutoDecodeNestedTests.java @@ -433,6 +433,132 @@ void testDeflateToBase32ToBase64_3Levels() { assertEquals("<@deflate_compress><@base32><@base64>foobar", decoded); } + @Test + void testBase64urlToBase64_2Levels() { + String encoded = hackvertor.convert("<@base64url><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64url><@base64>foobar", decoded); + } + + @Test + void testBase64urlToBase32_2Levels() { + String encoded = hackvertor.convert("<@base64url><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64url><@base32>foobar", decoded); + } + + @Test + void testBase64urlToGzip_2Levels() { + String encoded = hackvertor.convert("<@base64url><@gzip_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64url><@gzip_compress>foobar", decoded); + } + + @Test + void testBase64urlToDeflate_2Levels() { + String encoded = hackvertor.convert("<@base64url><@deflate_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64url><@deflate_compress>foobar", decoded); + } + + @Test + void testHexEscapesToBase64urlWithGzip_2Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@base64url><@gzip_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base64url><@gzip_compress>foobar", decoded); + } + + @Test + void testBase64urlToGzipToBase64_3Levels() { + String encoded = hackvertor.convert("<@base64url><@gzip_compress><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64url><@gzip_compress><@base64>foobar", decoded); + } + + @Test + void testBase58Single() { + String encoded = hackvertor.convert("<@base58>hello world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base58>hello world", decoded); + } + + @Test + void testBase58ToBase64_2Levels() { + String encoded = hackvertor.convert("<@base58><@base64>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base58><@base64>foobar", decoded); + } + + @Test + void testHexEscapesToBase58_2Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@base58>hello world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base58>hello world", decoded); + } + + @Test + void testBase58ToBase32_2Levels() { + String encoded = hackvertor.convert("<@base58><@base32>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base58><@base32>foobar", decoded); + } + + @Test + void testQuotedPrintableSingle() { + String encoded = hackvertor.convert("<@quoted_printable>hello=world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@quoted_printable>hello=world", decoded); + } + + @Test + void testQuotedPrintableToBase64_2Levels() { + String encoded = hackvertor.convert("<@quoted_printable><@base64>test", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@quoted_printable><@base64>test", decoded); + } + + @Test + void testHexEscapesToQuotedPrintable_2Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@quoted_printable>hello=world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@quoted_printable>hello=world", decoded); + } + + @Test + void testQuotedPrintableToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@quoted_printable><@base64><@base64>test", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@quoted_printable><@base64><@base64>test", decoded); + } + + @Test + void testUtf7Single() { + String encoded = hackvertor.convert("<@utf7>hello + world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@utf7>hello + world", decoded); + } + + @Test + void testUtf7ToBase64_2Levels() { + String encoded = hackvertor.convert("<@utf7><@base64>hello + world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@utf7><@base64>hello + world", decoded); + } + + @Test + void testHexEscapesToUtf7_2Levels() { + String encoded = hackvertor.convert("<@hex_escapes><@utf7>hello + world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@utf7>hello + world", decoded); + } + + @Test + void testUtf7ToBase64ToBase64_3Levels() { + String encoded = hackvertor.convert("<@utf7><@base64><@base64>hello + world", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@utf7><@base64><@base64>hello + world", decoded); + } + @Test void testBase64ToBase64ToBase64ToBase32_4Levels() { String encoded = hackvertor.convert("<@base64><@base64><@base64><@base32>foobar", hackvertor); @@ -474,4 +600,332 @@ void testUrlencodeToBase64ToBase64ToBase64_4Levels() { String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); assertEquals("<@urlencode_not_plus><@base64><@base64><@base64>foobar", decoded); } + + @Test + void testBase64XssPayload() { + String xss = ""; + String encoded = hackvertor.convert("<@base64>" + xss + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + xss + "", decoded); + } + + @Test + void testUrlencodeToBase64XssPayload() { + String xss = ""; + String encoded = hackvertor.convert("<@urlencode_all><@base64>" + xss + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64>" + xss + "", decoded); + } + + @Test + void testBase64SqlInjection() { + String sqli = "' OR 1=1--"; + String encoded = hackvertor.convert("<@base64>" + sqli + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + sqli + "", decoded); + } + + @Test + void testHexEscapesToBase64SqlInjection() { + String sqli = "'; DROP TABLE users;--"; + String encoded = hackvertor.convert("<@hex_escapes><@base64>" + sqli + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base64>" + sqli + "", decoded); + } + + @Test + void testBase64JsonPayload() { + String json = "{\"user\":\"admin\",\"role\":\"superuser\",\"token\":\"abc123\"}"; + String encoded = hackvertor.convert("<@base64>" + json + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + json + "", decoded); + } + + @Test + void testGzipToBase64JsonPayload() { + String json = "{\"username\":\"admin\",\"password\":\"secret123\",\"remember\":true}"; + String encoded = hackvertor.convert("<@gzip_compress><@base64>" + json + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base64>" + json + "", decoded); + } + + @Test + void testBase64UrlPath() { + String url = "https://example.com/api/users?id=1&action=delete"; + String encoded = hackvertor.convert("<@base64>" + url + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + url + "", decoded); + } + + @Test + void testUrlencodeToBase64UrlPath() { + String url = "https://target.com/admin/config.php?debug=true"; + String encoded = hackvertor.convert("<@urlencode_all><@base64>" + url + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64>" + url + "", decoded); + } + + @Test + void testBase64CommandInjection() { + String cmd = "; cat /etc/passwd"; + String encoded = hackvertor.convert("<@base64>" + cmd + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + cmd + "", decoded); + } + + @Test + void testUnicodeEscapesToBase64CommandInjection() { + String cmd = "| ls -la /var/www/html"; + String encoded = hackvertor.convert("<@unicode_escapes><@base64>" + cmd + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@unicode_escapes><@base64>" + cmd + "", decoded); + } + + @Test + void testBase64LdapInjection() { + String ldap = "*)(&(objectClass=*)(uid=admin))"; + String encoded = hackvertor.convert("<@base64>" + ldap + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + ldap + "", decoded); + } + + @Test + void testBase64XpathInjection() { + String xpath = "' or '1'='1"; + String encoded = hackvertor.convert("<@base64>" + xpath + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + xpath + "", decoded); + } + + @Test + void testBase64SstiPayload() { + String ssti = "{{constructor.constructor('return this')().process.mainModule.require('child_process').execSync('id')}}"; + String encoded = hackvertor.convert("<@base64>" + ssti + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + ssti + "", decoded); + } + + @Test + void testHexEscapesToBase64SstiPayload() { + String ssti = "${7*7}"; + String encoded = hackvertor.convert("<@hex_escapes><@base64>" + ssti + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base64>" + ssti + "", decoded); + } + + @Test + void testBase64XxePayload() { + String xxe = "]>&xxe;"; + String encoded = hackvertor.convert("<@base64>" + xxe + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@html_entities>" + xxe + "", decoded); + } + + @Test + void testDeflateToBase64XxePayload() { + String xxe = "%remote;]>"; + String encoded = hackvertor.convert("<@deflate_compress><@base64>" + xxe + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@deflate_compress><@base64>" + xxe + "", decoded); + } + + @Test + void testBase64HttpHeader() { + String header = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + String encoded = hackvertor.convert("<@base64>" + header + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + header + "", decoded); + } + + @Test + void testUrlencodeToBase64Cookie() { + String cookie = "session=abc123; admin=true; Path=/; HttpOnly"; + String encoded = hackvertor.convert("<@urlencode_all><@base64>" + cookie + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64>" + cookie + "", decoded); + } + + @Test + void testBase64CsrfToken() { + String csrf = "csrf_token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"; + String encoded = hackvertor.convert("<@base64>" + csrf + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + csrf + "", decoded); + } + + @Test + void testBase64HtmlInjection() { + String html = ""; + String encoded = hackvertor.convert("<@base64>" + html + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + html + "", decoded); + } + + @Test + void testHexEntitiesToBase64HtmlInjection() { + String html = ""; + String encoded = hackvertor.convert("<@hex_entities><@base64>" + html + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_entities><@base64>" + html + "", decoded); + } + + @Test + void testBase64OpenRedirect() { + String redirect = "//evil.com/phish?target=https://bank.com"; + String encoded = hackvertor.convert("<@base64>" + redirect + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + redirect + "", decoded); + } + + @Test + void testUrlencodeToBase64Ssrf() { + String ssrf = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"; + String encoded = hackvertor.convert("<@urlencode_all><@base64>" + ssrf + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64>" + ssrf + "", decoded); + } + + @Test + void testBase64PathTraversal() { + String path = "../../../etc/passwd"; + String encoded = hackvertor.convert("<@base64>" + path + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + path + "", decoded); + } + + @Test + void testOctalEscapesToBase64PathTraversal() { + String path = "....//....//....//etc/shadow"; + String encoded = hackvertor.convert("<@octal_escapes><@base64>" + path + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@octal_escapes><@base64>" + path + "", decoded); + } + + @Test + void testBase64NoSqlInjection() { + String nosql = "{\"$gt\":\"\"}"; + String encoded = hackvertor.convert("<@base64>" + nosql + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + nosql + "", decoded); + } + + @Test + void testGzipToBase64NoSqlInjection() { + String nosql = "{\"username\":{\"$ne\":null},\"password\":{\"$ne\":null}}"; + String encoded = hackvertor.convert("<@gzip_compress><@base64>" + nosql + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base64>" + nosql + "", decoded); + } + + @Test + void testBase64GraphqlInjection() { + String graphql = "{__schema{types{name,fields{name}}}}"; + String encoded = hackvertor.convert("<@base64>" + graphql + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + graphql + "", decoded); + } + + @Test + void testBase64JwtPayload() { + String jwtPayload = "{\"sub\":\"admin\",\"iat\":1516239022,\"exp\":9999999999,\"role\":\"admin\"}"; + String encoded = hackvertor.convert("<@base64>" + jwtPayload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + jwtPayload + "", decoded); + } + + @Test + void testBase64ToBase64XssInJson() { + String payload = "{\"name\":\"\"}"; + String encoded = hackvertor.convert("<@base64><@base64>" + payload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64><@base64>" + payload + "", decoded); + } + + @Test + void testUrlencodeToBase64ToBase64DeepNested() { + String payload = "{\"redirect\":\"javascript:alert(origin)\"}"; + String encoded = hackvertor.convert("<@urlencode_all><@base64><@base64>" + payload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@urlencode_not_plus><@base64><@base64>" + payload + "", decoded); + } + + @Test + void testHexEscapesToBase64ToBase64SqlUnion() { + String sqli = "' UNION SELECT username,password FROM users--"; + String encoded = hackvertor.convert("<@hex_escapes><@base64><@base64>" + sqli + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@hex_escapes><@base64><@base64>" + sqli + "", decoded); + } + + @Test + void testBase64AwsCredentials() { + String creds = "aws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"; + String encoded = hackvertor.convert("<@base64>" + creds + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + creds + "", decoded); + } + + @Test + void testGzipToBase64LargePayload() { + String payload = "SELECT * FROM users WHERE id=1; SELECT * FROM admin_users; SELECT * FROM secrets; SELECT * FROM api_keys; SELECT * FROM sessions WHERE active=1;"; + String encoded = hackvertor.convert("<@gzip_compress><@base64>" + payload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@gzip_compress><@base64>" + payload + "", decoded); + } + + @Test + void testBase64PrototypePollution() { + String payload = "{\"__proto__\":{\"admin\":true}}"; + String encoded = hackvertor.convert("<@base64>" + payload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + payload + "", decoded); + } + + @Test + void testCssEscapesToBase64PrototypePollution() { + String payload = "{\"constructor\":{\"prototype\":{\"isAdmin\":true}}}"; + String encoded = hackvertor.convert("<@css_escapes><@base64>" + payload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@css_escapes><@base64>" + payload + "", decoded); + } + + @Test + void testBase64DeserializationPayload() { + String payload = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcA=="; + String encoded = hackvertor.convert("<@base64>" + payload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + payload + "", decoded); + } + + @Test + void testBase64RegexDos() { + String payload = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"; + String encoded = hackvertor.convert("<@base64>" + payload + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + payload + "", decoded); + } + + @Test + void testBase32ToGzipXmlPayload() { + String xml = "adminpassword123"; + String encoded = hackvertor.convert("<@base32><@gzip_compress>" + xml + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base32><@gzip_compress>" + xml + "", decoded); + } + + @Test + void testBase64HostHeaderInjection() { + String header = "Host: evil.com\r\nX-Forwarded-Host: attacker.com"; + String encoded = hackvertor.convert("<@base64>" + header + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64>" + header + "", decoded); + } + + @Test + void testUnicodeEscapesToBase64CrlfInjection() { + String crlf = "header\r\nSet-Cookie: session=hijacked"; + String encoded = hackvertor.convert("<@unicode_escapes><@base64>" + crlf + "", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@unicode_escapes><@base64>" + crlf + "", decoded); + } } diff --git a/src/test/java/burp/ConvertorTests.java b/src/test/java/burp/ConvertorTests.java index 379d104..1954061 100644 --- a/src/test/java/burp/ConvertorTests.java +++ b/src/test/java/burp/ConvertorTests.java @@ -582,4 +582,17 @@ void testDeflateCompressStoreBase32Output() { String encoded = hackvertor.convert("<@base32><@deflate_compress('store')>foobar", hackvertor); assertEquals("PAAQCBQA7H7WM33PMJQXECFLAJ5A====", encoded); } + + @Test + void testBase64urlGzipOutput() { + String encoded = hackvertor.convert("<@base64url><@gzip_compress>foobar", hackvertor); + assertTrue(encoded.matches("^[A-Za-z0-9_-]+$"), "base64url output should only contain base64url characters, got: " + encoded); + } + + @Test + void testAutoDecodeBase64urlGzip() { + String encoded = hackvertor.convert("<@base64url><@gzip_compress>foobar", hackvertor); + String decoded = hackvertor.convert("<@auto_decode_no_decrypt>" + encoded + "", hackvertor); + assertEquals("<@base64url><@gzip_compress>foobar", decoded); + } } \ No newline at end of file diff --git a/src/test/java/burp/stubs/StubCallbacks.java b/src/test/java/burp/stubs/StubCallbacks.java index b96d420..b6d20a0 100644 --- a/src/test/java/burp/stubs/StubCallbacks.java +++ b/src/test/java/burp/stubs/StubCallbacks.java @@ -99,7 +99,7 @@ public byte[] base64Decode(byte[] bytes) { @Override public String base64Encode(String s) { - return Base64.getEncoder().encodeToString(s.getBytes()); + return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.ISO_8859_1)); } @Override diff --git a/src/test/java/burp/stubs/StubExtensionHelpers.java b/src/test/java/burp/stubs/StubExtensionHelpers.java index 536ebf8..cf5488f 100644 --- a/src/test/java/burp/stubs/StubExtensionHelpers.java +++ b/src/test/java/burp/stubs/StubExtensionHelpers.java @@ -64,7 +64,7 @@ public byte[] base64Decode(byte[] data) { @Override public String base64Encode(String data) { - return Base64.getEncoder().encodeToString(data.getBytes()); + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.ISO_8859_1)); } @Override From 905bf90990e55913b53ccf3131610190d6af5130 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Wed, 26 Nov 2025 21:17:35 +0000 Subject: [PATCH 16/24] Prevented dangerous categories from being shown in the MultiEncoderWindow. Added checkboxes to enable them. Made the window persist state in the project file when closed. --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 204 +++++++++++++++++- 2 files changed, 199 insertions(+), 7 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 390cc2a..06ede41 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.27"; + public static String version = "v2.2.28"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index 89a2109..ede5c1d 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -9,8 +9,12 @@ import burp.hv.Convertors; import burp.hv.HackvertorExtension; import burp.hv.tags.Tag; +import org.json.JSONArray; +import org.json.JSONObject; import javax.swing.*; +import java.util.EnumSet; +import java.util.Set; import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.datatransfer.Clipboard; @@ -35,6 +39,12 @@ public class MultiEncoderWindow { private static final int MAX_VARIANTS_TOTAL = 10000; private static final int MAX_TAGS_PER_LAYER = 50; private static final int CONVERT_TIMEOUT_SECONDS = 20; + private static final Set DANGEROUS_CATEGORIES = EnumSet.of( + Tag.Category.Custom, + Tag.Category.System, + Tag.Category.Languages + ); + private static final String PERSISTENCE_KEY = "multiEncoderState"; private final MontoyaApi montoyaApi; private final String selectedText; @@ -50,6 +60,8 @@ public class MultiEncoderWindow { private int layerCounter = 1; private JLabel statusLabel; private Timer statusClearTimer; + private final Set enabledDangerousCategories = EnumSet.noneOf(Tag.Category.class); + private final Map dangerousCategoryCheckboxes = new HashMap<>(); private class Layer { final Map tagCheckboxes; @@ -57,15 +69,14 @@ private class Layer { final JPanel tagsPanel; final JTextField searchField; final JCheckBox selectAllCheckbox; - final Runnable updateTags; + Runnable updateTags; - Layer(JPanel tagsPanel, JTextField searchField, JCheckBox selectAllCheckbox, Runnable updateTags) { + Layer(JPanel tagsPanel, JTextField searchField, JCheckBox selectAllCheckbox) { this.tagCheckboxes = new HashMap<>(); this.selectedTags = new ArrayList<>(); this.tagsPanel = tagsPanel; this.searchField = searchField; this.selectAllCheckbox = selectAllCheckbox; - this.updateTags = updateTags; } } @@ -141,7 +152,40 @@ public void show() { layerButtonPanel.add(removeLayerButton); montoyaApi.userInterface().applyThemeToComponent(layerButtonPanel); - topPanel.add(layerButtonPanel, BorderLayout.NORTH); + loadState(); + + JPanel dangerousCategoriesPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JLabel dangerousLabel = new JLabel("Enable dangerous categories:"); + dangerousLabel.setFont(new Font("Inter", Font.PLAIN, 12)); + montoyaApi.userInterface().applyThemeToComponent(dangerousLabel); + dangerousCategoriesPanel.add(dangerousLabel); + + for (Tag.Category category : DANGEROUS_CATEGORIES) { + JCheckBox categoryCheckbox = new JCheckBox(category.name()); + categoryCheckbox.setFont(new Font("Inter", Font.PLAIN, 12)); + categoryCheckbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); + categoryCheckbox.setToolTipText("Enable " + category.name() + " tags (may execute code)"); + categoryCheckbox.setSelected(enabledDangerousCategories.contains(category)); + categoryCheckbox.addActionListener(e -> { + if (categoryCheckbox.isSelected()) { + enabledDangerousCategories.add(category); + } else { + enabledDangerousCategories.remove(category); + } + refreshAllLayers(); + }); + montoyaApi.userInterface().applyThemeToComponent(categoryCheckbox); + dangerousCategoriesPanel.add(categoryCheckbox); + dangerousCategoryCheckboxes.put(category, categoryCheckbox); + } + montoyaApi.userInterface().applyThemeToComponent(dangerousCategoriesPanel); + + JPanel layerControlPanel = new JPanel(new BorderLayout()); + layerControlPanel.add(layerButtonPanel, BorderLayout.WEST); + layerControlPanel.add(dangerousCategoriesPanel, BorderLayout.EAST); + montoyaApi.userInterface().applyThemeToComponent(layerControlPanel); + + topPanel.add(layerControlPanel, BorderLayout.NORTH); topPanel.add(layerTabbedPane, BorderLayout.CENTER); JPanel previewPanel = new JPanel(new BorderLayout()); @@ -236,6 +280,7 @@ public void show() { window.addWindowFocusListener(new java.awt.event.WindowAdapter() { @Override public void windowLostFocus(java.awt.event.WindowEvent e) { + saveState(); window.dispose(); } }); @@ -260,7 +305,15 @@ public void windowLostFocus(java.awt.event.WindowEvent e) { montoyaApi.userInterface().applyThemeToComponent(window.getContentPane()); window.setLocationRelativeTo(montoyaApi.userInterface().swingUtils().suiteFrame()); - addLayer(); + int savedLayerCount = getSavedLayerCount(); + int layersToCreate = Math.max(1, savedLayerCount); + for (int i = 0; i < layersToCreate; i++) { + addLayer(); + if (i < savedLayerCount) { + loadLayerState(layers.get(i), i); + } + } + updatePreview(); window.setVisible(true); if (!layers.isEmpty()) { @@ -310,12 +363,16 @@ private void addLayer() { layerPanel.add(searchAndSelectAllPanel, BorderLayout.NORTH); layerPanel.add(tagsScrollPane, BorderLayout.CENTER); - Layer layer = new Layer(tagsPanel, searchField, selectAllCheckbox, null); + Layer layer = new Layer(tagsPanel, searchField, selectAllCheckbox); Function> filterTags = searchText -> { ArrayList filtered = new ArrayList<>(); String lowerSearch = searchText.toLowerCase(); for (Tag tag : tags) { + if (DANGEROUS_CATEGORIES.contains(tag.category) && + !enabledDangerousCategories.contains(tag.category)) { + continue; + } if (lowerSearch.isEmpty() || tag.name.toLowerCase().contains(lowerSearch) || tag.category.toString().toLowerCase().contains(lowerSearch) || @@ -393,6 +450,8 @@ private void addLayer() { tagsPanel.repaint(); }; + layer.updateTags = updateTags; + java.awt.event.ActionListener selectAllListener = new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { @@ -470,6 +529,17 @@ private void removeCurrentLayer() { } } + private void refreshAllLayers() { + for (Layer layer : layers) { + layer.selectedTags.removeIf(tag -> + DANGEROUS_CATEGORIES.contains(tag.category) && + !enabledDangerousCategories.contains(tag.category) + ); + layer.updateTags.run(); + } + updatePreview(); + } + private ArrayList> getAllLayerTags() { ArrayList> allLayerTags = new ArrayList<>(); for (Layer layer : layers) { @@ -767,4 +837,126 @@ private void sendToIntruder() { JOptionPane.showMessageDialog(window, "Sent to Intruder. " + variants.size() + " payload(s) copied to clipboard.", "Success", JOptionPane.INFORMATION_MESSAGE); window.dispose(); } + + private void saveState() { + try { + JSONObject state = new JSONObject(); + + JSONArray enabledCategories = new JSONArray(); + for (Tag.Category category : enabledDangerousCategories) { + enabledCategories.put(category.name()); + } + state.put("enabledDangerousCategories", enabledCategories); + + JSONArray layersArray = new JSONArray(); + for (Layer layer : layers) { + JSONObject layerObj = new JSONObject(); + layerObj.put("searchText", layer.searchField.getText()); + + JSONArray selectedTagNames = new JSONArray(); + for (Tag tag : layer.selectedTags) { + selectedTagNames.put(tag.name); + } + layerObj.put("selectedTags", selectedTagNames); + + layersArray.put(layerObj); + } + state.put("layers", layersArray); + + montoyaApi.persistence().extensionData().setString(PERSISTENCE_KEY, state.toString()); + } catch (Exception e) { + System.err.println("Failed to save MultiEncoder state: " + e.getMessage()); + } + } + + private void loadState() { + try { + String content = montoyaApi.persistence().extensionData().getString(PERSISTENCE_KEY); + if (content == null || content.isEmpty()) { + return; + } + + JSONObject state = new JSONObject(content); + + if (state.has("enabledDangerousCategories")) { + JSONArray enabledCategories = state.getJSONArray("enabledDangerousCategories"); + for (int i = 0; i < enabledCategories.length(); i++) { + String categoryName = enabledCategories.getString(i); + try { + Tag.Category category = Tag.Category.valueOf(categoryName); + if (DANGEROUS_CATEGORIES.contains(category)) { + enabledDangerousCategories.add(category); + } + } catch (IllegalArgumentException ignored) { + } + } + } + } catch (Exception e) { + System.err.println("Failed to load MultiEncoder state: " + e.getMessage()); + } + } + + private void loadLayerState(Layer layer, int layerIndex) { + try { + String content = montoyaApi.persistence().extensionData().getString(PERSISTENCE_KEY); + if (content == null || content.isEmpty()) { + return; + } + + JSONObject state = new JSONObject(content); + if (!state.has("layers")) { + return; + } + + JSONArray layersArray = state.getJSONArray("layers"); + if (layerIndex >= layersArray.length()) { + return; + } + + JSONObject layerObj = layersArray.getJSONObject(layerIndex); + + if (layerObj.has("searchText")) { + layer.searchField.setText(layerObj.getString("searchText")); + } + + if (layerObj.has("selectedTags")) { + JSONArray selectedTagNames = layerObj.getJSONArray("selectedTags"); + for (int i = 0; i < selectedTagNames.length(); i++) { + String tagName = selectedTagNames.getString(i); + JCheckBox checkbox = layer.tagCheckboxes.get(tagName); + if (checkbox != null) { + checkbox.setSelected(true); + for (Tag tag : tags) { + if (tag.name.equals(tagName)) { + if (!layer.selectedTags.contains(tag)) { + layer.selectedTags.add(tag); + } + break; + } + } + } + } + } + } catch (Exception e) { + System.err.println("Failed to load layer state: " + e.getMessage()); + } + } + + private int getSavedLayerCount() { + try { + String content = montoyaApi.persistence().extensionData().getString(PERSISTENCE_KEY); + if (content == null || content.isEmpty()) { + return 0; + } + + JSONObject state = new JSONObject(content); + if (!state.has("layers")) { + return 0; + } + + return state.getJSONArray("layers").length(); + } catch (Exception e) { + return 0; + } + } } From c35cab7220f1c2761bd8ee7dd30a262d1d5e7596 Mon Sep 17 00:00:00 2001 From: floyd Date: Thu, 27 Nov 2025 09:56:44 +0100 Subject: [PATCH 17/24] Add zipfile tag to tag store --- tag-store/tag-store.json | 13 ++++ tag-store/zipfile/zipfile_.py | 118 ++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 tag-store/zipfile/zipfile_.py diff --git a/tag-store/tag-store.json b/tag-store/tag-store.json index 66b9645..97b4535 100644 --- a/tag-store/tag-store.json +++ b/tag-store/tag-store.json @@ -46,6 +46,19 @@ "numberOfArgs": 0, "language": "Python" }, + { + "tagName": "zipfile", + "description": "Create a zip file on the fly with one file in it, file name as argument 1, file content as input (raw by default, optionally in base64 - set second argument to 1)", + "author": "pentagridsec", + "numberOfArgs": 2, + "argument1": "filename", + "argument1Type": "String", + "argument1Default": "test.txt", + "argument2": "isBase64", + "argument2Type": "Number", + "argument2Default": "0", + "language": "Python" + }, { "tagName": "ip", "description": "Generates a random IP", diff --git a/tag-store/zipfile/zipfile_.py b/tag-store/zipfile/zipfile_.py new file mode 100644 index 0000000..885ff9d --- /dev/null +++ b/tag-store/zipfile/zipfile_.py @@ -0,0 +1,118 @@ +# Pentagrid AG + +import zipfile +import time + +# argument 1, filename, default "test.png" +try: + filename_from_argument = filename +except NameError: + filename_from_argument = "test.txt" +# argument 2, isBase64, default 0 +try: + is_base64_encoded = isBase64 +except NameError: + is_base64_encoded = 0 +# input (between tags), default empty string +file_content = input +# input is actually a built-in function of Python, so it's always defined... +if callable(file_content): + file_content = "This is the test file content" + +file_content = file_content.encode("utf-8") + +if is_base64_encoded: + file_content = file_content.decode("base64") + + +# Create zip file, using in-memory memory_buffer +# memory_buffer = StringIO.StringIO() +# As the above leads to unicode errors, try something else +JYTHON=False +PYTHON3=False +PYTHON2=False +try: + # Jython: use Java byte array output stream + from java.io import ByteArrayOutputStream + class MemoryBuffer(object): + def __init__(self): + self._buf = ByteArrayOutputStream() + self._pos = 0 + + def write(self, b): + if isinstance(b, unicode): + b = b.encode('utf-8') + if isinstance(b, bytearray): + b = bytes(b) + self._buf.write(b) # always calls write(byte[]) + self._pos = len(self._buf.toByteArray()) + + def tell(self): + return self._pos + + def seek(self, offset, whence=0): + # We don't support random-access writes, but ZipFile only uses seek(0) + if whence == 0 and offset == 0: + self._pos = 0 + else: + # Not required for writing ZIPs + pass + + def getvalue(self): + return self._buf.toByteArray() + + # ZipFile checks for "read" in some cases; safe to implement + def read(self, n=-1): + data = self._buf.toByteArray() + if n == -1: + return data + return data[:n] + + def flush(self): + pass + + memory_buffer = MemoryBuffer() + JYTHON=True +except ImportError: + # CPython / Python 3 + try: + # Python 3 + import io + memory_buffer = io.BytesIO() + PYTHON3=True + except ImportError: + # Python 2.7 + import cStringIO + memory_buffer = cStringIO.StringIO + PYTHON2=True + + +zf = zipfile.ZipFile(memory_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + +# Create empty directories if the file should be in directories +if '/' in filename_from_argument: + splitted = filename_from_argument.split("/") + directories = splitted[:-1] + parents = "" + for directory in directories: + dir_info = zipfile.ZipInfo(parents + directory + "/") + parents += directory + "/" + dir_info.date_time = time.localtime(time.time())[:6] + dir_info.external_attr = 0o755 << 16 + zf.writestr(dir_info, '') + +# Put file in zip file +info = zipfile.ZipInfo(filename_from_argument) +info.date_time = time.localtime(time.time())[:6] +info.external_attr = 0o777 << 16 +zf.writestr(info, file_content) + +zf.close() + + +if JYTHON: + output = ''.join(map(lambda x: chr(x % 256), memory_buffer.getvalue())) +else: + output = memory_buffer.getvalue() + +print(repr(output)) \ No newline at end of file From af946cadac8f4a19f85019a321aafdfc1f659e32 Mon Sep 17 00:00:00 2001 From: floyd Date: Thu, 27 Nov 2025 10:12:11 +0100 Subject: [PATCH 18/24] Renamed to zipfile.py and introduced a cursed hack for it to work --- tag-store/zipfile/{zipfile_.py => zipfile.py} | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) rename tag-store/zipfile/{zipfile_.py => zipfile.py} (86%) diff --git a/tag-store/zipfile/zipfile_.py b/tag-store/zipfile/zipfile.py similarity index 86% rename from tag-store/zipfile/zipfile_.py rename to tag-store/zipfile/zipfile.py index 885ff9d..732672f 100644 --- a/tag-store/zipfile/zipfile_.py +++ b/tag-store/zipfile/zipfile.py @@ -1,6 +1,13 @@ -# Pentagrid AG +# Pentagrid AG, pentagridsec + +# Work around the problem that zipfile is the name of the standard library but also this file +# Attention: Cursed Python ahead. +import sys +local = sys.path.pop(0) +import zipfile as zipfileLibrary +sys.path.insert(0, local) + -import zipfile import time # argument 1, filename, default "test.png" @@ -87,7 +94,7 @@ def flush(self): PYTHON2=True -zf = zipfile.ZipFile(memory_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) +zf = zipfileLibrary.ZipFile(memory_buffer, mode='w', compression=zipfileLibrary.ZIP_DEFLATED) # Create empty directories if the file should be in directories if '/' in filename_from_argument: @@ -95,14 +102,14 @@ def flush(self): directories = splitted[:-1] parents = "" for directory in directories: - dir_info = zipfile.ZipInfo(parents + directory + "/") + dir_info = zipfileLibrary.ZipInfo(parents + directory + "/") parents += directory + "/" dir_info.date_time = time.localtime(time.time())[:6] dir_info.external_attr = 0o755 << 16 zf.writestr(dir_info, '') # Put file in zip file -info = zipfile.ZipInfo(filename_from_argument) +info = zipfileLibrary.ZipInfo(filename_from_argument) info.date_time = time.localtime(time.time())[:6] info.external_attr = 0o777 << 16 zf.writestr(info, file_content) From 7facb2208b2cd08566f15dcad286f3479c19b3bf Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Thu, 27 Nov 2025 12:44:01 +0000 Subject: [PATCH 19/24] Added the first layer tag as the name of the Repeater tab --- src/main/java/burp/hv/HackvertorExtension.java | 2 +- src/main/java/burp/hv/ui/MultiEncoderWindow.java | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 06ede41..f817367 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.28"; + public static String version = "v2.2.29"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index ede5c1d..be4a8a7 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -781,13 +781,16 @@ private void sendToRepeater() { ArrayList variants = generateAllVariants(selectedText, shouldConvert); String modePrefix = shouldConvert ? "HV-" : "HVT-"; - int variantNum = 1; - for (String variant : variants) { + ArrayList layer1Tags = allLayerTags.get(0); + int variantsPerLayer1Tag = variants.size() / layer1Tags.size(); + for (int i = 0; i < variants.size(); i++) { + String variant = variants.get(i); String modifiedRequestStr = requestStr.replace(selectedText, variant); HttpRequest modifiedRequest = HttpRequest.httpRequest(modifiedRequestStr); - String tabName = modePrefix + "V" + variantNum + "-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); + int layer1TagIndex = i / variantsPerLayer1Tag; + String layer1TagName = layer1Tags.get(layer1TagIndex).name; + String tabName = modePrefix + layer1TagName + "-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); montoyaApi.repeater().sendToRepeater(modifiedRequest, tabName); - variantNum++; } showInfoMessage("Sent " + variants.size() + " variant(s) to Repeater."); From 70a6c31eb996cfa2625fd7ffdf6f32213545b560 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Thu, 27 Nov 2025 12:50:10 +0000 Subject: [PATCH 20/24] Added filter to output preview --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 83 ++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index f817367..cb36b05 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.29"; + public static String version = "v2.2.30"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index be4a8a7..11439b5 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -13,6 +13,7 @@ import org.json.JSONObject; import javax.swing.*; +import javax.swing.text.*; import java.util.EnumSet; import java.util.Set; import javax.swing.border.EmptyBorder; @@ -53,7 +54,9 @@ public class MultiEncoderWindow { private final HttpRequestResponse baseRequestResponse; private final ArrayList layers; private final Consumer hackvertorCallback; - private JTextArea previewArea; + private JTextPane previewArea; + private JTextField previewSearchField; + private String lastPreviewContent = ""; private JWindow window; private JComboBox modeComboBox; private JTabbedPane layerTabbedPane; @@ -191,7 +194,25 @@ public void show() { JPanel previewPanel = new JPanel(new BorderLayout()); previewPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0)); - previewArea = new JTextArea(); + JPanel previewSearchPanel = new JPanel(new BorderLayout(5, 0)); + previewSearchPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0)); + JLabel previewSearchLabel = new JLabel("Filter:"); + previewSearchLabel.setFont(new Font("Inter", Font.PLAIN, 12)); + montoyaApi.userInterface().applyThemeToComponent(previewSearchLabel); + previewSearchField = new JTextField(); + previewSearchField.setFont(new Font("Monospaced", Font.PLAIN, 12)); + previewSearchField.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + applyPreviewFilter(); + } + }); + montoyaApi.userInterface().applyThemeToComponent(previewSearchField); + previewSearchPanel.add(previewSearchLabel, BorderLayout.WEST); + previewSearchPanel.add(previewSearchField, BorderLayout.CENTER); + montoyaApi.userInterface().applyThemeToComponent(previewSearchPanel); + + previewArea = new JTextPane(); previewArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); previewArea.setEditable(false); montoyaApi.userInterface().applyThemeToComponent(previewArea); @@ -201,6 +222,7 @@ public void show() { previewScrollPane.setBorder(BorderFactory.createTitledBorder("Preview")); montoyaApi.userInterface().applyThemeToComponent(previewScrollPane); + previewPanel.add(previewSearchPanel, BorderLayout.NORTH); previewPanel.add(previewScrollPane, BorderLayout.CENTER); statusLabel = new JLabel(" "); @@ -240,6 +262,8 @@ public void show() { layer.selectAllCheckbox.setSelected(false); } previewArea.setText(""); + lastPreviewContent = ""; + previewSearchField.setText(""); }); montoyaApi.userInterface().applyThemeToComponent(clearButton); @@ -700,7 +724,8 @@ private void copyToClipboard() { private void updatePreview() { ArrayList> allLayerTags = getAllLayerTags(); if (allLayerTags.isEmpty()) { - previewArea.setText("No tags selected. Please select at least one tag in any layer."); + lastPreviewContent = "No tags selected. Please select at least one tag in any layer."; + previewArea.setText(lastPreviewContent); return; } @@ -737,7 +762,57 @@ private void updatePreview() { .append(" more variants not shown ...\n"); } - previewArea.setText(preview.toString()); + lastPreviewContent = preview.toString(); + applyPreviewFilter(); + } + + private void applyPreviewFilter() { + String filterText = previewSearchField.getText().toLowerCase(); + StyledDocument doc = previewArea.getStyledDocument(); + + Style defaultStyle = previewArea.addStyle("default", null); + StyleConstants.setBackground(defaultStyle, previewArea.getBackground()); + + Style highlightStyle = previewArea.addStyle("highlight", null); + StyleConstants.setBackground(highlightStyle, new Color(255, 255, 0)); + StyleConstants.setForeground(highlightStyle, Color.BLACK); + + if (filterText.isEmpty()) { + previewArea.setText(lastPreviewContent); + previewArea.setCaretPosition(0); + return; + } + + String[] lines = lastPreviewContent.split("\n"); + StringBuilder filteredContent = new StringBuilder(); + ArrayList highlights = new ArrayList<>(); + + for (String line : lines) { + if (line.toLowerCase().contains(filterText)) { + int lineStart = filteredContent.length(); + filteredContent.append(line).append("\n"); + + String lowerLine = line.toLowerCase(); + int searchStart = 0; + int idx; + while ((idx = lowerLine.indexOf(filterText, searchStart)) != -1) { + highlights.add(new int[]{lineStart + idx, filterText.length()}); + searchStart = idx + 1; + } + } + } + + if (filteredContent.isEmpty()) { + previewArea.setText("No matches found for: " + previewSearchField.getText()); + return; + } + + previewArea.setText(filteredContent.toString()); + + for (int[] highlight : highlights) { + doc.setCharacterAttributes(highlight[0], highlight[1], highlightStyle, false); + } + previewArea.setCaretPosition(0); } From 0701270fd1f3a78e526ce349b8cb7c95b0b37678 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Thu, 27 Nov 2025 18:03:06 +0000 Subject: [PATCH 21/24] Fixed flakey tests and persistence problems when Montoya isn't available in a test. --- src/main/java/burp/hv/HackvertorExtension.java | 2 +- src/main/java/burp/hv/ui/ExtensionPanel.java | 3 +++ src/test/java/burp/ui/HackvertorUiTest.java | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index cb36b05..05ace72 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.30"; + public static String version = "v2.2.31"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/ExtensionPanel.java b/src/main/java/burp/hv/ui/ExtensionPanel.java index 6653cd8..9f3815a 100644 --- a/src/main/java/burp/hv/ui/ExtensionPanel.java +++ b/src/main/java/burp/hv/ui/ExtensionPanel.java @@ -151,6 +151,9 @@ public void saveState() { } public void restoreState() { + if(montoyaApi == null) { + return; + } try { String savedState = montoyaApi.persistence().extensionData().getString("extensionPanelState"); if (savedState == null || savedState.isEmpty()) { diff --git a/src/test/java/burp/ui/HackvertorUiTest.java b/src/test/java/burp/ui/HackvertorUiTest.java index d415db2..4b22049 100644 --- a/src/test/java/burp/ui/HackvertorUiTest.java +++ b/src/test/java/burp/ui/HackvertorUiTest.java @@ -1559,7 +1559,7 @@ void testAutoDecodeDeflateBase32Ui() throws Exception { GuiActionRunner.execute(() -> finalInputArea.setText("<@deflate_compress><@base32>foobar")); window.robot().waitForIdle(); - Thread.sleep(300); + Thread.sleep(500); window.robot().waitForIdle(); String encodedOutput = outputArea.getText(); @@ -1568,7 +1568,7 @@ void testAutoDecodeDeflateBase32Ui() throws Exception { GuiActionRunner.execute(() -> finalInputArea.setText("<@auto_decode_no_decrypt>" + finalOutputArea.getText() + "")); window.robot().waitForIdle(); - Thread.sleep(300); + Thread.sleep(500); window.robot().waitForIdle(); String decodedOutput = outputArea.getText(); From 22466223c539fea8c4baba4b1acabb13fb6004eb Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Thu, 27 Nov 2025 20:06:59 +0000 Subject: [PATCH 22/24] Made the filter using the tag name. Changed dimensions. --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 49 +++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index 05ace72..d57dbf7 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.31"; + public static String version = "v2.2.32"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index 11439b5..ebf2d4d 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -32,10 +32,10 @@ import java.util.function.Function; public class MultiEncoderWindow { - private static final int DEFAULT_WIDTH = 900; - private static final int DEFAULT_HEIGHT = 600; + private static final int DEFAULT_WIDTH = 1000; + private static final int DEFAULT_HEIGHT = 750; private static final int CORNER_RADIUS = 20; - private static final int COLUMN_COUNT = 3; + private static final int COLUMN_COUNT = 4; private static final int MAX_VARIANTS_DISPLAY = 100; private static final int MAX_VARIANTS_TOTAL = 10000; private static final int MAX_TAGS_PER_LAYER = 50; @@ -710,15 +710,38 @@ private void copyToClipboard() { return; } boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - ArrayList variants = generateAllVariants(selectedText, shouldConvert); + ArrayList taggedVariants = generateAllVariants(selectedText, false); + ArrayList resultVariants = generateAllVariants(selectedText, shouldConvert); + String filterText = previewSearchField.getText().toLowerCase(); + + ArrayList variantsToCopy = new ArrayList<>(); + if (filterText.isEmpty()) { + variantsToCopy.addAll(resultVariants); + } else { + for (int i = 0; i < taggedVariants.size(); i++) { + if (taggedVariants.get(i).toLowerCase().contains(filterText)) { + variantsToCopy.add(resultVariants.get(i)); + } + } + } + + if (variantsToCopy.isEmpty()) { + showWarningMessage("No variants match the filter."); + return; + } + StringBuilder output = new StringBuilder(); - for (int i = 0; i < variants.size(); i++) { + for (int i = 0; i < variantsToCopy.size(); i++) { if (i > 0) output.append("\n"); - output.append(variants.get(i)); + output.append(variantsToCopy.get(i)); } StringSelection selection = new StringSelection(output.toString()); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(selection, null); + + if (!filterText.isEmpty()) { + showInfoMessage("Copied " + variantsToCopy.size() + " filtered variant(s)."); + } } private void updatePreview() { @@ -899,20 +922,6 @@ private void sendToIntruder() { HttpRequestTemplate intruderTemplate = HttpRequestTemplate.httpRequestTemplate(baseRequest, Collections.singletonList(insertionPoint)); String tabName = "HV-Layers-" + selectedText.substring(0, Math.min(selectedText.length(), 10)); montoyaApi.intruder().sendToIntruder(baseRequestResponse.request().httpService(), intruderTemplate, tabName); - - boolean shouldConvert = "Convert".equals(modeComboBox.getSelectedItem()); - ArrayList variants = generateAllVariants(selectedText, shouldConvert); - - StringBuilder payloadList = new StringBuilder(); - for (String variant : variants) { - payloadList.append(variant).append("\n"); - } - - StringSelection selection = new StringSelection(payloadList.toString()); - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - clipboard.setContents(selection, null); - - JOptionPane.showMessageDialog(window, "Sent to Intruder. " + variants.size() + " payload(s) copied to clipboard.", "Success", JOptionPane.INFORMATION_MESSAGE); window.dispose(); } From e3499b4dd77a11ec61d59574661426ca31366933 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Thu, 27 Nov 2025 20:46:06 +0000 Subject: [PATCH 23/24] Added category checkboxes and a message when all variants are copied, --- .../java/burp/hv/HackvertorExtension.java | 2 +- .../java/burp/hv/ui/MultiEncoderWindow.java | 68 ++++++++++++++++--- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/main/java/burp/hv/HackvertorExtension.java b/src/main/java/burp/hv/HackvertorExtension.java index d57dbf7..7dde64a 100644 --- a/src/main/java/burp/hv/HackvertorExtension.java +++ b/src/main/java/burp/hv/HackvertorExtension.java @@ -35,7 +35,7 @@ public class HackvertorExtension implements BurpExtension, IBurpExtender, ITab, IExtensionStateListener, IMessageEditorTabFactory { //TODO Unset on unload public static String extensionName = "Hackvertor"; - public static String version = "v2.2.32"; + public static String version = "v2.2.33"; public static JFrame HackvertorFrame = null; public static IBurpExtenderCallbacks callbacks; public static IExtensionHelpers helpers; diff --git a/src/main/java/burp/hv/ui/MultiEncoderWindow.java b/src/main/java/burp/hv/ui/MultiEncoderWindow.java index ebf2d4d..1567dff 100644 --- a/src/main/java/burp/hv/ui/MultiEncoderWindow.java +++ b/src/main/java/burp/hv/ui/MultiEncoderWindow.java @@ -64,7 +64,9 @@ public class MultiEncoderWindow { private JLabel statusLabel; private Timer statusClearTimer; private final Set enabledDangerousCategories = EnumSet.noneOf(Tag.Category.class); + private final Set enabledCategories = EnumSet.noneOf(Tag.Category.class); private final Map dangerousCategoryCheckboxes = new HashMap<>(); + private final Map categoryCheckboxes = new HashMap<>(); private class Layer { final Map tagCheckboxes; @@ -158,7 +160,7 @@ public void show() { loadState(); JPanel dangerousCategoriesPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - JLabel dangerousLabel = new JLabel("Enable dangerous categories:"); + JLabel dangerousLabel = new JLabel("Dangerous:"); dangerousLabel.setFont(new Font("Inter", Font.PLAIN, 12)); montoyaApi.userInterface().applyThemeToComponent(dangerousLabel); dangerousCategoriesPanel.add(dangerousLabel); @@ -183,9 +185,47 @@ public void show() { } montoyaApi.userInterface().applyThemeToComponent(dangerousCategoriesPanel); + JPanel categoriesInnerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); + for (Tag.Category category : Tag.Category.values()) { + if (DANGEROUS_CATEGORIES.contains(category)) { + continue; + } + enabledCategories.add(category); + JCheckBox categoryCheckbox = new JCheckBox(category.name()); + categoryCheckbox.setFont(new Font("Inter", Font.PLAIN, 12)); + categoryCheckbox.setCursor(new Cursor(Cursor.HAND_CURSOR)); + categoryCheckbox.setToolTipText("Show " + category.name() + " tags"); + categoryCheckbox.setSelected(true); + categoryCheckbox.addActionListener(e -> { + if (categoryCheckbox.isSelected()) { + enabledCategories.add(category); + } else { + enabledCategories.remove(category); + } + refreshAllLayers(); + }); + montoyaApi.userInterface().applyThemeToComponent(categoryCheckbox); + categoriesInnerPanel.add(categoryCheckbox); + categoryCheckboxes.put(category, categoryCheckbox); + } + montoyaApi.userInterface().applyThemeToComponent(categoriesInnerPanel); + + JScrollPane categoriesScrollPane = new JScrollPane(categoriesInnerPanel); + categoriesScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + categoriesScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + categoriesScrollPane.setBorder(BorderFactory.createTitledBorder("Categories")); + categoriesScrollPane.setPreferredSize(new Dimension(DEFAULT_WIDTH - 50, 60)); + categoriesScrollPane.getHorizontalScrollBar().setUnitIncrement(16); + montoyaApi.userInterface().applyThemeToComponent(categoriesScrollPane); + + JPanel allCategoriesPanel = new JPanel(new BorderLayout()); + allCategoriesPanel.add(categoriesScrollPane, BorderLayout.CENTER); + allCategoriesPanel.add(dangerousCategoriesPanel, BorderLayout.SOUTH); + montoyaApi.userInterface().applyThemeToComponent(allCategoriesPanel); + JPanel layerControlPanel = new JPanel(new BorderLayout()); layerControlPanel.add(layerButtonPanel, BorderLayout.WEST); - layerControlPanel.add(dangerousCategoriesPanel, BorderLayout.EAST); + layerControlPanel.add(allCategoriesPanel, BorderLayout.CENTER); montoyaApi.userInterface().applyThemeToComponent(layerControlPanel); topPanel.add(layerControlPanel, BorderLayout.NORTH); @@ -393,9 +433,14 @@ private void addLayer() { ArrayList filtered = new ArrayList<>(); String lowerSearch = searchText.toLowerCase(); for (Tag tag : tags) { - if (DANGEROUS_CATEGORIES.contains(tag.category) && - !enabledDangerousCategories.contains(tag.category)) { - continue; + if (DANGEROUS_CATEGORIES.contains(tag.category)) { + if (!enabledDangerousCategories.contains(tag.category)) { + continue; + } + } else { + if (!enabledCategories.contains(tag.category)) { + continue; + } } if (lowerSearch.isEmpty() || tag.name.toLowerCase().contains(lowerSearch) || @@ -555,10 +600,13 @@ private void removeCurrentLayer() { private void refreshAllLayers() { for (Layer layer : layers) { - layer.selectedTags.removeIf(tag -> - DANGEROUS_CATEGORIES.contains(tag.category) && - !enabledDangerousCategories.contains(tag.category) - ); + layer.selectedTags.removeIf(tag -> { + if (DANGEROUS_CATEGORIES.contains(tag.category)) { + return !enabledDangerousCategories.contains(tag.category); + } else { + return !enabledCategories.contains(tag.category); + } + }); layer.updateTags.run(); } updatePreview(); @@ -741,6 +789,8 @@ private void copyToClipboard() { if (!filterText.isEmpty()) { showInfoMessage("Copied " + variantsToCopy.size() + " filtered variant(s)."); + } else { + showInfoMessage("Copied " + variantsToCopy.size() + " variant(s)."); } } From 87a9914f56460f3dfc681447985d2145792b8e35 Mon Sep 17 00:00:00 2001 From: Gareth Heyes Date: Thu, 27 Nov 2025 20:52:44 +0000 Subject: [PATCH 24/24] Updated README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 730ee78..25d0e1e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ Tags also support arguments. The find tag allows you to find a string by regex a # Changelog +## Version v2.2.33 (2025-11-27) + +- Added category checkboxes and a message when all variants are copied, +- Made the filter using the tag name. Changed dimensions. +- Added filter to output preview +- Added the first layer tag as the name of the Repeater tab +- Prevented dangerous categories from being shown in the MultiEncoderWindow. Added checkboxes to enable them. Made the window persist state in the project file when closed. + ## Version v2.2.26 (2025-11-26) - Fixed dialog problems