« Back to home

Identity Providers for RedTeamers

In my previous blog post, I looked at effective techniques for operating against Okta during a RedTeam post-exploitation phase. And while Okta is certainly a popular provider, they are by no means the only company that I come across during assessments.

In this blog post, we’re going to take a more generic look at other providers, to see just what works, and what new things we need to be aware of.

This blog post follows a talk I gave at SO-CON 2024 of the same title. The slides for this talk can be found here, and this blog post aims to expand on some of the technical information presented.

First up.. Ranting

So, one of the things that I wanted to address up front is the inevitable “Aren’t you giving techniques to The Bad Guys”. And that is a valid question, however when I was looking for existing attacks against Identity Providers, it didn’t take long to see that multiple attackers are already using some of these techniques during campaigns.

For example, if we take a look at the MGM breach, the following is an extract documenting the ALPHV group’s TTP’s during the campaign:

Now this isn’t new, in fact if we go back to 2019, we can see that I also discussed this technique. And I’ve no doubt that it was discussed many times before by other researchers. And yet we’re still seeing compromises using this technique along with the subsequent surprise that this actually works.

Similar if we look at APT29 and their Sunburst campaign:

The Golden SAML technique again caught some by surprise, but this was disclosed by CyberArk way back in 2017.

So, the missing link here seems to be the knowledge of these techniques rather than providing attackers with something “new”. Hopefully by discussing things out in the open, we can level the playing field, providing defenders with the data they need to form detections, while giving RedTeam the techniques they need to simulate this for their clients.

I will also point out that nothing in this blog post is related to a vulnerability in any of the providers discussed (that’s what Bug Bounty non-disclosure agreements are for after all :/). All of the techniques require elevated privileges, use SAML features only accessible once keying material is compromised, and/or are by design as part of providing a service to authenticate users.

That being said, as we continue to require access beyond an identity providers dashboard, techniques like those discussed here are used… and both defenders and RedTeamers need to be aware of how they work.

LogonUserW

OK, let’s start off with something very common… LogonUserW hooking. This is a method commonly used by attackers when looking to grab plain-text credentials after compromising a server running an Active Directory sync or pass through authentication function. To recap, in 2019 I looked at how Azure AD Connect allows attackers to hook LogonUserW when deployed in Pass-Through Authentication mode:

If we look at other providers, we see that most use a similar method of validating credentials during authentication.

Here we have Okta’s OktaAgentService.exe using P/Invoke to call to LogonUser:

Similar, in OneLogin we have LogonUser being invoked in Microsoft.ApplicationProxy.Connector.Runtime.dll:

With Entra ID Connect, we basically find the same code as in the previous Azure AD Connect instance:

Finally we have Ping Identity who actually do something very different which we’ll discuss later, so they don’t appear to be vulnerable to the techniques used against this API call:

So of course if we hook LogonUserW within any of these agents, we’re going to see plain-text credentials. I’ll show this here using a x64dbg where we retrieve credentials entered during authentication against an Okta portal:

This also means that injecting a DLL into either the Entra ID, OneLogin or Okta AD agents will result in a similar retrieval of credentials to that of Azure AD Connect tool that I showed back in 2019:

A very simple tool to do this can be found at https://github.com/xpn/CloudInject if you want to try this for yourself.

Agent Spoofing

But what happens if we want to take credentials for the agent and run it outside of the target environment? Well in this case, we can turn to what I’ve been calling “Agent Spoofing”. This technique is where we recreate just enough agent functionality to connect to the identity provider, and extract user credentials without having to be present within the client network.

In the Okta for RedTeamers post I released Cloud-Nine, which is a tool to spoof Okta agent communication. But just how easy is this to do with some of the other providers?

OneLogin Agent Spoofing

Let’s start with OneLogin, which stores its API key in the registry at:

HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\OneLogin, Inc.\Active Directory Connector

If we extract this token from a server running the Active Directory Connector, we can spoof the agent from outside of the client environment by making the correct API calls.

NOTE: What I’ve noticed is that attempting to re-use an existing OneLogin token between two agents actually results in the first connector refusing to respond to requests for authentication. The only way to fix this is to restart the connector service. Factor this in and always test anything presented within a lab environment.

With that said, let’s look at how OneLogin HTTP protocol actually works when receiving authentication requests.

First up there is the configuration request, which is used to retrieve information on the tenant. A HTTP GET call is made to https://api.onelogin.com/api/adc/v4/configuration with the following URL parameters:

  • version - Set to the agent version
  • token - Set to Directory Token taken from registry
  • mux - Set to 1
  • directory_token - Directory Token taken from registry
  • adcVersion - Also set to the agent version

