Debugging SAML messages

SAML is a standard protocol for parties to exchange authentication and authorization information. It stands for Security Assertion Markup Language and defines multiple use-cases and features. The most interesting one is Single Sign One for web browsing. Its native integration with HTTP makes it easy and fully compatible to implement.
Single sign on is known as SSO and allows a user to authenticate to independent applications without being prompted for credentials. It is particularly interesting as it increases users experience because no credential has to be typed each time for each application.
What is SAML SSO in short
The SSO SAML protocol defines two different entities:
- The Service Provider (SP): this is the web service the user is willing to use.
- The Identity Provider (IDP) : this is the web service providing user’s identity for authentication.
When the user browses the Service Provider, it will be redirected to the Identity Provider. Once authenticated against the Identity Provider, the user will be redirected to the Service Provider with authentication information. This authentication information contains user attributes for the Service Provider to authenticate the user and optionally roles for authorization.
As Service Provider, we can find Facebook, Azure and many others. For instance, because the user is already authenticated with Facebook he won’t have to type his credentials for authentication. Facebook will transparently forward user attributes to the Service Provider. How many times do you log into Facebook? Never! this is why SAML is particularly interesting for user experience.
The exchanged messages between the Service Provider and the Identity Provider is performed with redirection through end-user requests. The providers never exchange messages between each other, it always goes through the user browser.

