Advisories 1-2: Azure AD and Common WS-Trust MFA Bypass explained

If there is single post I’ve been delaying for a good while, then it’s gotta be this one. This blog details a common oversight in MFA enforcement regarding federation implementations where MFA is invoked and required in the 3rd party IDP only. This oversight become effective when several by- design features, and implementation decisions align in ”wrong” way

Well how actionable it is? I’ve been exploiting this oversight in Red Teaming assignments, so in my experience its been really popular in deployments where the MFA is not checked on cloud side.

The reason of the delay is advisory 1. I’ve been discussing the ramifications with a major vendor providing Federation solutions about mitigating the configuration risks with WS-Trust when used in conjunction with Azure AD

Advisory 1 is related to how WS-Trust protocol is used in conjunction with Azure AD where clear text username and password are forwarded in TLS protected transport using the active endpoints to provide access for so-called non modern clients (Rich /Active endpoint consuming clients)

The scope is protocol implementation related, not vendor related! List of third 3rd party vendors providing WS-Federation/Trust support for Azure AD is long (some 15 last time I checked), so I am not highlighting any vendors here. I am assuming this affects most of the vendors implementing the WS-Trust with Azure AD without knowledge of the Azure AD OAuth2 Grant Types. Obviously I can’t say anything on any of the vendors behalf.

Microsoft’s AD FS can be also considered applicable in this context. Especially advisory 2

Advisory 2 is already documented, but not well communicated, and the existing documentation is AD FS specific. How is advisory 2 related to advisory 1? The existence of advisory 2 makes advisory 1 possible in the described scope.

Scope