The following information is returned from the request (for brevity I’ve included only the bits we care about below):

<configuration>
  <api_key>1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1234</api_key>
  <base_dn></base_dn>
  <directory_id>12345</directory_id>
  <connector_id>67890</connector_id>
  <authentication_attribute>email</authentication_attribute>
  <provisioning_enabled></provisioning_enabled>
  <sync_disabled_users>false</sync_disabled_users>
  <is_primary>true</is_primary>
  <deletion_enabled>false</deletion_enabled>
  <fields></fields>
  ...
</configuration>

Next we send over events to notify OneLogin that our fake agent has started. This is a POST request made to https://api.onelogin.com/api/adc/v4/events/ with URL parameters of:

  • api_key - API Key returned from configuration request
  • directory_id - Directory ID from configuration request
  • directory_token - Directory Token taken from registry
  • adc_version - Set to the agent version

And we send the following XML:

<event>
  <directory-id>12345</directory-id>
  <event-type-id>44</event-type-id>
  <host>EXAMPLE.internal.local</host>
  <notes>The connector was successfully started</notes>
</event>

Then we need to make a call to retrieve a list of current users. This is a call made to https://api.onelogin.com/api/adc/v4/users/ which returns the following XML:

<user>
  <user_id>123456789</user_id>
  <external_id>AAAAAAAAAAAAAAAAAAAAAAA==</external_id>
  <attributes>
    <attribute value="NAMEHERE" type="ldap" name="givenName" operation="create" />
    <attribute value="SURNAMEHERE" type="ldap" name="sn" operation="create" />
    <attribute value="testaccount@internal.local" type="ldap" name="mail" operation="create" />
    <attribute value="CN=NAMEHERE SURNAMEHERE,OU=Test,DC=internal,DC=local" type="ldap" name="distinguishedName" operation="create" />
    <attribute value="testaccount@internal.local" type="ldap" name="userPrincipalName" operation="create" />
    <attribute value="testaccount" type="ldap" name="sAMAccountName" operation="create" />
    <attribute value="CN=NAMEHERE SURNAMEHERE,OU=Test,DC=internal,DC=local" type="ldap" name="memberOf" operation="create" />
    <attribute value="1" type="onelogin" name="status" operation="create" />
  </attributes>
</user>

Finally we start up a websocket session targeting https://smux.us.onelogin.com/socket.io/. This uses socket.io on the server side, but on the agent side, the socket.io protocol is handled by hand crafting requests. This works for OneLogin, so it’s good enough for us!

There are three types of inbound messages that we will handle to support our spoofed agent functionality:

  • ping - A simple ping/pong used to avoid timing out the websocket connection
  • auth_req - An inbound authentication request that needs to be responded to
  • reload_config - Send when a configuration change needs to be handed by our agent

The message we’re obviously interested in the most is auth_req. This message is sent with a number of values used to decrypt the username / password sent:

  • iv - The IV used by AES to decrypt the username and password
  • user - An encrypted username
  • pass - An encrypted password

The username and password are encrypted using AES in CBC mode. The decryption key is the previously retrieved api_key from the configuration API response which is SHA1 hashed, with the first 32 bytes of ASCII hex being the actual key. The 100% totally not OpenAI generated code to produce the key looks like this:

def _generate_decryption_key(self):
  return hashlib.sha1(self.cryptoKey.encode('utf-8')).hexdigest()[:32]

Now I’m not sure why OneLogin re-encrypt the username / password given that communication is all done over TLS, but in any case, decrypting these credentials allows us to extract entered usernames and passwords from the OneLogin portal as they are entered:

The code for this agent can be found at https://github.com/xpn/OneLoginPostExToolkit.

Ping Identity Agent Spoofing

Now Ping Identity’s Gateway agent is another beast entirely. It doesn’t use .NET, it doesn’t even run on Windows exclusively. By default, the Ping Gateway is actually a Java app and runs in a Docker container. Credentials are stored within the gatewayCredentials environment variable:

These credentials are actually just a JWT:

As for the agent, well for our purposes it’s essentially an LDAP client. Which doesn’t sound too bad, but there’s a lot of Java serialization / deserialization going on.

To make life a little easier, I decided to reuse some of the libraries used by Ping to do some of the heavy lifting. But let’s take a look at the protocol.

To kick off the process, a websocket is connected to wss://gateways.pingone.eu (or wss://gateways.pingone.com if in the US).