For obvious security reasons, the SAML request and especially the SAML response are signed. This signature prevent the user to impersonate another user by modifying the user attributes from the SAML response. Moreover for confidential reasons, the authentication information containing the user’s attribute is encrypted and this is when the fun begins!
SAML sample messages
The request to the IDP is usually a HTTP GET request. The actual SAML Request payload containing the data for the IDP is usually a GET parameter called SAMLRequest.
SAMLRequest=eJx9U8uO2jAU3c9XWN5DnAyPxoJUFPpAohAR2kU3lWvfFEvxo7YzQ/%2B%2BTgpVKs2wsmSfc%2B45914vPFONpas2nPURfrXgA7qoRnvaPyxx6zQ1zEtPNVPgaeC0Wn3e0WxMqHUmGG4aPKDcZzDvwQVpNEbbzRIf9u93h4/b/fc3JJ%2BTeU3II2FTQcgsIzwXuajzeT3L5qyGXHCeTTD6Cs5H/hJHOYxKZ56kALePlZa4KlGIAaK29y1stQ9Mh4gk6WRE5qN0dsoe6TSjk%2Bk3jDYRKTULvdg5BEuTRAo7hgtTtoExNyqpqkMF7klyGNuz7cv1gd9JLaT%2BeT/rj78gTz%2BdTuWoPFQnjFa3/GujfavAXeW/HHf/TPj/PQhQJk2iFlw6E28Z97h4QGjRdZv2SV1xj6ogMMEC69iLZMi6qVja9W%2B7KU0j%2BW/0wTjFwuvh0nHa30gxqnsoBcVksxLCgfcxZNOY57UDFuJMgmsBJ4NS1y0D0e9c7EOAS0Broyxz0nfDiBF46DPeUg6h6yYu0RHq4u6ecco7XLwu4/FsnOhmBzzWPTmmvTUuXJvxonjnN7ljuHi4PQ%2B/TvEHIUUmDw%3D%3D
The SAML Response is much longer because it contains authentication information with user attributes. Depending on the implementation, this redirection uses a HTTP Post method performed with Javascript through a web page. As the redirection is instantaneous, it’s fully transparent from the end-user point of view.
SAMLResponse=eJytWEfP42hyvu%2BvaLSPxDRz%2BjDTBnOOYr4sGEWKFLOYfr3VPdP2JC/WgAGdihUeVa735yV9duOHWy7j0C/lp%2BPZ9cvHd%2BIvn19z/zGkS7N89OmzXD7W/OPGGPoH8gX6GOdhHfKh%2B/w7kX8tkS5LOa/N0H/%2BpPC/fP4nVVJFjlcEndJUnmM5XFUohpAlnpcoRkBEVRAkUtFwSXz%2BFJTz8pb85fNb0Vt8WV6l0i9r2q9vEgRjP0HkTzDpQfDH%2B4dRyedPfLmsTZ%2Bu36XqdR0/QHAZv5RH%2Bhy78ks%2BPMGifA4w2PRFeXwZ6/E/03×56%2B5/%2BMIbfvlsmYJuSYr5T6wqSzSDMAKl8RwrSRyGYLikyYqiISjDERItcIKg8M9f//Hp08/fnPHxHeX89TfbTfFH489yTYt0Tb9Z/hn8vcAPBePHbU3X1/KN8CcSNxTlpyDtXuW/dvnynfvj9srzclk%2Bg991g39W/itcoc/nc1zLgvkRp98MH2Wf/89X/o35t5B/%2B/Dfvt33/cuOfhnmO4hAEAxCGPjmenP8x48MKZbm/r%2BwQyBEf2P/xvLm986x/DcUC135LPv189c/QHwDN8q1HopPTHcf5matn/%2BGrrRcYIT6Kc/yt5d%2B/objQytPpa%2BG/yv6r39y2FvN/wfAeUl/shjB/gbvuzauGety/haOPxC%2BZ8XX7oTv6tlDR2JLIXrJCBRoQpqhKtBGguqg0XB73s0KlUk/w%2BV%2BLSsbo4%2BuJetushZ3ZQN2sQtysWHFX2noMfEiH16Q9oQJL6Cs4iD8aEzjZaLltAZ8Gz0b4YYkAOqzS59ylGkreFDgNtBT7Brxyg7pWA/f27gjfHQhZGeTpqeSSVgX//Iz%2BBf4fyD9%2BhfBv/Eq%2BPs4fc/Wv7jmewr/kf6ridTnlSY5sire7E24WxVIK8xIcaOkw8sTiyImVmN4n7M1fyEp7iaygEKWp2a8iUnS8m5QL7ozXxyi55Q7uNKeQwg/lWznXbOLXZyKQosjlhDW3d1sGZF7kzUlu2wPJ1vc/ZWN3OiZlh142F75Ks7e0MiK3cfoLoLqK4C4opeL5hUbJfSNUVNA9o%2BHpF8EeOpH22gpcyMe1B6xr4oUbtklV/Zjmng97p6OMQq34tVuCqEqXkQrM0lLfk1g50vbNlJpsGei7ec%2B2FrKny2qsUa38NadRm73YUS4IHn5dvu6gaZHwwbCt4fCmgQ7BdhUjSzfh3PdlNsmH3foYHcASMiFIJw1ADHU2TUl0M5Zb8g3CL557gh5VVfX4p0uPe5iUGkwLeMGcGJd5BFdjrwYoVIexTFtdKaMiaf7ORgzwFLast433AzVXFTYxJyn3RNklMGRBLptA/tYyGrUJLUu1hcmMEeRJWhhSlsAXqmwZTCNoN4t7aPJ28ok4AKdyobMZR5EchqkadsuPbkloe/tg6jJ57ge4OMRydY%2BSdB0IMMyJukYnlpk3FAoB3Nbpe7Z8wz9nmgCCHmUw1WBNTFA7j3wmqmQzRbiWdqEAIeYe5x/%2BwDCwzMhY%2BxWHZ5r7EU5ixdUkXf7sHSVc/v4Xf33u3uNa6qtGlosQYeV4atEk4R3dmV/LD6Fa4ZSDxR7wjfj4dDmmN3l5rY5xjzN%2BmTchifvWv1MHQg5sAuWUex8sn3R3q2tQNuJhPWH2AEWTdqQrzm7irhkEPOJDclIQGhgYHDvmK1dOcF3ffNdsxZXuVhNp2MKGulRZAOvgpMi1GDZQVXQ3BJfdIg0V2eBnuCohV3kCfS6RW0VM4HVwMqwK7N%2B7ZuDN7kFUBrFwzR23SGRnTSAr1nJpKnDjzruehJ8j8wgZV3uCGAKmphzFnuPbusgh0MJUB0vU1QfEOfNr562C6gxOwrWNt2fcmQyR/SFq4qVWlPkEnobOg1cJPeYuqdJSQPZnulmoJXsir4bEBTxW5W%2BWpORowJDX6MhPFs3o1IIRQ1uGxeQgwbdKZ2UIzqI395z%2BOKs/GXrUIropRdyMU6g48KlUEC0wYi98zjz18ljOPRZnJfHYkPkXXWrM4SkdOlpzIUQhj2l4BsER6NxLMlTVuokyVaK3nbs7gFTbWzkYrGzpmoW1lMgnGax3CpKuT2mRLjoanm2h2PoBMYCQ8ybc70BxlE53NxqeF0cQD4hbdBr/gBRnJ9WFEFf7KyggynR%2BrzuFoYBV6fv%2BcIZZ8gn7eyShec5u%2BmjL4x5ry97YcAwmwyVNCA4TCWcA7estEi9JxL8hHjzkc46TrkWf1cr4CGtT7EgfWf2FFAQeGMq26vck5h3sSVGBfUeqg8u1NkJwuh21CEjqSU54edcwvsc09oE7/ZRh3PE3M2HxD7mkRhqfsqzmY0Kv9TM3sLC3nBZ5oI7u9e2ll42uxM7952fL3jNETGkQDMgKCAg6%2BV4upneoBS06MA7MSpVqHCZmPi5fcCd1mpVSXtKAj%2BI6UoVjd89TBDgaeSM3VhPRyeaMHnSAh/4uuXct9Gpx8uXc6ucfLDs1eiiMoK9FdIJOtqdiNjlNvoQFF2uGB/d0JrYDshL7ZiyuPLFY6ypKssXBqEelYAfKnTutvJ48TNfO82V8s2muO8ZGa7VE74xpywxlBICbLCh6RTfmR2ITPseLLqukC%2BbJNjhjpy91z%2Bgyh0kh0G8cIifoLlI92Craufg4ZVJx8YaGHdYJ24orTm9beFOnmsH8LP6EqV7WvscsuZW4nn8ZZC03%2B15RjXDIUUTWWaPDjo6b6Wi7lETIqglfELcbUgbgFvBZ8tM8GZabkpjSq5f8EALvnMbKD0oMiYjGjUHaZ5YcIGs1im7UxyyqcZOySSccWUrTWa3g83kPQZxJdTdQ9ZrTPPumP7cdxR9j1YfOiUorTuhCR9k3xpqNMN6g/VWGw3d0cx910yV3xZSkpY8NC0buiGS5bbpO5NWfNtM40pMcevFUR9zqGovdIG3Otuxm0rjfle6ySFeiQvuDNGJN7rnL2qqACN%2ByOrCB4N1H0VHRF2hP8jARQUKFg25baVCzOXEYwOVt/Ut9%2BVCdckmZaiX%2BnJk5L2ICYl6WwCGnOq50Mwi7AiaxKWOr2xJ2DCK0dF3S2L5pBdv6lizDHxGaKHn8bG/gHbmiHrE3zVURjlNO/AELtdIDYCQIU7spcLhdolNZQZnETcwoSscReTLrxh/urdIvlhK2sy%2Byzms0zq3Y6jgOZQPxslB1JPLCivaTbf9SN5BMFFSwKPdY24SB6Sucnyyk/R60cNeCxemMFMi5%2Bbak4nS9g8bpmMMgmoUmwHjRVzNUkrUaMp0jcOBsJN09HzVt53D1YDpq/LFczuMiWfFuaYnTjboW6K6XOYzaNOBL9TiPBsqTqSYXGhmLTh/oHGWpAi4WN%2B3nZLG5hWYzXmFz22PHoDiGh5jBiVebQcz1FVePVJVpB/ILXzym3NFUaytblqe7zKsbg2XHYFf9QtAEswQUWxe76eJGD2aeEepYE0PJTVP6/4sIgN6OaVlFu9eUZQDaycr7UZV0hs2SLYh5GI9A4U1RcYOEKos1gDqSHue5bnHwIJVlcEvvfYjhVYd%2BhFi6i0rT9/Rn8Y%2BlDyGZykt3EjYWXxtdPkNIH0XutkxVSNI4Ggj2Lna0nFEE02noYfiIHBZvl8ZrWo7Hy9hwFnnbaa5W30rvHLDOblDdkNOt5tljiXCvq5gfK1taktSor2S8xzV8eZMi/EADbV03YeqdGxSvMeyNSwiGOds%2BrLVjDqTdUTAKq/7%2BykmbHKJju0MVBo69/4kII%2BV5V57V9U%2BzvUDok1V314HExPVhr0b/HbGIT8uRlps0DxCJA6ciEslUqqJcjldQStE%2B4q2x33u2nhLuC63mvSA2%2BV2IUTMT0ERx6QCqxOgAbfKpVUAyNX0OnrNdpqnAju9n3VYNK5jx0iLR4prM0n4%2BET47AUT1LHPqoQeucAUuMOZqas1tQ6IJR%2BT3i5llIUHV9JMo9F0pxN6pSf0qsmtM2hPicYCeawT4WXb7xPLVttpuwUb7hWcKHW2sAbBnusMtjvoLO%2B1wqNga5nEu7wxTU%2Bo0yaiYLYNuwj0niwWJFOu/D7C2M4/TwyEk%2BBEixv63iCyYKKpMhBCiQXinsRWm83a98wW7yIo86lKYeEJFl56JaGsyvp5y%2BvbGg6PFNlTcgCcoQLfI/VdWZ7khvBjrg5BjkiR5jA5pqQuHphMV%2BQGDoGAXySbE6jQwtVhQykJeHHSk7x2B7tQMTRgEiY7BM9Vc%2BIyEhOdu97nyqPvlIHahFm0Fvy4kdJRlTEynx2CqDWLD9ZSP09zgtpk6fhLzT1f916mrfbN%2B3bLj3eljhLjA4%2B1VCUsy8h8iGstu2I3CnRaOZVxBd1Uelhq80SmIySLxI%2Be%2B61CNC4Xv%2B2uQeswjp5UzOpA9lMIhQfBdk21acE9riH62bNGCYZlKaPDqzxuZb41nuENPu8ea9aasJMYxNZD6Dgzyckz4nZQqflOuayzMqutqZsjbed5QkYJh97TNvd2n1%2BhN6uLb2dyFVm8N9ACGLk8tQt%2BGSb5GLM4l03mcDlj2A%2BBVEl7wo7WxJbxY3v3gXMVdoOF5s647B4LJEGFNN6cSGu%2Bxc9K95MszM75qlA7ifFLAF8PfK4Uia/Yl0aKQCmCpBeIURCWVxfsIrO4SVhKIG0NrKn6IQLwFfDuYGdHpzhFEe%2BFne5EkrZAAK/L64wdcNdyNafM7VXQWJ3I1MGHIe7pr1ekmsqQkBxGPltSPZrSSGdhkvzsb67m7wfwXy7nf/z5dP5xF//26vR37z4/3oh%2BPIV9/S9Hvrur
These samples have been taken from a very interesting website around saml: https://www.samltool.com/
Integration problem with SAML
Like any other IT implementation, sometimes it turns out the integration doesn’t work as expected. Sometimes the Service Provider is unable to authenticate the user with the parameters received from the Identity Provider.
In some corporate environments where a lot of commercial solutions are used, it can sometimes be tricky to troubleshoot and debug SAML authentication issues. Usually you don’t easily have access to debugging information from either the Service Provider or the Identity Provider.
In my case, the Identity Provider was Azure where I didn’t have any kind of privileges to get debugging details. The Service Provider was an old legacy commercial application where even the trace log level didn’t provide much information to understand why the authentication failed.
Running out of idea, the next actions were simple: read the content of these SAML Request and SAML Response.
Debugging SAML messages
Debugging SAML request
The SAML Request presented before is definitely not readable as it is:
eJx9U8uO2jAU3c9XWN5DnAyPxoJUFPpAohAR2kU3lWvfFEvxo7YzQ/%2B%2BTgpVKs2wsmSfc%2B45914vPFONpas2nPURfrXgA7qoRnvaPyxx6zQ1zEtPNVPgaeC0Wn3e0WxMqHUmGG4aPKDcZzDvwQVpNEbbzRIf9u93h4/b/fc3JJ%2BTeU3II2FTQcgsIzwXuajzeT3L5qyGXHCeTTD6Cs5H/hJHOYxKZ56kALePlZa4KlGIAaK29y1stQ9Mh4gk6WRE5qN0dsoe6TSjk%2Bk3jDYRKTULvdg5BEuTRAo7hgtTtoExNyqpqkMF7klyGNuz7cv1gd9JLaT%2BeT/rj78gTz%2BdTuWoPFQnjFa3/GujfavAXeW/HHf/TPj/PQhQJk2iFlw6E28Z97h4QGjRdZv2SV1xj6ogMMEC69iLZMi6qVja9W%2B7KU0j%2BW/0wTjFwuvh0nHa30gxqnsoBcVksxLCgfcxZNOY57UDFuJMgmsBJ4NS1y0D0e9c7EOAS0Broyxz0nfDiBF46DPeUg6h6yYu0RHq4u6ecco7XLwu4/FsnOhmBzzWPTmmvTUuXJvxonjnN7ljuHi4PQ%2B/TvEHIUUmDw%3D%3D
It turns out, the SAML Request and Response are compressed and (obviously) base64 encoded. Because these are used in HTTP Request parameters, they’re also URL encoded.

