diff --git a/README.md b/README.md index 50deed0..b244dde 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Features of the SAML Raider message editor: * Supported Profiles: SAML Webbrowser Single Sign-on Profile, Web Services Security SAML Token Profile * Supported Bindings: POST Binding, Redirect Binding, SOAP Binding, URI Binding +* XML is pretty printed, syntax highlighted and editable live +* Search field at the bottom to auto scroll & highlight searched text SAML Attacks: diff --git a/build.gradle b/build.gradle index 80d3344..d764c15 100644 --- a/build.gradle +++ b/build.gradle @@ -8,9 +8,11 @@ repositories { mavenCentral() } -compileJava { - targetCompatibility "21" - sourceCompatibility "21" +// Build/compile against Java 21 regardless of system default Java +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } dependencies { @@ -30,7 +32,7 @@ dependencies { testImplementation libs.org.junit.jupiter } -compileJava { +tasks.withType(JavaCompile).configureEach { options.compilerArgs << "-Xlint:deprecation" } diff --git a/doc/saml_attacks.png b/doc/saml_attacks.png index a647cd5..11678e2 100644 Binary files a/doc/saml_attacks.png and b/doc/saml_attacks.png differ diff --git a/doc/saml_info.png b/doc/saml_info.png index d43523f..e3b3a0a 100644 Binary files a/doc/saml_info.png and b/doc/saml_info.png differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..b6dbb2f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +# Keep builds cross-platform. +# This project uses Gradle toolchains (see build.gradle) to compile with Java 21. +# Do NOT hard-pin org.gradle.java.home to a machine-specific path. + +# If a matching JDK isn't installed locally, allow Gradle to auto-provision one. +org.gradle.java.installations.auto-download=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/application/SamlTabController.java b/src/main/java/application/SamlTabController.java index cec7758..6009ef3 100644 --- a/src/main/java/application/SamlTabController.java +++ b/src/main/java/application/SamlTabController.java @@ -8,11 +8,11 @@ import burp.api.montoya.http.message.params.HttpParameterType; import burp.api.montoya.http.message.requests.HttpRequest; import burp.api.montoya.ui.Selection; -import burp.api.montoya.ui.editor.RawEditor; import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; import gui.CVEHelpWindow; import gui.SamlMain; import gui.SamlPanelInfo; +import gui.SamlXmlEditor; import gui.SignatureHelpWindow; import gui.XSWHelpWindow; import helpers.CVE_2022_41912; @@ -73,8 +73,7 @@ public class SamlTabController implements ExtensionProvidedHttpRequestEditor, Ob private SamlMessageAnalysisResult samlMessageAnalysisResult; private String orgSAMLMessage; private String samlMessage; - private RawEditor textArea; - private RawEditor textEditorInformation; + private SamlXmlEditor textArea; private SamlMain samlGUI; private boolean editable; private XSWHelpers xswHelpers; @@ -84,10 +83,8 @@ public SamlTabController(boolean editable, CertificateTabController certificateT this.certificateTabController = requireNonNull(certificateTabController, "certificateTabController"); this.editable = editable; samlGUI = new SamlMain(this); - textArea = samlGUI.getTextEditorAction(); + textArea = samlGUI.getXmlEditorAction(); textArea.setEditable(editable); - textEditorInformation = samlGUI.getTextEditorInformation(); - textEditorInformation.setEditable(false); xmlHelpers = new XMLHelpers(); xswHelpers = new XSWHelpers(); this.certificateTabController.addObserver(this); @@ -126,7 +123,7 @@ public HttpRequest getRequest() { setInfoMessageText(XML_NOT_WELL_FORMED); } } else { - String textMessage = textArea.getContents().toString(); + String textMessage = textArea.getText(); String parameterToUpdate; if (this.samlMessageAnalysisResult.isWSSMessage()) { @@ -163,7 +160,11 @@ public HttpRequest getRequest() { @Override public Selection selectedData() { - return textArea.selection().orElse(null); + String sel = textArea.selectedText(); + if (sel != null && !sel.isEmpty()) { + return Selection.selection(ByteArray.byteArray(sel)); + } + return null; } @Override @@ -199,8 +200,9 @@ public void setRequestResponse(HttpRequestResponse requestResponse) { resetInfoMessageText(); isEdited = false; if (requestResponse == null) { - textArea.setContents(null); + textArea.setText(""); textArea.setEditable(false); + textArea.resetModified(); setGUIEditable(false); resetInformationDisplay(); } else { @@ -258,22 +260,30 @@ public void setRequestResponse(HttpRequestResponse requestResponse) { updateCertificateList(); updateXSWList(); orgSAMLMessage = samlMessage; - textArea.setContents(ByteArray.byteArray(samlMessage)); + + // Show prettified XML (editable) for sanity when working with big SAML blobs. + textArea.setText(prettifyXmlOrFallback(samlMessage)); textArea.setEditable(editable); setGUIEditable(editable); } } + private String prettifyXmlOrFallback(String xml) { + try { + Document doc = xmlHelpers.getXMLDocumentOfSAMLMessage(xml); + return xmlHelpers.getStringOfDocument(doc, 2); + } catch (Exception ignored) { + return xml; + } + } + private void setInformationDisplay() { - samlGUI.getTextEditorInformation().setContents(ByteArray.byteArray("")); SamlPanelInfo infoPanel = samlGUI.getInfoPanel(); infoPanel.clearAll(); try { Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(samlMessage); - String formattedDocumentWithIndentation = xmlHelpers.getStringOfDocument(xmlHelpers.getXMLDocumentOfSAMLMessage(samlMessage), 2); - textEditorInformation.setContents(ByteArray.byteArray(formattedDocumentWithIndentation.getBytes())); NodeList assertions = xmlHelpers.getAssertions(document); if (assertions.getLength() > 0) { Node assertion = assertions.item(0); @@ -289,7 +299,7 @@ private void setInformationDisplay() { Node assertion = assertions.item(0); infoPanel.setEncryptionAlgorithm(xmlHelpers.getEncryptionMethod(assertion)); } - } catch (SAXException | IOException e) { + } catch (SAXException e) { setInfoMessageText(XML_NOT_WELL_FORMED); } } @@ -304,17 +314,16 @@ private void resetInformationDisplay() { infoPanel.setSignatureAlgorithm(""); infoPanel.setDigestAlgorithm(""); infoPanel.setEncryptionAlgorithm(""); - textEditorInformation.setContents(ByteArray.byteArray("")); } public void removeSignature() { resetInfoMessageText(); try { - Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getContents().toString()); + Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getText()); if (xmlHelpers.removeAllSignatures(document) > 0) { samlMessage = xmlHelpers.getStringOfDocument(document); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText("Message signature successful removed"); } else { @@ -327,9 +336,22 @@ public void removeSignature() { } } + public void formatXml() { + resetInfoMessageText(); + String current = textArea.getText(); + String formatted = prettifyXmlOrFallback(current); + if (formatted.equals(current)) { + setInfoMessageText("XML is already formatted (or not well-formed)"); + } else { + textArea.setText(formatted); + setInfoMessageText("XML formatted"); + } + } + public void resetMessage() { samlMessage = orgSAMLMessage; - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); + textArea.resetModified(); samlGUI.getStatusPanel().setText(""); isEdited = false; } @@ -340,7 +362,7 @@ public void resignAssertion() { BurpCertificate cert = samlGUI.getActionPanel().getSelectedCertificate(); if (cert != null) { setInfoMessageText("Signing..."); - Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getContents().toString()); + Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getText()); NodeList assertions = xmlHelpers.getAssertions(document); String signAlgorithm = xmlHelpers.getSignatureAlgorithm(assertions.item(0)); String digestAlgorithm = xmlHelpers.getDigestAlgorithm(assertions.item(0)); @@ -351,7 +373,7 @@ public void resignAssertion() { xmlHelpers.removeEmptyTags(doc); xmlHelpers.signAssertion(doc, signAlgorithm, digestAlgorithm, cert.getCertificate(), cert.getPrivateKey()); samlMessage = xmlHelpers.getStringOfDocument(doc); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText("Assertions successfully signed"); } else { @@ -378,7 +400,7 @@ public void resignMessage() { setInfoMessageText("Signing..."); BurpCertificate cert = samlGUI.getActionPanel().getSelectedCertificate(); if (cert != null) { - Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getContents().toString()); + Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getText()); NodeList responses = xmlHelpers.getResponse(document); String signAlgorithm = xmlHelpers.getSignatureAlgorithm(responses.item(0)); String digestAlgorithm = xmlHelpers.getDigestAlgorithm(responses.item(0)); @@ -386,7 +408,7 @@ public void resignMessage() { xmlHelpers.removeOnlyMessageSignature(document); xmlHelpers.signMessage(document, signAlgorithm, digestAlgorithm, cert.getCertificate(), cert.getPrivateKey()); samlMessage = xmlHelpers.getStringOfDocument(document); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText("Message successfully signed"); } else { @@ -440,7 +462,7 @@ private void updateXSWList() { public void sendToCertificatesTab() { try { - Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getContents().toString()); + Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(textArea.getText()); String cert = xmlHelpers.getCertificate(document.getDocumentElement()); if (cert != null) { certificateTabController.importCertificateFromString(cert); @@ -495,25 +517,25 @@ public void applyCVE() { switch (cve) { case CVE_2022_41912.CVE: samlMessage = CVE_2022_41912.apply(orgSAMLMessage); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText("%s applied".formatted(cve)); break; case CVE_2025_23369.CVE: samlMessage = CVE_2025_23369.apply(orgSAMLMessage); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText("%s applied".formatted(cve)); break; case CVE_2025_25291.CVE: samlMessage = CVE_2025_25291.apply(orgSAMLMessage); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText("%s applied".formatted(cve)); break; case CVE_2025_25292.CVE: samlMessage = CVE_2025_25292.apply(orgSAMLMessage); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText("%s applied".formatted(cve)); break; @@ -530,7 +552,7 @@ public void applyXSW() { document = xmlHelpers.getXMLDocumentOfSAMLMessage(orgSAMLMessage); xswHelpers.applyXSW(samlGUI.getActionPanel().getSelectedXSW(), document); samlMessage = xmlHelpers.getStringOfDocument(document); - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText(XSW_ATTACK_APPLIED); } catch (SAXException e) { @@ -551,7 +573,7 @@ public void applyXXE(String collabUrl) { String xmlDeclaration = "\n"; samlMessage = xmlDeclaration + xxePayload + orgSAMLMessage; } - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText(XXE_CONTENT_APPLIED); } @@ -592,7 +614,7 @@ public void applyXSLT(String collabUrl) { String firstPart = orgSAMLMessage.substring(0, substringIndex); String secondPart = orgSAMLMessage.substring(substringIndex); samlMessage = firstPart + xslt + secondPart; - textArea.setContents(ByteArray.byteArray(samlMessage)); + textArea.setText(prettifyXmlOrFallback(samlMessage)); isEdited = true; setInfoMessageText(XSLT_CONTENT_APPLIED); } @@ -638,11 +660,11 @@ public void update() { } public String getEditorContents() { - return this.textArea.getContents().toString(); + return this.textArea.getText(); } public void setEditorContents(String text) { this.isEdited = true; - this.textArea.setContents(ByteArray.byteArray(text)); + this.textArea.setText(prettifyXmlOrFallback(text)); } } diff --git a/src/main/java/gui/OobDomainDialog.java b/src/main/java/gui/OobDomainDialog.java new file mode 100644 index 0000000..2112648 --- /dev/null +++ b/src/main/java/gui/OobDomainDialog.java @@ -0,0 +1,100 @@ +package gui; + +import burp.BurpExtender; +import burp.api.montoya.core.BurpSuiteEdition; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.util.Optional; +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +/** + * Dialog that lets the user choose between Burp Collaborator and a + * custom OOB domain for XXE/XSLT payloads. + */ +public class OobDomainDialog { + + private OobDomainDialog() {} + + /** + * Show the dialog and return the chosen OOB URL, or empty if cancelled. + */ + public static Optional prompt(Component parent, String title) { + boolean isPro = BurpExtender.api.burpSuite().version().edition() == BurpSuiteEdition.PROFESSIONAL; + + var useCollab = new JCheckBox("Use Burp Collaborator", isPro); + useCollab.setEnabled(isPro); + if (!isPro) { + useCollab.setToolTipText("Burp Collaborator is only available in Burp Suite Professional"); + } + + var domainField = new JTextField(30); + domainField.setEnabled(!isPro); + var domainLabel = new JLabel("OOB Domain:"); + domainLabel.setEnabled(!isPro); + + useCollab.addActionListener(e -> { + boolean custom = !useCollab.isSelected(); + domainField.setEnabled(custom); + domainLabel.setEnabled(custom); + if (custom) { + domainField.requestFocusInWindow(); + } + }); + + var collabRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + collabRow.add(useCollab); + + var domainRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); + domainRow.add(domainLabel); + domainRow.add(domainField); + + var panel = new JPanel(new BorderLayout(0, 8)); + panel.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); + panel.add(collabRow, BorderLayout.NORTH); + panel.add(domainRow, BorderLayout.CENTER); + + int result = JOptionPane.showConfirmDialog( + parent, panel, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); + + if (result != JOptionPane.OK_OPTION) { + return Optional.empty(); + } + + if (useCollab.isSelected()) { + try { + String payload = BurpExtender.api.collaborator() + .defaultPayloadGenerator() + .generatePayload() + .toString(); + return Optional.of("https://" + payload); + } catch (Exception ex) { + BurpExtender.api.logging().logToError("Could not generate Collaborator payload: " + ex.getMessage()); + BurpExtender.api.logging().logToError(ex); + JOptionPane.showMessageDialog(parent, + "Failed to generate Burp Collaborator payload.\n" + + "Make sure Collaborator is enabled in Burp settings.\n\n" + + ex.getMessage(), + "Collaborator Error", JOptionPane.ERROR_MESSAGE); + return Optional.empty(); + } + } else { + String domain = domainField.getText().trim(); + if (domain.isEmpty()) { + JOptionPane.showMessageDialog(parent, + "Please enter an OOB domain.", title, JOptionPane.WARNING_MESSAGE); + return Optional.empty(); + } + // Ensure it has a scheme + if (!domain.startsWith("http://") && !domain.startsWith("https://")) { + domain = "https://" + domain; + } + return Optional.of(domain); + } + } +} diff --git a/src/main/java/gui/SamlMain.java b/src/main/java/gui/SamlMain.java index 7016be1..22a4668 100644 --- a/src/main/java/gui/SamlMain.java +++ b/src/main/java/gui/SamlMain.java @@ -1,16 +1,11 @@ package gui; import application.SamlTabController; -import burp.BurpExtender; -import burp.api.montoya.core.ByteArray; -import burp.api.montoya.ui.editor.RawEditor; import java.awt.BorderLayout; import java.awt.Dimension; -import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; -import javax.swing.border.EmptyBorder; import static java.util.Objects.requireNonNull; @@ -18,8 +13,7 @@ public class SamlMain extends JPanel { private final SamlTabController controller; - private RawEditor textEditorAction; - private RawEditor textEditorInformation; + private SamlXmlEditor xmlEditorAction; private SamlPanelAction panelAction; private SamlPanelInfo panelInformation; private SamlPanelStatus panelStatus; @@ -34,17 +28,17 @@ private void initializeUI() { JPanel splitPaneActionTop = new JPanel(); splitPaneActionTop.setLayout(new BorderLayout()); - splitPaneActionTop.setPreferredSize(new Dimension(0, 460)); + splitPaneActionTop.setPreferredSize(new Dimension(0, 195)); splitPaneActionTop.add(panelAction); - textEditorAction = BurpExtender.api.userInterface().createRawEditor(); - textEditorAction.setContents(ByteArray.byteArray("")); - textEditorAction.setEditable(false); + xmlEditorAction = new SamlXmlEditor(); + xmlEditorAction.setText(""); + xmlEditorAction.setEditable(false); JPanel splitPaneActionBottom = new JPanel(); splitPaneActionBottom.setLayout(new BorderLayout()); splitPaneActionBottom.setPreferredSize(new Dimension(0, 100)); - splitPaneActionBottom.add(textEditorAction.uiComponent(), BorderLayout.CENTER); + splitPaneActionBottom.add(xmlEditorAction, BorderLayout.CENTER); JSplitPane splitPaneAction = new JSplitPane(); splitPaneAction.setOrientation(JSplitPane.VERTICAL_SPLIT); @@ -59,27 +53,9 @@ private void initializeUI() { splitPaneInformationTop.setPreferredSize(new Dimension(0, 375)); splitPaneInformationTop.add(panelInformation); - textEditorInformation = BurpExtender.api.userInterface().createRawEditor(); - textEditorInformation.setContents(ByteArray.byteArray("")); - - var splitPaneInformationButtomLabel = new JLabel("Parsed & Prettified"); - splitPaneInformationButtomLabel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - JPanel splitPaneInformationBottom = new JPanel(); - splitPaneInformationBottom.setLayout(new BorderLayout()); - splitPaneInformationBottom.setPreferredSize(new Dimension(0, 100)); - splitPaneInformationBottom.add(splitPaneInformationButtomLabel, BorderLayout.NORTH); - splitPaneInformationBottom.add(textEditorInformation.uiComponent(), BorderLayout.CENTER); - - JSplitPane splitPaneInformation = new JSplitPane(); - splitPaneInformation.setOrientation(JSplitPane.VERTICAL_SPLIT); - splitPaneInformation.setLeftComponent((splitPaneInformationTop)); - splitPaneInformation.setRightComponent(splitPaneInformationBottom); - splitPaneInformation.resetToPreferredSizes(); - JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.addTab("SAML Attacks", null, splitPaneAction, "SAML Attacks"); - tabbedPane.addTab("SAML Message Info", null, splitPaneInformation, "SAML Message Info"); + tabbedPane.addTab("SAML Message Info", null, splitPaneInformationTop, "SAML Message Info"); panelStatus = new SamlPanelStatus(); @@ -91,12 +67,8 @@ private void initializeUI() { updateUI(); } - public RawEditor getTextEditorAction() { - return textEditorAction; - } - - public RawEditor getTextEditorInformation() { - return textEditorInformation; + public SamlXmlEditor getXmlEditorAction() { + return xmlEditorAction; } public SamlPanelAction getActionPanel() { diff --git a/src/main/java/gui/SamlPanelAction.java b/src/main/java/gui/SamlPanelAction.java index 93d6b6d..e47c6bb 100644 --- a/src/main/java/gui/SamlPanelAction.java +++ b/src/main/java/gui/SamlPanelAction.java @@ -7,6 +7,7 @@ import helpers.CVE_2025_25292; import java.awt.BorderLayout; import java.awt.Component; +import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; @@ -37,8 +38,9 @@ public class SamlPanelAction extends JPanel { private SamlTabController controller; private final JButton btnMessageReset = new JButton("Reset Message"); + private final JButton btnFormatXml = new JButton("Format XML"); - private final JButton btnXSWHelp = new JButton("Help"); + private final JButton btnXSWHelp = new JButton("?"); private final JComboBox cmbboxXSW = new JComboBox<>(); private final JButton btnXSWPreview = new JButton("Preview in Browser..."); private final JButton btnMatchAndReplace = new JButton("Match and Replace"); @@ -49,13 +51,13 @@ public class SamlPanelAction extends JPanel { private final JComboBox cmbboxCVE = new JComboBox<>(); private final JButton btnCVEApply = new JButton("Apply CVE"); - private final JButton btnCVEHelp = new JButton("Help"); + private final JButton btnCVEHelp = new JButton("?"); - private final JButton btnSignatureHelp = new JButton("Help"); + private final JButton btnSignatureHelp = new JButton("?"); private final JComboBox cmbboxCertificate = new JComboBox<>(); private final JButton btnSignatureRemove = new JButton("Remove Signatures"); private final JButton btnResignAssertion = new JButton("(Re-)Sign Assertion"); - private final JButton btnSendCertificate = new JButton("Send Certificate to SAML Raider Certificates"); + private final JButton btnSendCertificate = new JButton("Store Certificate"); private final JButton btnResignMessage = new JButton("(Re-)Sign Message"); @@ -73,94 +75,88 @@ private void initialize() { controller.resetMessage(); }); - var samlMessagePanel = new JPanel(); - samlMessagePanel.setBorder(BorderFactory.createTitledBorder("SAML Message")); - samlMessagePanel.setLayout(new MigLayout()); - samlMessagePanel.add(btnMessageReset, "wrap"); + btnFormatXml.addActionListener(event -> controller.formatXml()); + // --- Wire listeners --- btnXSWHelp.addActionListener(event -> controller.showXSWHelp()); - btnXSWPreview.addActionListener(event -> controller.showXSWPreview()); - btnMatchAndReplace.addActionListener(event -> showMatchAndReplaceDialog()); - btnXSWApply.addActionListener(event -> controller.applyXSW()); - var xswAttacksPanel = new JPanel(); - xswAttacksPanel.setBorder(BorderFactory.createTitledBorder("XSW Attacks")); - xswAttacksPanel.setLayout(new MigLayout()); - xswAttacksPanel.add(btnXSWHelp, "wrap"); - xswAttacksPanel.add(cmbboxXSW, "split 4"); - xswAttacksPanel.add(btnMatchAndReplace); - xswAttacksPanel.add(btnXSWPreview); - xswAttacksPanel.add(btnXSWApply, "wrap"); - btnTestXXE.addActionListener(event -> - Optional.ofNullable(JOptionPane.showInputDialog(btnXSWApply, "Enter Burp Collaborator URL (e.g. https://xyz.burpcollaborator.net)")) + OobDomainDialog.prompt(this, "XXE — OOB Domain") .ifPresent(controller::applyXXE)); - btnTestXSLT.addActionListener(event -> - Optional.ofNullable(JOptionPane.showInputDialog(btnXSWApply, "Enter Burp Collaborator URL (e.g. https://xyz.burpcollaborator.net)")) + OobDomainDialog.prompt(this, "XSLT — OOB Domain") .ifPresent(controller::applyXSLT)); - var xmlAttacksPanel = new JPanel(); - xmlAttacksPanel.setBorder(BorderFactory.createTitledBorder("XML Attacks")); - xmlAttacksPanel.setLayout(new MigLayout()); - xmlAttacksPanel.add(btnTestXXE, "split 2"); - xmlAttacksPanel.add(btnTestXSLT, "wrap"); - cmbboxCVE.setModel(new DefaultComboBoxModel<>(new String[]{ - CVE_2022_41912.CVE, - CVE_2025_23369.CVE, - CVE_2025_25291.CVE, - CVE_2025_25292.CVE - })); - + CVE_2022_41912.CVE, CVE_2025_23369.CVE, + CVE_2025_25291.CVE, CVE_2025_25292.CVE })); btnCVEApply.addActionListener(event -> controller.applyCVE()); - btnCVEHelp.addActionListener(event -> controller.showCVEHelp()); - var cvePanel = new JPanel(); - cvePanel.setBorder(BorderFactory.createTitledBorder("CVEs")); - cvePanel.setLayout(new MigLayout()); - cvePanel.add(cmbboxCVE); - cvePanel.add(btnCVEApply); - cvePanel.add(btnCVEHelp, "wrap"); - btnSignatureHelp.addActionListener(event -> controller.showSignatureHelp()); - btnSignatureRemove.addActionListener(event -> controller.removeSignature()); - btnResignAssertion.addActionListener(event -> controller.resignAssertion()); - btnSendCertificate.addActionListener(event -> controller.sendToCertificatesTab()); - btnResignMessage.addActionListener(event -> controller.resignMessage()); - var signatureAttacksPanel = new JPanel(); - signatureAttacksPanel.setBorder(BorderFactory.createTitledBorder("Signature Attacks")); - signatureAttacksPanel.setLayout(new MigLayout()); - signatureAttacksPanel.add(btnSignatureHelp, "wrap"); - signatureAttacksPanel.add(btnSignatureRemove, "split 2"); - signatureAttacksPanel.add(btnSendCertificate, "wrap"); - signatureAttacksPanel.add(cmbboxCertificate, "split 3"); - signatureAttacksPanel.add(btnResignAssertion); - signatureAttacksPanel.add(btnResignMessage, "wrap"); - - var actionPanels = new JPanel(); - var actionPanelConstraints = "wrap"; - actionPanels.setLayout(new MigLayout()); - actionPanels.add(samlMessagePanel, actionPanelConstraints); - actionPanels.add(xswAttacksPanel, actionPanelConstraints); - actionPanels.add(cvePanel, actionPanelConstraints); - actionPanels.add(xmlAttacksPanel, actionPanelConstraints); - actionPanels.add(signatureAttacksPanel, actionPanelConstraints); - - var scrollPane = new JScrollPane(actionPanels); - scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); + // --- Compact layout: labeled sections with separators --- + var panel = new JPanel(new MigLayout("insets 6 8 6 8, gap 4 6, fillx", "[grow]", "")); + + // Row 1: Message + panel.add(sectionLabel("Message"), "split"); + panel.add(btnMessageReset); + panel.add(btnFormatXml, "wrap"); + + panel.add(separator(), "growx, wrap"); + + // Row 2: XSW + panel.add(sectionLabel("XSW"), "split"); + panel.add(cmbboxXSW); + panel.add(btnXSWApply); + panel.add(btnMatchAndReplace); + panel.add(btnXSWPreview); + panel.add(btnXSWHelp, "wrap"); + + // Row 3: CVE + panel.add(sectionLabel("CVE"), "split"); + panel.add(cmbboxCVE); + panel.add(btnCVEApply); + panel.add(btnCVEHelp, "wrap"); + + // Row 4: XML + panel.add(sectionLabel("XML"), "split"); + panel.add(btnTestXXE); + panel.add(btnTestXSLT, "wrap"); + + panel.add(separator(), "growx, wrap"); + + // Row 5: Signing + panel.add(sectionLabel("Signing"), "split"); + panel.add(cmbboxCertificate); + panel.add(btnResignAssertion); + panel.add(btnResignMessage); + panel.add(btnSignatureRemove); + panel.add(btnSignatureHelp, "wrap"); + + // Row 6: Store Certificate (under Signing) + panel.add(new JLabel(""), "split"); // indent to align + panel.add(btnSendCertificate, "wrap"); setLayout(new BorderLayout()); - add(scrollPane, BorderLayout.CENTER); + add(panel, BorderLayout.NORTH); + } + + private static JLabel sectionLabel(String text) { + var label = new JLabel(text); + label.setFont(label.getFont().deriveFont(Font.BOLD, 11f)); + return label; + } + + private static javax.swing.JSeparator separator() { + return new javax.swing.JSeparator(javax.swing.SwingConstants.HORIZONTAL); } public void setCertificateList(List list) { @@ -202,6 +198,7 @@ public void disableControls() { btnSendCertificate.setEnabled(false); btnResignMessage.setEnabled(false); btnMatchAndReplace.setEnabled(false); + btnFormatXml.setEnabled(false); btnTestXXE.setEnabled(false); btnTestXSLT.setEnabled(false); cmbboxCVE.setEnabled(false); @@ -222,6 +219,7 @@ public void enableControls() { btnSendCertificate.setEnabled(true); btnResignMessage.setEnabled(true); btnMatchAndReplace.setEnabled(true); + btnFormatXml.setEnabled(true); btnTestXXE.setEnabled(true); btnTestXSLT.setEnabled(true); cmbboxCVE.setEnabled(true); diff --git a/src/main/java/gui/SamlXmlEditor.java b/src/main/java/gui/SamlXmlEditor.java new file mode 100644 index 0000000..cbdd293 --- /dev/null +++ b/src/main/java/gui/SamlXmlEditor.java @@ -0,0 +1,479 @@ +package gui; + +import burp.BurpExtender; +import burp.api.montoya.ui.Theme; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.Serial; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.JTextPane; +import javax.swing.Timer; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultHighlighter; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.Highlighter; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; + +/** + * Editable XML editor with syntax highlighting, built on JTextPane. + * Uses a simple state-machine tokenizer — no external library needed, + * so no Swing/ClassLoader conflicts inside Burp. + */ +public class SamlXmlEditor extends JPanel { + + @Serial + private static final long serialVersionUID = 1L; + + private enum Tk { TEXT, BRACKET, TAG_NAME, ATTR_NAME, ATTR_VALUE, COMMENT, CDATA, PI } + + private final JTextPane textPane; + private final DefaultStyledDocument doc; + private final Timer highlightTimer; + private final AtomicBoolean modified = new AtomicBoolean(false); + private volatile boolean suppressEvents = false; + private boolean softWrap = false; + + // Search + private final JTextField searchField; + private final JLabel searchStatus; + private final Highlighter.HighlightPainter searchPainter; + private int currentMatchIndex = -1; + private int[] matchPositions = new int[0]; // start positions of all matches + + // Attribute sets for each token type + private final SimpleAttributeSet aDefault = new SimpleAttributeSet(); + private final SimpleAttributeSet aBracket = new SimpleAttributeSet(); + private final SimpleAttributeSet aTagName = new SimpleAttributeSet(); + private final SimpleAttributeSet aAttrName = new SimpleAttributeSet(); + private final SimpleAttributeSet aAttrValue = new SimpleAttributeSet(); + private final SimpleAttributeSet aComment = new SimpleAttributeSet(); + private final SimpleAttributeSet aCdata = new SimpleAttributeSet(); + private final SimpleAttributeSet aPi = new SimpleAttributeSet(); + + public SamlXmlEditor() { + super(new BorderLayout()); + + doc = new DefaultStyledDocument(); + + // Override to control word-wrapping via softWrap flag. + // When softWrap=false, long lines scroll horizontally. + // When softWrap=true, content wraps at viewport edge. + textPane = new JTextPane(doc) { + @Serial + private static final long serialVersionUID = 1L; + @Override + public boolean getScrollableTracksViewportWidth() { + if (softWrap) return true; + Component parent = getParent(); + if (parent == null) return true; + return getUI().getPreferredSize(this).width <= parent.getWidth(); + } + }; + + textPane.setEditable(true); + textPane.setEnabled(true); + textPane.setFocusable(true); + + // Font: prefer Burp's editor font, fall back to monospaced + Font burpFont = BurpExtender.api.userInterface().currentEditorFont(); + Font font = (burpFont != null) ? burpFont : new Font(Font.MONOSPACED, Font.PLAIN, 13); + textPane.setFont(font); + + applyThemeColors(font); + + // Debounced highlighting: re-color 150 ms after last keystroke + highlightTimer = new Timer(150, e -> applyHighlighting()); + highlightTimer.setRepeats(false); + + doc.addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { onEdit(); } + @Override public void removeUpdate(DocumentEvent e) { onEdit(); } + @Override public void changedUpdate(DocumentEvent e) { /* style changes — ignore */ } + private void onEdit() { + if (!suppressEvents) { + modified.set(true); + highlightTimer.restart(); + } + } + }); + + var scroll = new JScrollPane(textPane); + scroll.setBorder(null); + add(scroll, BorderLayout.CENTER); + + // --- Search bar --- + boolean dark = BurpExtender.api.userInterface().currentTheme() == Theme.DARK; + searchPainter = new DefaultHighlighter.DefaultHighlightPainter( + dark ? new Color(0x806030) : new Color(0xFFE08A)); + + searchField = new JTextField(); + searchField.setFont(font.deriveFont(Font.PLAIN, 12f)); + searchField.setPreferredSize(new Dimension(220, 26)); + searchField.setToolTipText("Search XML (Enter = next, Shift+Enter = previous, Esc = close)"); + + searchStatus = new JLabel(""); + searchStatus.setFont(font.deriveFont(Font.PLAIN, 11f)); + searchStatus.setForeground(dark ? new Color(0x999999) : new Color(0x666666)); + + // Live search as you type + searchField.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { doSearch(); } + @Override public void removeUpdate(DocumentEvent e) { doSearch(); } + @Override public void changedUpdate(DocumentEvent e) { doSearch(); } + }); + + // Enter = next match, Shift+Enter = prev, Escape = hide + searchField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (e.isShiftDown()) jumpToMatch(-1); else jumpToMatch(1); + } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + clearSearch(); + textPane.requestFocusInWindow(); + } + } + }); + + // Nav arrows: ▲ previous, ▼ next + var btnPrev = new JButton("▲"); + btnPrev.setFont(font.deriveFont(Font.PLAIN, 10f)); + btnPrev.setToolTipText("Previous match (Shift+Enter)"); + btnPrev.setFocusable(false); + btnPrev.setMargin(new java.awt.Insets(1, 4, 1, 4)); + btnPrev.addActionListener(e -> jumpToMatch(-1)); + + var btnNext = new JButton("▼"); + btnNext.setFont(font.deriveFont(Font.PLAIN, 10f)); + btnNext.setToolTipText("Next match (Enter)"); + btnNext.setFocusable(false); + btnNext.setMargin(new java.awt.Insets(1, 4, 1, 4)); + btnNext.addActionListener(e -> jumpToMatch(1)); + + var navPanel = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT, 2, 0)); + navPanel.add(btnPrev); + navPanel.add(btnNext); + navPanel.add(searchStatus); + + // Soft wrap checkbox + var wrapCheck = new JCheckBox("Wrap"); + wrapCheck.setFont(font.deriveFont(Font.PLAIN, 11f)); + wrapCheck.setFocusable(false); + wrapCheck.setToolTipText("Soft wrap long lines"); + wrapCheck.setSelected(softWrap); + wrapCheck.addActionListener(e -> { + softWrap = wrapCheck.isSelected(); + textPane.getParent().invalidate(); + textPane.revalidate(); + textPane.repaint(); + }); + + var rightPanel = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT, 4, 0)); + rightPanel.add(wrapCheck); + + var searchBar = new JPanel(new BorderLayout(6, 0)); + searchBar.setBorder(BorderFactory.createEmptyBorder(3, 6, 3, 6)); + var searchLabel = new JLabel("Find:"); + searchLabel.setFont(font.deriveFont(Font.PLAIN, 12f)); + searchBar.add(searchLabel, BorderLayout.WEST); + searchBar.add(searchField, BorderLayout.CENTER); + + var eastPanel = new JPanel(new BorderLayout(4, 0)); + eastPanel.add(navPanel, BorderLayout.WEST); + eastPanel.add(rightPanel, BorderLayout.EAST); + searchBar.add(eastPanel, BorderLayout.EAST); + + add(searchBar, BorderLayout.SOUTH); + } + + /* ------------------------------------------------------------------ */ + /* Theme */ + /* ------------------------------------------------------------------ */ + + private void applyThemeColors(Font font) { + boolean dark = BurpExtender.api.userInterface().currentTheme() == Theme.DARK; + + textPane.setBackground(dark ? new Color(0x1E1F22) : Color.WHITE); + textPane.setForeground(dark ? new Color(0xD6D6D6) : new Color(0x1F2328)); + textPane.setCaretColor(dark ? new Color(0xEDEDED) : Color.BLACK); + textPane.setSelectionColor(dark ? new Color(0x264F78) : new Color(0xBBDDFF)); + + setAttr(aDefault, font, dark ? 0xD6D6D6 : 0x1F2328, false); + setAttr(aBracket, font, dark ? 0x808080 : 0x333333, false); + setAttr(aTagName, font, dark ? 0x569CD6 : 0x0000FF, false); + setAttr(aAttrName, font, dark ? 0x9CDCFE : 0xA31515, false); + setAttr(aAttrValue, font, dark ? 0xCE9178 : 0x0451A5, false); + setAttr(aComment, font, dark ? 0x6A9955 : 0x008000, true); + setAttr(aCdata, font, dark ? 0xD7BA7D : 0x800000, false); + setAttr(aPi, font, dark ? 0x808080 : 0x808080, true); + } + + private static void setAttr(SimpleAttributeSet a, Font font, int rgb, boolean italic) { + StyleConstants.setFontFamily(a, font.getFamily()); + StyleConstants.setFontSize(a, font.getSize()); + StyleConstants.setForeground(a, new Color(rgb)); + StyleConstants.setItalic(a, italic); + StyleConstants.setBold(a, false); + } + + /* ------------------------------------------------------------------ */ + /* XML tokenizer (state machine) */ + /* ------------------------------------------------------------------ */ + + private Tk[] tokenize(String text) { + Tk[] tokens = new Tk[text.length()]; + Arrays.fill(tokens, Tk.TEXT); + int len = text.length(); + int i = 0; + + while (i < len) { + if (text.charAt(i) != '<') { i++; continue; } + + if (regionMatches(text, i, "", i + 4); + int endPos = (end == -1) ? len : end + 3; + Arrays.fill(tokens, i, Math.min(endPos, len), Tk.COMMENT); + i = endPos; + } else if (regionMatches(text, i, "", i + 9); + int endPos = (end == -1) ? len : end + 3; + Arrays.fill(tokens, i, Math.min(endPos, len), Tk.CDATA); + i = endPos; + } else if (i + 1 < len && text.charAt(i + 1) == '?') { + int end = text.indexOf("?>", i + 2); + int endPos = (end == -1) ? len : end + 2; + Arrays.fill(tokens, i, Math.min(endPos, len), Tk.PI); + i = endPos; + } else { + // Regular tag: or + tokens[i++] = Tk.BRACKET; // < + if (i < len && text.charAt(i) == '/') { + tokens[i++] = Tk.BRACKET; // / + } + while (i < len && isNameChar(text.charAt(i))) { + tokens[i++] = Tk.TAG_NAME; + } + i = tokenizeInsideTag(text, tokens, i); + } + } + return tokens; + } + + /** Tokenize attribute region inside a tag until closing '>' */ + private int tokenizeInsideTag(String text, Tk[] tokens, int i) { + int len = text.length(); + while (i < len) { + char c = text.charAt(i); + if (c == '>') { + tokens[i++] = Tk.BRACKET; + return i; + } else if (c == '/') { + tokens[i++] = Tk.BRACKET; + } else if (c == '=') { + tokens[i++] = Tk.BRACKET; + } else if (c == '"' || c == '\'') { + char q = c; + tokens[i++] = Tk.ATTR_VALUE; + while (i < len && text.charAt(i) != q) { tokens[i++] = Tk.ATTR_VALUE; } + if (i < len) { tokens[i++] = Tk.ATTR_VALUE; } // closing quote + } else if (isNameStartChar(c)) { + while (i < len && isNameChar(text.charAt(i))) { tokens[i++] = Tk.ATTR_NAME; } + } else { + i++; // whitespace + } + } + return i; + } + + /* ------------------------------------------------------------------ */ + /* Apply highlighting */ + /* ------------------------------------------------------------------ */ + + private void applyHighlighting() { + String text; + try { text = doc.getText(0, doc.getLength()); } + catch (BadLocationException e) { return; } + if (text.isEmpty()) return; + + Tk[] tokens = tokenize(text); + + // Group consecutive same-type tokens into runs and apply style per run + int runStart = 0; + Tk runType = tokens[0]; + for (int i = 1; i <= tokens.length; i++) { + Tk t = (i < tokens.length) ? tokens[i] : null; + if (t != runType) { + doc.setCharacterAttributes(runStart, i - runStart, attrFor(runType), true); + runStart = i; + runType = t; + } + } + } + + private SimpleAttributeSet attrFor(Tk token) { + return switch (token) { + case BRACKET -> aBracket; + case TAG_NAME -> aTagName; + case ATTR_NAME -> aAttrName; + case ATTR_VALUE -> aAttrValue; + case COMMENT -> aComment; + case CDATA -> aCdata; + case PI -> aPi; + default -> aDefault; + }; + } + + /* ------------------------------------------------------------------ */ + /* Char classification helpers */ + /* ------------------------------------------------------------------ */ + + private static boolean regionMatches(String text, int pos, String prefix) { + return text.regionMatches(pos, prefix, 0, prefix.length()); + } + + private static boolean isNameStartChar(char c) { + return Character.isLetter(c) || c == '_' || c == ':'; + } + + private static boolean isNameChar(char c) { + return Character.isLetterOrDigit(c) || c == ':' || c == '-' || c == '.' || c == '_'; + } + + /* ------------------------------------------------------------------ */ + /* Public API */ + /* ------------------------------------------------------------------ */ + + /* ------------------------------------------------------------------ */ + /* Search */ + /* ------------------------------------------------------------------ */ + + private void doSearch() { + textPane.getHighlighter().removeAllHighlights(); + String query = searchField.getText(); + if (query == null || query.isEmpty()) { + searchStatus.setText(""); + matchPositions = new int[0]; + currentMatchIndex = -1; + return; + } + + String text; + try { text = doc.getText(0, doc.getLength()); } + catch (BadLocationException e) { return; } + + String lowerText = text.toLowerCase(); + String lowerQuery = query.toLowerCase(); + + // Find all matches + java.util.List positions = new java.util.ArrayList<>(); + int idx = 0; + while ((idx = lowerText.indexOf(lowerQuery, idx)) != -1) { + positions.add(idx); + try { + textPane.getHighlighter().addHighlight(idx, idx + query.length(), searchPainter); + } catch (BadLocationException ignored) {} + idx += query.length(); + } + + matchPositions = positions.stream().mapToInt(Integer::intValue).toArray(); + + if (matchPositions.length == 0) { + searchStatus.setText("No matches"); + currentMatchIndex = -1; + } else { + currentMatchIndex = 0; + scrollToMatch(0); + updateSearchStatus(); + } + } + + private void jumpToMatch(int direction) { + if (matchPositions.length == 0) return; + currentMatchIndex = (currentMatchIndex + direction + matchPositions.length) % matchPositions.length; + scrollToMatch(currentMatchIndex); + updateSearchStatus(); + } + + private void scrollToMatch(int index) { + if (index < 0 || index >= matchPositions.length) return; + int pos = matchPositions[index]; + textPane.setCaretPosition(pos); + // Select the match so it's visually obvious + textPane.setSelectionStart(pos); + textPane.setSelectionEnd(pos + searchField.getText().length()); + try { + textPane.scrollRectToVisible(textPane.modelToView2D(pos).getBounds()); + } catch (BadLocationException ignored) {} + } + + private void updateSearchStatus() { + if (matchPositions.length == 0) { + searchStatus.setText("No matches"); + } else { + searchStatus.setText((currentMatchIndex + 1) + " / " + matchPositions.length); + } + } + + private void clearSearch() { + searchField.setText(""); + textPane.getHighlighter().removeAllHighlights(); + matchPositions = new int[0]; + currentMatchIndex = -1; + searchStatus.setText(""); + } + + /* ------------------------------------------------------------------ */ + /* Public API */ + /* ------------------------------------------------------------------ */ + + public void setText(String text) { + suppressEvents = true; + try { + textPane.setText(text != null ? text : ""); + textPane.setCaretPosition(0); + modified.set(false); + } finally { + suppressEvents = false; + } + applyHighlighting(); + } + + public String getText() { + try { return doc.getText(0, doc.getLength()); } + catch (BadLocationException e) { return textPane.getText(); } + } + + public void setEditable(boolean editable) { + textPane.setEditable(editable); + textPane.setEnabled(true); + textPane.setFocusable(true); + } + + public boolean isModified() { + return modified.get(); + } + + public void resetModified() { + modified.set(false); + } + + public String selectedText() { + return textPane.getSelectedText(); + } +}