A number of HTTP headers need to be present in the initial HTTPS connection request:

  • Ping-Gateway-Instance-Hostname - Hostname of instance running agent
  • Ping-Gateway-Instance-Id - Random UUID
  • Ping-Gateway-Instance-Version - Version of the agent
  • Ping-Gateway-Token - Authentication Token / JWT taken from variable
  • Ping-Gateway-Type - Type of Ping Gateway (ldap-gateway)

Once started, we need to send a websocket initialize message to inform Ping that our agent is alive and ready to accept connections:

{
	"messageType":"initialize",
	"logMessages":[],
	"requestId":"be95e956-e840-4974-a7bd-88e7a13c5eb1",
	"data": {
		"instanceStats": {
			"unavailableDetails":null,
			"health":"HEALTHY"
		}
	}
}

We can then receive one of several message types, but the ones that we’re going to pay attention to are:

  • configuration - Contains information on how to connect to the LDAP server
  • rawLdapSearchesThenOpRequest - An LDAP search to identify the entered user, followed by a LDAP bind with credentials to verify authentication

In the configuration message, an interesting field is returned revealing the username and password used by the agent to authenticate to LDAP. This isn’t retrievable using the website as we can see when viewed:

But using the agent, we are sent the password as part of the design:

{
  "data": {
    "updatedAt": 1699568785060,
    "ldapConnectionDetailsJSONSpecification": "{\"server-details\":{\"failover-set\":{\"failover-order\":[{\"single-server\":{\"address\":\"dc.internal.local\",\"port\":389}}]}},\"communication-security\":{\"security-type\":\"StartTLS\",\"trust-all-certificates\":true,\"trust-expired-certificates\":true},\"authentication-details\":{\"authentication-type\":\"simple\",\"dn\":\"cn=pingservice,cn=users,dc=internal,dc=local\",\"password\":\"SecureP$assw0rd1\"},\"connection-pool-options\":{\"maximum-connection-age-millis\":300000,\"retry-failed-operations-due-to-invalid-connections\":true},\"connection-options\":{\"follow-referrals\":false}}",
    "initialConnections": 1,
    "maximumConnections": 100
  },
  "messageType": "configuration"
}

If this account is the same account used for Kerberos authentication (if enabled), it means that we may be able to also pull off a silver ticket attack (see below).

But anyway, the rawLdapSearchesThenOpRequest message sent by Ping is actually what contains credentials.

{
  "messageType": "rawLdapSearchesThenOpRequest",
  "requestId": "9218f72a-c6fc-471d-1739-f079e84567af",
  "timeout": null,
  "callbackUrl": "{\"keyId\":\"163f1caa-1f1f-6166-b352-d74165973971\",\"encryptedData\":\"9TxFrSJI3efOBSJqaFSm4z3LxSK5TCMfeMHWIUmduD1LmfoAdDPNEmkX/b6gI2k20hW6DnCzATepte0EsyiofQIZClM9viZAag4SmAXeIu5AFlAwrstAlCH/JcCwA5xeHBOD7brVb+Ba+lj9BBioLmAQrBd5BQ20/rWMkCrZ1Cl3sh/7oVD1/qnfNeCiwb4yYy8NfMpWCIHQ=\"}",
  "data": {
    "searchRequests": [
      {
        "asn1": "MGkCAQBjZAQXT1U9cGluZyxEQz1pbnRlcm5hbCxEQz1sb2NhbAoBAgoBAAIBAAIBAAEBAKEyoxwEDnNBTUFjY291bnROYW1lBApteXVzZXJuYW1loxIEBG1haWwECm15dXNlcm5hbWUwBgQBKgQBKwo=",
        "requestClass": "com.unboundid.ldap.sdk.SearchRequest"
      }
    ],
    "postSearchRequest": {
      "asn1": "MEICAQBgPQIBAwQlY249dGhpcy1pcy1yZXBsYWNlZC13aXRoLW1hdGNoZWQtdXNlcoARdGhpc2lzdGhlUGFzc3dvcmQ=",
      "requestClass": "com.unboundid.ldap.sdk.SimpleBindRequest"
    }
  },
  "loggingPropertyMap": {},
  "responseRequired": true
}

If we base64 decode the postSearchRequest we find the password that we are looking for:

This is enough to allow us to recreate the agent in Java (ugh), which will connect to Ping and allow parsing of credentials provided at the portal:

The code for this agent can be found at https://github.com/xpn/PingPostExToolkit

Entra ID Agent Spoofing