To be able to de-obfuscate the SAML Request, we need to apply in order:
- URL unquote
- Base64 decode
- Inflate
import zlib import base64 from urllib.parse import unquote request = "eJx9U8uO2jAU3c9XWN5DnAyPxoJ..." unquoted_request = unquote(request) decoded_request = base64.b64decode(unquoted_request) decompressed_request = zlib.decompress(decoded_request) print(decompressed_request.decode('UTF-8'))
The result is:
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="ONELOGIN_809707f0030a5d00620c9d9df97f627afe9dcc24" Version="2.0" ProviderName="SP test" IssueInstant="2014-07-16T23:52:45Z" Destination="http://idp.example.com/SSOService.php" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="http://sp.example.com/demo1/index.php?acs"> <saml:Issuer>http://sp.example.com/demo1/metadata.php</saml:Issuer> <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/> <samlp:RequestedAuthnContext Comparison="exact"> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef> </samlp:RequestedAuthnContext> </samlp:AuthnRequest>
Debugging SAML response
The most interesting part is the SAML response. If we de-obfuscate the response like we did above, we have the following:
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"> <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </samlp:Status> <saml:EncryptedAssertion> <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Type="http://www.w3.org/2001/04/xmlenc#Element"> <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/> <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"> <xenc:EncryptedKey> <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-OAEP"/> <xenc:CipherData><xenc:CipherValue>ly1gJyn0xZPGW3zH20VKEab3J+kXEJQ3XoSmgNf3H7Ub5HntefP49xlk7hlqOsRtBVBsPd7sP1IUt90jqDFDWz0Km16TV8Odx6UXpaYsq9Hah+UP3yiES2Z+3UBsnaC8NPI5Vd5P+n8BtXDIw0L4n1gkYl6U3s6HQvGqmIbG4lY=</xenc:CipherValue></xenc:CipherData> </xenc:EncryptedKey> </dsig:KeyInfo> <xenc:CipherData> <xenc:CipherValue>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</xenc:CipherValue> </xenc:CipherData> </xenc:EncryptedData> </saml:EncryptedAssertion> </samlp:Response>
The actual authentication information with user attributes is what we are looking for debugging. This information is encrypted and stored in the CipherData tag under EncryptedAssertion. The EncryptedData tag provides useful information about how it has been encrypted
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
The information seems to have been encrypted with AES 128 bits with CBC mode. AES is a symmetric cryptographic algorithm where a unique key is used to both encrypt and decrypt.
There is no such shared symmetric secret key between the Service Provider and the Identity Provider. The key is actually bundled into the response as well! In order to prevent anyone to decrypt the content, the symmetric key is itself encrypted inside the EncrypedKey tag. It also provide useful information about how the key has been encrypted:
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-OAEP"/>
This time, the content (the symmetric key) has been encrypted with RSA and the OAEP padding scheme. During the initial SAML integration, you need to exchange metadata between the Service Provider and the Identity Provider. These metadata includes certificate linked to their respective RSA private key. The public key from the Service Provider certificate is used to encrypt the symmetric key.
Because I was managing the on-premises Service Provider, I was easily able to get the hands on the RSA private key. Without having access to this key, it is fortunately almost impossible to decrypt the SAML Response.
Knowing all these valuable information, we need to perform in order:
- Decrypt the symmetric key with the RSA private key.
- Decrypt the authentication information using the decrypted symmetric key.
Decrypt the saml symmetric key
Decrypting the symmetric key is pretty easy as we can rely on libraries.
from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA import base64 encrypted_key = 'ly1gJyn0xZPGW3zH20VKEab3J+kX...' key = RSA.importKey(open('key.pem').read()) cipher = PKCS1_OAEP.new(key) clear_key = cipher.decrypt(base64.b64decode(encrypted_key)) print(clear_key.hex()) print(len(clear_key))
The output tells us the key value:
cb0bb80fff69797dabdef51463fa3cb8
And the length confirms the decryption was a success (OAEP padding):
16
They key is a valid AES 128 bits key.
Decrypt the saml statements
If we look at how the CBC cipher mode works:

