Introduction
Jump in my plane, fasten your seatbelt, and get ready to configure your infrastructure a bit more securely while you transition some of your workloads and identities into the Azure- and Microsoft 365 cloud.
I’d like to explain how and why you should scope your Azure AD Connect directory sync to only include the necessary (in this case regular users). Next, I’ll showcase an example of what could happen if an adversary compromises a regular user on-premises, which happens to carry higher “hidden” permissions in Azure, enabling the adversary to pivot into the cloud and move laterally between Azure Active Directory and Azure Resources.
Section 1
So you want to transition to a modern hybrid solution for your users, workstation, and infrastructure? Good for you, good for your IT department and good for your employees.
One of the first actions in this journey is to take advantage from Azure Active Directory Connect and enable your on-premises users to be directory synced to Azure Active Directory. This allows them to utilize their same user account for various Office 365 applications such as Exchange Online, OneDrive for Business, SharePoint, and Teams.
You might also enable Password hash synchronization as your default sign-in method, which means the users password will be hashed and stored on your on-premises Active Directory domain service. The passwords are then grabbed by the Azure Active Directory Connect sync service which hashes the hash, and then ships it to the Azure Active Directory authentication service. With this implemented, your users will now be able to sign-in to the mentioned Software-as-a-Service applications with their usual corporate credentials.
With these actions in place, you are now starting to synchronize your user accounts to Azure Active Directory, and this is where you need to think about which users you want to synchronize and scope your settings accordingly. Unfortunately, I frequently encounter Azure Active Directories with directory synced user accounts that holds excessive Active Directory domain services privileges, such as Enterprise- and Domain Admins. These permissions do not grant a single value in Azure, so there is absolutely no reason to synchronize those accounts.
If you later choose to enable password writeback, which enables your users to reset and/or change their password in the cloud, and then have the Azure Active Directory services synchronize that new password down to your on-premises Active Directory domain services, you greatly expand your attack surface by exposing your potential Domain Admin to a password spray or brute force attack from the cloud, giving the adversary the possibility to reset the users account from the cloud and make it travel down to the corporate network.
Now take this approach and flip it. In your Azure Active Directory environment, you have the need to make administrative actions to your configuration and cloud infrastructure. In this case you need to have separate cloud-only administrative accounts with the necessary privileges, and these accounts should have nothing in common or any relationship with your on-premises Active Directory domain services. As with the previous example, a Global Administrator only grants the role bearer permission in Azure, and therefor it is not needed to synchronize that account to your on-premises Active Directory domain services. If your on-premises Active Directory domain services gets compromised and the adversary gets access to a on-premises user, which is directory synced to Azure and holds the Global Administrator permission, it is now very easy for the adversary to pivot into the Azure environment and continue with their escapades.
This picture shows the configuration of a default Azure Active Directory Connect configuration:
As pictured, lots of unnecessary groups and users will be synchronized to the Azure Active Directory. This is what we don’t want.
What we like to see is organizational unit-based filtering, which enables you to scope your synchronization to specific organizational units, and by then only targeting the necessary regular user accounts, as pictured below:
I encourage you to review your current Azure Active Directory Connect synchronization implementation and configure accordingly. Make sure you only include specific organizational nits with regular users in your filtering options.
In short, keep your environments separated at the highest level.
Section 2
Ok – you have now selected specific organizational units and chosen to directory sync them to your Azure Active Directory. After a while you realise the potential to deploy compute and resources in Azure, and by then you start lifting and shifting your on-premises solutions to the cloud.
Fast forward and you have implemented several compute and resource solutions in Azure. You are now deploying Web Applications, Storage Accounts and Virtual Machines. You need someone in your organization to operate this new environment, and therefor you assign IT members to specific tasks. You also allow your developers to start building cool stuff in Azure.
I’d like to showcase a scenario taken from a real engagement, where a developer on the surface didn’t have any direct administrative privileges in Azure nor on the on-premises corporate network, suddenly had the privileges to execute code on Azure Virtual Machines, which could potentially happen to be sensitive servers and even Domain Controllers.
Scenario
Meet John. John is an aspiring developer and likes to build stuff. John is just a regular Domain User in his corporate on-premises Active Directory domain services. But John is currently cooking on a new line of business application which needs to utilize the Microsoft identity platform and thus provide authentication and authorization services for the application and the corporate users, which allows them to use their corporate credentials against John’s new line of business application. The IT department creates a single tenant Azure App Registration in their tenant, which then automatically creates an Enterprise Application which will then act as a Service Principal in their tenant, giving the application an identity to access resources via Role-based access control (RBAC). This application will facilitate a trust relationship between Johns own line of business application and the Microsoft identity platform. Since John is responsible for the application and its lifecycle, he gets granted “Owner” of the newly created Azure App Registration and Enterprise Application, giving him the ability to manage it.
At some point John figures that his own line of business application needs to access and modify resources inside several Resource Groups. It could be SQL databases, Storage Blobs, or even secrets from Key Vaults. Luckily, John has been provided with the Service Principal (Enterprise Application) associated with the App Registration. So, the IT department grants the Service Principal associated with the App Registration the privilege “Contributor” on the Subscription, which contains the different Resource Groups and their correspondingly resources. This enables the Service Principal to access and modify applicable resources as needed. Great, John is happy.
But since the Azure Resource Manager hierarchy structure has been built flat, another Resource Group inside the same Subscription contains several other resources, which includes the likes of Virtual Machines and other sensitive workloads. Now the kicker here is that Role-base Access Control (RBAC) permissions can be specified on a scope at four levels:
· Management Groups
· Subscriptions
· Resource Group
· Resource
Here’s a picture visualizing the above
In this specific scenario the permission was set on the Subscription level, which effectively grants the Service Principal inherited Contributor permissions on all applicable Resource Groups and Resources inside that Subscription.
Contributor is a Role-base Access Control (RBAC) permission which grants the permission bearer the ability to read and modify all applicable resources in the scope, and when there’s a Virtual Machine involved, you now have the ability to execute code in SYSTEM context on that Virtual Machine.
Summarizing the above:
IT Department creates an Azure App Registration and Enterprise Application for John
John gets assigned Owner on the Azure App Registration and Enterprise Application
The Service Principal assigned to the Azure App Registration gets assigned Contributor on the Subscription level, to access and modify resources as needed
The same Subscription happens to include another Resource Group wich contains multiple Virtual Machines
This effectively gives John the ability to execute code on the Virtual Machines through the likes of authenticating as the Service Principal
If an adversary compromises John, let’s say through a phishing email, and by then gain code execution on Johns workstation in the on-premises Active Directory, the actor will then start to enumerate the environment and discover that John is directory synced into an Azure Active Directory tenant. Since Johns synchronized identity “owns” an Azure App Registration and its Service Principal, the actor has an attack path from the on-premises Active Directory into the cloud and the Virtual Machines, with indirect permissions through the Service Principal. Let’s see that in action.
Walkthrough
John is compromised and the adversary was able to phish John’s password. He/she proceeds to access the company’s Azure portal with John’s credentials. The adversary was astonished that he/she didn’t get prompted for multi-factor authentication, but chuckles and proceeds to enumerate the tenant.
Please enable Azure MFA on all your user accounts. Read my previous blogpost for an easy implementation: Two Azure Active Directory essential security initiatives to protect your identities and tenant — Improsec | improving security
It doesn’t require much effort to discover that John is owner of an App Registration:
This is interesting, and it is now time to discover if the Service Principal is configured with any interesting permissions in the tenant. The adversary takes notes of the Application (client) ID:
Since there’s no easy way to determine what RBAC permissions the Service Principal has been granted from the Azure Portal GUI, he/she connects to the tenant with PowerShell providing John’s credentials, and gather the needed information:
And just like that, he/she now knows that the Service Principal has the permission “Contributor” on the Subscription Id “81dfdac6-97bc-449d-8276-54767df0ee1c”, which makes the Service Principal Contributor on all Resource Groups and Resources inside that Subscription.
It’s now time to authenticate as the Service Principal to abuse the permissions. To do so, the adversary needs the Application ID, a secret, or a certificate which effectively acts as credentials for the Service Principal. Since John is owner of the application, it is straightforward to add a new secret:
The adversary is now ready to authenticate as the Service Principal:
The adversary now starts enumerating the Subscription as the Service Principal, and with a single command a Virtual Machine inside the Subscription shows up:
Consequently, the adversary can now execute code on the Virtual Machine:
The following picture also shows the succeeded PowerShell run command and the Service Principal initiating it:
Worst case scenario, this Virtual Machine could have been a Domain Controller, effectively giving the adversary the ability to dump the Active Directory database ntds.dit and extract the hashes offline on their own machine.
Summarizing the above:
1. John gets compromised and the adversary obtains Johns credentials
2. The adversary enumerates the Azure tenant and discover that John is an owner of an Azure App Registration and its Service Principal (Enterprise Application)
3. The adversary discovers that the Service Principal is configured with the RBAC permission “Contributor” on a Subscription level
4. The adversary creates a new secret for the Azure App Registration, and authenticate as the Service Principal towards the Azure tenant
5. The adversary enumerates the Subscription and discover a Virtual Machine
6. The adversary can now execute code in SYSTEM context on the Virtual Machine
Mitigation
Require Azure MFA on all your users
I encourage you to enforce this through Conditional Access policies and scope it to all Cloud Applications
Give your developers a separate dev account and require them to work on a separate workstation or virtual machine, designed for the purpose. This effectively separates development from production
Don’t assign any email account and prohibit the dev user from logging on to the regular workstation.
Assign the appropriate Azure Active Directory roles and Role-base Access Control (RBAC) permissions to the dev account and not the regular user account
Architect and organize your Azure Resources with Management Groups, Subscriptions and Resource Groups and make sure to utilize those containers as Security Boundaries.
Larger enterprises might need several Management Groups and Subscriptions, and smaller company’s might be fine with one Subscription and multiple Resource Groups
I can highly recommend designing your cloud infrastructure referencing Microsoft’s Cloud Adoption Framework and Enterprise-scale Landing Zones
Place your sensitive workloads in their own Subscription or Resource Groups and make sure only the necessary Security Principals (Users, Groups and Service Principals) has access to those resources
Look out for inherited permissions from parent Subscriptions and/or Management Groups
Make sure to spool all your Azure Audit Logs and Azure Activity Logs into a Log Analytics Workspace, Azure Monitor, Azure Sentinel, or a third-party SIEM
Alert on newly added credentials to an application where a credential was already present
Alert on “Run Command on Virtual Machine” initiated on your sensitive Virtual Machines
Kusto queries for both alerts can be found below
Query for newly added credentials to an application
AuditLogs
| where OperationName has_any ("Add service principal", "Certificates and secrets management") // captures "Add service principal", "Add service principal credentials", and "Update application - Certificates and secrets management" events
| where Result =~ "success"
| mv-expand target = TargetResources
| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
| extend targetDisplayName = tostring(TargetResources[0].displayName)
| extend targetId = tostring(TargetResources[0].id)
| extend targetType = tostring(TargetResources[0].type)
| extend keyEvents = TargetResources[0].modifiedProperties
| mv-expand keyEvents
| where keyEvents.displayName =~ "KeyDescription"
| extend new_value_set = parse_json(tostring(keyEvents.newValue))
| extend old_value_set = parse_json(tostring(keyEvents.oldValue))
| where old_value_set != "[]"
| extend diff = set_difference(new_value_set, old_value_set)
| where isnotempty(diff)
| parse diff with * "KeyIdentifier=" keyIdentifier: string ",KeyType=" keyType: string ",KeyUsage=" keyUsage: string ",DisplayName=" keyDisplayName: string "]" *
| where keyUsage == "Verify" or keyUsage == ""
| extend UserAgent = iff(AdditionalDetails[0].key == "User-Agent", tostring(AdditionalDetails[0].value), "")
| extend InitiatingUserOrApp = iff(isnotempty(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| extend InitiatingIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
// The below line is currently commented out but Azure Sentinel users can modify this query to show only Application or only Service Principal events in their environment
//| where targetType =~ "Application" // or targetType =~ "ServicePrincipal"
| project-away diff, new_value_set, old_value_set
| project-reorder
TimeGenerated,
OperationName,
InitiatingUserOrApp,
InitiatingIpAddress,
UserAgent,
targetDisplayName,
targetId,
targetType,
keyDisplayName,
keyType,
keyUsage,
keyIdentifier,
CorrelationId,
TenantId
| extend
timestamp = TimeGenerated,
AccountCustomEntity = InitiatingUserOrApp,
IPCustomEntity = InitiatingIpAddress
Query for run commands initiated on virtual machines
AzureActivity
| where OperationName == "Run Command on Virtual Machine"
| where ActivityStatus has_any ("Succeeded", "Accepted")
| project TimeGenerated,
Resource,
ResourceGroup,
Caller,
CallerIpAddress,
ActivityStatus
End note
I hope you found this read interesting and informative and I advise you to have a plan and design for your cloud journey into the hybrid infrastructure era before entering the skies. Microsoft has provided great documentation on all subjects which are worth reading, and I personally prefer to start with the Cloud Adoption Framework: Microsoft Cloud Adoption Framework for Azure - Cloud Adoption Framework | Microsoft Docs