We won’t go into this agent in this blog post as this research has already been done by @DrAzureAD in his blog post here.

What I did find interesting however was the approach that AADInternals uses to achieve this, which is to instrument the legitimate Azure AD Connect binary by injecting in a DLL. The result is the same.. plain text credentials extracted:

Kerberos Authentication

Next up is Kerberos Authentication, which we looked at with Okta in the previous post. There we found that Kerberos authentication was actually a pretty nice way to move from a compromised endpoint to the SSO provider without much effort.

Again let’s take a look at each of our other providers to see if something similar can be used there to achieve a foothold.

Ping Identity Kerberos Authentication

First up is Ping which can be configured to support Kerberos authentication, with the following SPN’s being a tell-tale sign that Kerberos may be in use on the network:

HTTP/kerberos.pingone.com
HTTP/kerberos.pingone.asia
HTTP/kerberos.pingone.ca
HTTP/kerberos.pingone.eu

For authenticating to Ping, we follow a similar path to that of Okta in the last post, essentially grabbing a service ticket for kerberos.pingone.TLD and using pass-the-cache with Mimikatz on our attacker controlled machine.

We can also craft a silver ticket if we compromise the service account used by Ping to encrypt service tickets:

A quick demo of this in action:

OneLogin Kerberos

OneLogin is actually different in this case, there isn’t any traditional Kerberos web endpoints, instead an internal connector listens on a HTTP port for IWA. This isn’t something that I looked at during this research run, but is certainly something that should be looked at in the future!

Entra ID Kerberos

Again the precedent has been set for this technique by Edwin David who has blogged about techniques to extract and use Kerberos tickets to access Azure accounts here.

SAML Attacks

OK, so SAML attacks are an interesting one because if pulled off, they allow authenticated access to other accounts on the IdP without having to do something like change the password for the user account.

@TekDefense first inspired me here with his blog post. He showed how updating the subject of a SAML token in Okta could result in access to service providers as different user accounts. While not a vulnerability, it was a neat to see when it comes to manipulating the SAML tokens issued.

But what about adding in external IDP’s to authenticate to Okta / Ping / OneLogin or Entra ID as arbitrary users? Well in the Okta blog post I showed that by crafting a simple IdP (named malIDP), we could pull this off quite easily, but let’s also take a look at the other providers to see the potential there.

Ping External IDP

First up is Ping. Things look hopeful when we start looking to add in an external IDP. As with Okta, we don’t need to verify the domain of the IdP URL, which means we can do funky things like add in arbitrary locations to redirect to:

Then we point our local /etc/hosts entry for the IdP URL to capture our browser request as the redirection is done client-side:

We then start malIDP with:

python ./main.py --provider ping --cert ./example.com.crt --key ./example.com.key --metadata metadata.xml --issuer 'https://www.google.com'

Starting the SAML flow is a little more complicated with Ping, as we need to assign our external IdP to an authentication policy:

Then we can use malIDP to craft SAML tokens to authenticate to Ping. We see that we can reference arbitrary users just as with Okta:

However… Ping require the user account be linked on first access from a new IDP, which means you still need to enter in the users existing password:

This is a nice way to protect against this very attack, as it would force us down the route of still having to know a users password, which negates the impact, after all if we knew the passwords of our target accounts.. we wouldn’t bother with this technique. Of course we could still use this for persistence, retaining access to a set of users who are valuable… but it does blunt this as a post-exploit opportunity… good job Ping!

OneLogin External IDP

Now for OneLogin. This process looks very similar to Okta in that we can again register an external IdP with any URL:

We want to set our Issuer value to something that we reference later:

And we make sure that Sign Users into OneLogin is enabled:

Once configured, we start malIDP using:

python ./main.py --provider onelogin --cert ./example.com.crt --key ./example.com.key --issuer 'www.example.com'

Then we start the connection to malIDP by navigating to the following url:

https://tenant-name.onelogin.com/access/initiate?iss=ISSUER_HERE

And unlike Ping, and more like Okta, this time when we try and authenticate using something like MalIDP, we don’t receive any roadblocks which stop us:

Entra ID External IDP

Just to round things off, we’ll look at Entra ID, which was recently covered by Clément Notin here.

Entra ID requires domain verification before federation can be enabled:

However, we can actually hijack the signing for a domain by providing a second signing certificate alongside the existing one:

The other consideration is that each user that we wish to authenticate to needs a ImmutableID attribute assigned. This value is set to the ObjectGUID of an Active Directory user account during a sync, but if the value isn’t assigned, it must be set using something like:

Finally we need to grab the metadata xml to pass to malIDP, which is downloaded from:

https://nexus.microsoftonline-p.com/federationmetadata/saml20/federationmetadata.xml

Once completed, we can use our malIDP instance to authenticate to the user account:

python ./main.py --provider azure --cert ./example.com.crt --key ./example.com.key --metadata federationmetadata.xml --issuer 'http://adfs.lab.local/adfs/services/trust'

We initiate the connection by navigating to malIDP on http://localhost/init and complete the login as usual:

We should note that if the Entra tenant approves ADFS or the external IdP to assert to MFA use via Conditional Access, we will skip the prompts to provide MFA. If not.. MFA will still get in the way!

Phishing

Phishing is another potential use case for Identity Providers when it comes to attacking users. Luke Jennings from Push Security used the Okta cloud-nine agent to demonstrate this in his blog post OktaJacking here.

But what about the other providers out there, is this also a possibility? Well any time we can set up an AD agent with an identity provider, we can extract credentials.

There are limitations however, the main one is that in advance we need to know the email addresses of the users who will be targeted. This is because, without an existing account registered, the connection attempt will never be made to the AD connector, preventing us from extracting credentials.

There is an exception to this case however.

Ping Phishing

It should come as no surprise that Ping can also be used like this, again we recreated the agent, so surfacing credentials entered into the authentication prompt is indended functionality.

One difference however is that we don’t need to know the usernames up front. This is because of how Ping works to verify user credentials, in that a LDAP query is made for the user account. So this means that Ping can be leveraged without having to know the username / email addresses up front.

A quick demo of how this would look:

One Last Thing… Okta FastPass

I wanted to throw this in at the end as I found it interesting when recently looking at Okta on macOS.

Okta Verify is an application which can listen for the user requesting FastPass authentication using the web portal. For example, when a user navigates to their organisations portal, they may get an authentication prompt like:

If the user logs in with FastPass, and the user has the application installed, they may get a prompt such as:

Or if configured, the user may receive no prompt at all, instead the fact that Okta Verify is installed on the laptop and authenticated is enough to pass the required checks to allow the user verification stage to pass.

The way that this works is by the macOS Okta Verify app opening a TCP port 8769 and listening for HTTP requests from the browser. Upon requesting FastPass for authentication, a HTTP request is made to http://localhost:8769/probe:

If the agent is listening, a JWT is passed to the Okta Verify agent. Validation is performed in the Okta Verify agent, and upon the user meeting verification requirements (or lack of if configured), a back-channel communication is made to Okta to forward the result of the verification.

Visualised, the flow would look like:

However, what happens if we have compromised a macOS endpoint and we want to use FastPass to access the users account? As we’re in the lockdown world of Apple, there isn’t a simple way to inject into Okta Verify, or to steal keying material. We also don’t know if the user will receive a popup, giving the game away, or if they will be asked to verify their login… or even if they will receive a silent login which we can exploit?

Well in this case we can take advantage of how the localhost HTTP connection takes place. As an attacker with access to the host, what if we instead trigger the FastPass flow on our own machine, inspect the JWT passed to us, and if we’re happy to.. forward the request onto the target host’s Okta Verify instance using SOCKS? That would give us back some control and decision making during an assessment.

Visualised, the attack would look like this:

To do this I’ve created a quick Python POC named OktaRealFast, which will perform the inspection of the validation requirements, allowing forwarding using SOCKS to the target host if we want to accept the risk of popping up a prompt (maybe seeded with further social engineering). Or allows us to forward if we know in advance that no prompt will be issued.

Let’s take a look at how this works when the organisation has configured Okta to require user verification.

To set the scene, in the top left we have a compromised macOS instance running our SOCKS proxy. Bottom left is our browser running on the attacker host, and right is our OktaRealFast agent running on the attacker host:

If we use OktaRealFast here, we see that we are notified that the user will receive a prompt before we forward this to the user using SOCKS:

Now let’s look at another configuration in Okta that does not require user interaction, but just requires hardware protected, phishing resistant assurance:

This time if we intercept the FastPass request, we can see in advance that the user will not be prompted, allowing us to forward the request to the users macOS workstation over SOCKS, transparently approving the session and permitting us access:

The code for this can be found at https://github.com/xpn/OktaPostExToolkit/.

Conclusion

And that’s everything for now. When looking for options for monitoring any of these techniques, there did appear to be some interesting posts for Okta:

For Ping and OneLogin… not so much.