We see that we are missing the Initialization Vector to be able to decrypt the SAML Response. It turns out the IV is the first bytes of the ciphertext. As we have here a AES-128 encryption, we can assume the IV is the first 16 bytes.
import base64 encrypted_text = 'aUDIiZxbfYvPvEgOf/9IAp8CpGL1sm4XXAYJY1wrbtcu2a5RZ...' raw = base64.b64decode(encrypted_text) iv = raw[:16] cipher_text = raw[16:]
As soon as you have the initialization vector, the key and the cipher text, the decryption is straightforward:
from Crypto.Cipher import AES cipher = AES.new(clear_key, AES.MODE_CBC, iv=iv) plaintext = cipher.decrypt(cipher_text)
Before using the plaintext value, you’ll have to manually remove the padding. It turns the last byte of the plaintext is the size of the padding.
plaintext = plaintext[:-plaintext[-1]]
At the end, we have the following code:
from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA from Crypto.Cipher import AES import base64 encrypted_key = 'ly1gJyn0xZPGW3zH20VKEab3J+kXEJQ3X...' encrypted_text = 'aUDIiZxbfYvPvEgOf/9IAp8CpGL1sm4XX...' key = RSA.importKey(open('key.pem').read()) cipher = PKCS1_OAEP.new(key) clear_key = cipher.decrypt(base64.b64decode(encrypted_key)) raw = base64.b64decode(encrypted_text) iv = raw[:16] cipher_text = raw[16:] cipher = AES.new(clear_key, AES.MODE_CBC, iv=iv) plaintext = cipher.decrypt(cipher_text) plaintext = plaintext[:-plaintext[-1]] print(plaintext.decode('UTF-8'))
Which gives:
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"> <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </samlp:Status> <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z"> <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer> <saml:Subject> <saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID> <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/> </saml:SubjectConfirmation> </saml:Subject> <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z"> <saml:AudienceRestriction> <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience> </saml:AudienceRestriction> </saml:Conditions> <saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93"> <saml:AuthnContext> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef> </saml:AuthnContext> </saml:AuthnStatement> <saml:AttributeStatement> <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue> </saml:Attribute> <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue> </saml:Attribute> <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue> <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue> </saml:Attribute> </saml:AttributeStatement> </saml:Assertion> </samlp:Response>
Going Beyond
The are two interesting mechanisms we can exploit to decrypt the SAML Response without the private key.
- CBC padding attack to decrypt the authentication information.
- Bleichenbacher’s attack if the symmetric key is encrypted with PKCS1_5.
CBC padding attack
This is only exploitable if a padding oracle is available. A demonstration of this attack is shown here: https://blog.compass-security.com/2021/09/saml-padding-oracle/.
You’ll see that most of Identity provider are still using CBC as cipher block mode. If the Service provider expose a padding oracle, it will definitely be vulnerable.
This attack could be easily prevented by the Identity Provider if the authentication information is encrypted using AES-GCM instead of AES-CBC for instance.
Bleichenbacher's attack
I haven’t seen a SAML Response key encrypted with PKCS1_5 for obvious reasons. Most of public Identity Providers uses OAEP as padding scheme to prevent this attack. However some miss-configured self-managed Identity Provider could still use PKCS1_5.
In this case, an attacker could easily retrieve the symmetric key and possibly impersonate a user by forging an encrypted SAML Response. If the service provider is also vulnerable, the attacker could exploit a XSW vulnerability from the Service Provider to bypass the signature. Note that, an external signature is often used as well.
A python bleichenbacher’s attack implementation: https://github.com/emilystamm/rsa-bleichenbacher
More about XSW attacks: https://book.hacktricks.xyz/pentesting-web/saml-attacks