Scope of this advisory are primarily customers who use WS /* -Protocols for federated domains in Azure AD, and utilize access policies to enforce and bypass MFA only in the IDP side. Not checking the status of MFA in Conditional Access, or using the -SupportsMFA option for the Microsoft MFA enabled users.

Its worth noting, that the attack can also pierce cloud MFA requirement if EAS clients can generate known bypass claims. There are unfortunately guides on web that advise doing this. In the latter scenario the attack penetrates two MFA layers, (Becomes even worse)

I won’t be providing any PoC’s, since all of the below advisories rely on by-design implementation, and can be replicated in few minutes, given you have understanding of both sides of the equation (Azure AD and WS-protocols)

Advisory 1 – Pivoting from legacy to modern auth

Background

UserName endpoints on WS-Trust are usually associated with legacy clients, thus they are often excluded from MFA. These endpoints are usually associated with legacy clients requesting access to Azure AD relying Party (Office 365 services often) – This is only true by association, as WS-Specification doesn’t define any difference between the so-called modern and legacy clients, the concept of difference only exists within Azure AD /Office 365 context, which the federation service is not aware of.

Accessing ”modern” API’s unprotected by the IDP

SAML assertion can extracted from the IDP, if the IDP is thinking it’s ActiveSync client. Once the token is received in response, it is then converted to OAuth2 Access Token with “saml1_1-bearer” OAuth2 Grant. This can be done by using many of the existing public clients.

Effectively the SAML token destined to legacy endpoint in Azure AD is converted to “Modern Auth” OAuth2 Access Token, and not being just used for legacy protocols anymore.

Root of this problem is related to the fact, that for SAML response from WS-Trust endpoint there is no ”validFor” context, which would dictate, that this request is valid for only let’s say https://graph.microsoft.com – not entire Azure AD Federation (Conditional Access solves this issue, as it binds access in cloud to different API’s)

In the picture: MFA is enabled for browser clients, and UserNameEndpoint is only allowed for ActiveSync clients without MFA /There lies the problem…

But wait, I have disabled legacy protocols in Office 365?

Disabling legacy protocols doesn’t help in this case, as the attacker is only consuming “modern API’s” (Microsoft OAuth2 parlance).

Attack in practice – On-prem MFA bypass

Example: The deployment only allows native ActiveSync to access WS-Trust’s Username bindings without MFA

What attacker does: Attacker uses ‘X-MS-Application’ header to spoof client to fetch SAML token using WS-Trust’s ’UserNameWSTrustBinding’.

The attacker then exchanges the SAML token in Azure AD to access the OAuth2 SPN (Audience). The exchange of the token is possible due to explicit support of “saml1_1-bearer” oAuth2 Grant Type, which cant be disabled on any of the ServicePrincipals in AzureAD

Documentation regarding the flow type

Link Microsoft identity platform and OAuth 2.0 SAML bearer assertion flow

Advisory 2 – Client Header Based Access Policies (Applies to WS-Federate as well)

This advisory is well documented, and is here only as reminder because it allows the first advisory to exist.

Don’t use user spoof-able headers as an basis for access policies at IDP’s controlling access to Azure AD.

Access Policies

Some vendors might implement additional checks here, but the protocol itself doesn’t dictate those to be done. This is basically where different products provide access policies to be enforced claims pipeline, to produce authorization result.

AD FS roles
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/technical-reference/the-role-of-the-claims-pipeline

Business policy claims?

Some of the claims presented in the following category should be only implemented to support business policies, not security policies, as advised in the Microsoft’s article

Client Spoof-able headers (Examples from AD FS)

Multiple claims exist which clients can modify, or set. Below are the most common ones. Read the article if you’re curious to see the list of claims in AD FS

  • X-MS-FORWARDED-CLIENT-IP
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/access-control-policies-w2k12#x-ms-forwarded-client-ip
  • X-MS-CLIENT-APPLICATION
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/access-control-policies-w2k12#x-ms-client-application
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/access-control-policies-w2k12#scenario3

The documentation exist for AD FS 2012 R2, but it’s applicable to similar implementations including subsequent AD FS versions at least as up to 2016. Be sure to read the article as it lists very clear reasons,

Mitigations

  • a. Use Conditional Access to check whether MFA was actually performed at the 3rd party IDP for the requested API, or if there is valid reason to explicitly bypass the API
  • b. If you don’t have Conditional Access, don’t allow UserNameMixed to be used with spoof-able headers. Basically you wouldn’t allow native clients anymore for ActiveSync, as there isn’t safe header available for this decision to be made in the authorization pipeline

About spoof-able headers

  • X-MS-ADFS-Proxy-Client-IP seems to be based on the actual client IP with WAP, and should be more reliable, than the client writable header X-MS-FORWARDED-CLIENT-IP, which is advised to be used in many scenarios.
  • X-MS-FORWARDED-CLIENT-IP is often advised to be used in many guides because MS Exchange Online Connections pass multiple IP’s in headers, and thus the actual client IP hitting ADFS includes both Exchange and the Client IP’s in the header
    • as the documentation says, is best effort style header, and shouldn’t be used outside of business logic use scenarios
  • X-MS-CLIENT-APPLICATION here isn’t reliable way to inspect ActiveSync header for a legitimate client at the IDP’s side, The only way to allow it without MFA is Conditional Access

Also its worth noting that Basic Auth for Exchange, which is key reason why MFA bypasses on the WS-Trust endpoint are still used is going to be deprecated very soon

Add sAMAccountName to Azure AD Access Token (JWT) with Claims Mapping Policy (and avoiding AADSTS50146)

With the possibilities available (and quite many of blogs) regarding the subject), I cant blame anyone for wondering whats the right way to do this. At least I can present one way that worked for me

Here are the total ways to do it (1. obviously not the JWT token)

  1. With SAML federations you have full claims selection in GUI
  2. Populate optional claims to the API in app registration manifest, given you’ve updated the schema for the particular app
  3. Create custom Claims Policy, to choose emitted claims (The option we’re exploring here)
  4. Query the directory extension claims from Microsoft Graph API appended in to the directory schema extension app* that Graph API can call

Please note, for sAMAccountName we’re not using the approach where we add directory extensions to Graph API queryable application = NO DIRECTORY EXTENSION SYNC IN AAD CONNECT NEEDED


Checklist for using Claims Mapping Policy

Pre: Have Client application, and web API ready before proceeding

#Example App to Add the Claims 

AzureADPreview\Connect-AzureAD

$Definition = [ordered]@{
    "ClaimsMappingPolicy" = [ordered]@{
        "Version" = 1
        "IncludeBasicClaimSet" = $true
        "ClaimsSchema" = @(
            [ordered]@{
                "Source" = "user"
                "ID" = "onpremisessamaccountname"
                "JwtClaimType" = "onpremisessamaccountname"
            }
        )
    }
}

$pol =  New-AzureADPolicy -Definition ($definition | ConvertTo-Json -Depth 3) -DisplayName ("Policy_" + ([System.Guid]::NewGuid().guid) + "_" + $template.Values.claimsschema.JwtClaimType) -Type "ClaimsMappingPolicy" 
 
$entApp =  New-AzureADApplication -DisplayName  ("DemoApp_" + $template.Values.claimsschema.JwtClaimType)
$spnob =  New-AzureADServicePrincipal -DisplayName $entApp.DisplayName -AppId $entApp.AppId 

Add-AzureADServicePrincipalPolicy -Id $spnob.ObjectId -RefObjectId $pol.Id 

#From the GUI change the Identifier and acceptMappedClaims value (From the legacy experience)


  • Generally: The app that will emit the claims is not the one you use as the clientID (Client subscribing to the Audience)
    • Essentially you should create un-trusted client with clientID, and then add under Api permissions the audience/resource you’re using
  • Ensure that SPN has IdentifierURI that matches registered custom domain in the tenant
    • The reasoning is vaguely explained here & here
      • Whatever research work the feedback senders did, it sure looked in depth 🙂
  • Update the app manifest to Accept Mapped Claims
    • Do this in the legacy experience, the new experience at least in my tenant didn’t support updating this particular value
”Insufficient privileges to complete the operation”

if mapped claims are not accepted in manifest, and pre-requisites are not satisfied you might get this error

”AADSTS50146: This application is required to be configured with an application-specific signing key. It is either not configured with one, or the key has expired or is not yet valid. Please contact the application’s administrator.”

  • Below is example for the Manifest changes (AcceptMappedClaims, and verified domain matching URI)
     "id": "901e4433-88a9-4f76-84ca-ddb4ceac8703",
    "acceptMappedClaims": true,
    "accessTokenAcceptedVersion": null,
    "addIns": [],
    "allowPublicClient": null,
    "appId": "9bcda514-7e6a-4702-9a0a-735dfdf248fd",
    "appRoles": [],
    "oauth2AllowUrlPathMatching": false,
    "createdDateTime": "2019-06-05T17:37:58Z",
    "groupMembershipClaims": null,
    "identifierUris": [
        "https://samajwt.dewi.red"
    ],

Testing

References

https://github.com/MicrosoftDocs/azure-docs/issues/5394

https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/develop/active-directory-claims-mapping.md

https://github.com/Azure-Samples/active-directory-dotnet-daemon-certificate-credential#create-a-self-signed-certificate

https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/develop/v2-protocols-oidc.md#fetch-the-openid-connect-metadata-document

https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/develop/active-directory-claims-mapping.md#example-create-and-assign-a-policy-to-include-the-employeeid-and-tenantcountry-as-claims-in-tokens-issued-to-a-service-principal

Azure API Management – JWT validation for multiple Azure AD partner registrations

If there is one popular theme regarding API management, it’s gotta be the subject of facilitating technically partner access via Azure AD. This can be as simple as creating a single app registration to designing full governance model with scopes & roles, and chained App Registrations.

As always, if you want to generally know what is Azure API management, or what is Azure Active Directory, then check the following articles:

Architecture

  • The architecture with new app endpoints enables same app registration to provide native and confidential client access with different settings
  • Scopes are defined per each partner registration

Create Centralized API Management Proxy SPN

  • Here we want to create single-tenant app in order to be able to control roles and authorizations within our tenant for B2B accounts. In multi-tenant scenario this is possible too, but requires that you control most of the authorizations in another management plane, or in scopes only
  • For redirect URI we input nothing, because this application will only act as Service Principal ”Front” for the actual app registrations consuming API management
  • We will use the FQDN of API management as displayName and AppID URL, in later calls we will use this as ’Audience /Resource for the partner clients”

Create Scopes and machine account role for the SPN

There is GUI to define scopes, but I have mine ready on template, so I am gonna use the ”old” experience updating via the manifest to create the scopes
oAuth2Permissions updated via the ”old experience” – Update ”Oauth2Permissions and AppRoles blocks”
 $apps = '{
  "oauth2Permissions": [
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda",
         "id":  "bc388acd-37a3-4358-8839-c84b4add4b23",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "user_impersonation"
     },
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda",
         "id":  "40cfe68d-bef2-4a4b-8944-fc9766ba17d8",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "user_impersonation2"
     },
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user as reader.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda -reader",
         "id":  "a1203e4c-54bf-4e92-bb61-73e1e347cf9d",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf as writer to the API.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda -reader",
         "value":  "Reader"
     },
     {
         "adminConsentDescription":  "Allow the application to access PartnerAPI-Colda on behalf of the signed-in user as Writer.",
         "adminConsentDisplayName":  "Access PartnerAPI-Colda -writer",
         "id":  "b68e3e4f-7f45-497f-a815-6dc6ef46bbc8",
         "isEnabled":  true,
         "type":  "User",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf as Writer.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "Writer"
     },
     {
         "adminConsentDescription":  "Service Account user Scope",
         "adminConsentDisplayName":  "Service Account App permissions",
         "id":  "508b851a-7393-478a-89b0-f83d1dcc825b",
         "isEnabled":  true,
         "type":  "Admin",
         "userConsentDescription":  "Allow the application to access PartnerAPI-Colda on your behalf as Writer.",
         "userConsentDisplayName":  "Access PartnerAPI-Colda",
         "value":  "Service_Accounts"
     }
                          ]
  }' | ConvertFrom-Json

  foreach ( $app in ($apps.oauth2Permissions)) {
  $app.id =([system.guid]::NewGuid()).guid
  }

  $apps |ConvertTo-Json -Depth 3

#Roles


$apps ='{
"appRoles":  [
      {
          "allowedMemberTypes":  [
                                     "application"
                                 ],
          "displayName":  "ReadOnly-ServiceAccount",
          "id":  "03b62322-1913-4c61-86a7-c325ebd5925f",
          "isEnabled":  true,
          "description":  "ReadOnly-ServiceAccount only to read data.",
          "value":  "ReadOnly-ServiceAccount"
      },
      {
          "allowedMemberTypes":  [
                                     "application"
                                 ],
          "displayName":  "ServiceAccounts",
          "id":  "a59b7301-039e-4782-ad1f-fc443cce1786",
          "isEnabled":  true,
          "description":  "Machine 2 Machine -type integrations.",
          "value":  "ServiceAccount"
      }
  ]
    
}' | ConvertFrom-Json 

  foreach ( $app in ($apps.appRoles)) {
  $app.id =([system.guid]::NewGuid()).guid
  }

  $apps |ConvertTo-Json -Depth 3

Create first partner application

  1. The first partner application is for typical use scenario of Client-Credentials Flow and Authorization Code Flow
From API permissions which we just defined on the manifest we can choose, what kind of user permissions (delegated) and application permissions we can set
In this case the partner needs full serviceAccount permissions, which I’ve set require admin consent
Since there is admin permission required I grant the directory access for the App. In the demo this will grant full scopes for the user. Later we can adjust the behavior of consents and pre-consents quite a lot if needed
Optionally, instead of ”allPrincipals consent” you have the option to leave the consent to the end user, if you’ve decided to make some of the scope end user consent -able (Example is the second partner API2)

Configure JWT validation for Partner Application

Each Partner is identified with their unique App ID claim. The control Any is used, to enable proxying of multiple Partner Clients. 401 is thrown if valid and signature checked AppID isnt found in the incoming Bearer Token
  • Note how we don’t need to reference the Proxy SPN at all? That’s because in previous step we’ve configured permissions for the Partner API to consume the Proxy SPN of API Management, and we need to reference the correct audience only

Test the API as a Partner using Code Authorization Flow

  • Response is what you would expect for Echo API’s create with POST-Verb
We are using ADAL binaries to invoke the code authorization flow
  • For PartnerClientID, we use the AppID we created in the previous step
  • We are using the public client redirect-uri to return the token for the user
This is how the Token Maps to JWT validation in Echo API

Test the API as a Partner using Client Credentials Flow

btw… don’t boughter with the client secret 🙂
  • Response is what you would expect for Echo API’s create with POST-Verb

Thats it for now! I will provide more insights, how to use multi-tenant apps, and how transform scopes into correlating back-end calls in the next blog

Demonstration – Illicit consent grant attack in Azure AD / Office 365

Disclaimer: All information contained within this post is common knowledge, given you grasp basic concepts of AAD default behavior,  OAuth2 Authorizations and how Javascript works in browsers.


 

How does it work?

Attacker creates multi-tenant app, and adds non-admin requiring API permissions for the desired API’s.  Attacker then delivers the link to end user, which after clicking the link only has to consent (Approve) the API permissions for the attacker.

Attacker has then access to user mail, this happens because attacker exfiltrates in the background the Access Token and uses it against the Exchange Online API

Down below I describe why it might be hard for the user to discern good app from the bad app.

Why it works?

Multi-tenants app by default have access to end users data, unless your Azure AD Admin have disabled the particular defaults that enable this behavior.

 

Exfil.jpg

Graph API + Other API’s

PoC

  • Create multi-tenant Azure AD Web-App/API + Azure Web App that delivers the JS. client side code to browser from Node.JS Web App
  • I am using minified ADAL.JS as <script src in the code. Other than that, I’ve just added few custom functions to the code, and studied wide variety of existing Vanilla ADAL JS Apps in GitHub
  • Figure out a way to deliver the link to the victim/victims in other AAD / Office 365 tenants
  • I’ve opted to forge existing Microsoft Newsletter with link to malicious Azure Web App to
  • When user clicks the link consent dialog is prompted.

JSA.PNG

  • Consent – This as any other dialog, you would get prompted for when consent framework kicks in (This is where the confidence part happens)

Newsletter.PNG

The process is same for benign apps, so there is superficially nothing to separate the good app from the bad (except caution, which is often unheeded, if you consider how often an mobile application presenting similar dialog is approved for multiple permissions in mobile phone)

  • User is redirected to the malicious app with seemingly benign OIDC-Flow (response_type=id_token)… no mention here of the OAuth2 Implicit Grant Flow (yet)

Redir

  • Hidden iFrame executes the OAuth2 Implicit Grant Flow, and gains Access Token to resource

Tokens

  • AJAX HTTP is used to post the Access Token as Payload to external exfil service
    • In case you wonder, that I’d left the function key there :)… Just try

JAX

  • User is redirected to the original link requested in the mail

OH.PNG

<- Users timeline stops here


 

– >Perpetrators timeline begins here

  • Token is copied by the malicious actor from Azure Functions where it was posted

Exfil

JWTIO

  • A Separate Powershell session running in some dark corner of the world is now searching users mail content for several keywords, and playing with the Access Token for the next 3600 seconds

Passwords

 

See it live:

Evil side:

User side

 

 

 

Microsoft References

https://docs.microsoft.com/en-us/office365/securitycompliance/detect-and-remediate-illicit-consent-grants 

https://blogs.technet.microsoft.com/office365security/defending-against-illicit-consent-grants/

Microsoft has issued multiple items in Secure Score controls about the scenario

Stay tuned for PT2 (How to defend against it)