In this section

When Attacks Cross Boundaries

5-6 hours · Module 6

What you already know

You can triage cloud, endpoint, Linux, and network alerts each on their own terms. Real intrusions don't respect those boundaries: they start in one environment and pivot into the next, and the most dangerous triage failure is not misclassifying an alert but failing to see it's one move in a larger chain.

Scenario

A cloud sign-in alert for j.morrison looks like a contained identity event, and on its own it classifies as a single risky login. But the same hour, his workstation beacons out and a Linux server he can reach over SSH logs a new root session. Treated separately, three medium alerts. Recognized as a pivot chain, one active intrusion crossing three environments.

The most dangerous triage failure is not misclassifying a single alert, it is failing to recognize that an alert in one environment is connected to activity in another. At Northgate Engineering, the SOC analyst who triaged the AiTM sign-in alert in Entra ID correctly classified it as a true positive. But they closed the triage after revoking the cloud session, missing the fact that the attacker had already replayed the stolen token to authenticate to a Windows endpoint via Remote Desktop, and from that endpoint, had SSH'd into the RHEL production database server. The cloud triage was correct. The overall triage was catastrophically incomplete. The attack had three components across three environments, and the responder only triaged one.

CROSS-ENVIRONMENT ATTACK PIVOT PATTERNS CLOUD → ENDPOINT AiTM token → RDP/VPN → endpoint code exec Pivot indicator: same IP in SigninLogs + DeviceLogonEvents CHAIN-HARVEST pattern ENDPOINT → CLOUD AD compromise → AAD Connect → Entra takeover Pivot indicator: DCSync + sync account in AuditLogs CHAIN-PRIVILEGE pattern ON-PREM → LINUX Windows lateral → SSH to RHEL/Ubuntu Pivot indicator: Windows IP in auth.log accepted SSH CHAIN-HARVEST extended CLOUD → LINUX (DIRECT) Azure VM compromise via cloud creds → SSH to on-prem Pivot indicator: Azure activity logs + auth.log from Azure IP CHAIN-DRIFT pattern LINUX → CLOUD Compromised Linux VM → stolen service principal → Entra Pivot indicator: ServicePrincipalSignInLogs from Linux IP CHAIN-FACTORY pattern

Figure TR6.1. The five cross-environment pivot patterns. Each attack begins in one environment and uses compromised credentials, tokens, or network access to move into another. The pivot indicator: the log field or alert pattern that reveals the boundary crossing, is the triage responder's primary detection mechanism.

Why attacks cross boundaries

Attackers do not think in terms of "cloud environment" or "Windows environment" or "Linux environment." They think in terms of objectives: steal data, deploy ransomware, maintain persistent access. The environment boundary is a defender's abstraction. The attacker crosses it whenever the path to their objective requires it.

At Northgate Engineering, the six canonical attack chains demonstrate this reality. CHAIN-HARVEST begins with AiTM phishing (cloud), moves to endpoint compromise via token replay (Windows), and pivots to the RHEL production database (Linux) because the financial data the attacker wants lives on that database server. The attacker crossed two environment boundaries not because they wanted to compromise Windows and Linux, but because the path from stolen credential to target data went through those environments.

The triage responder who understands this pattern asks a different question than the responder who does not. The single-environment responder asks: "Is this cloud alert a true positive?" The cross-environment responder asks: "If this cloud alert is a true positive, what would the attacker do NEXT, and is there evidence of that in the other environments?"

That second question is the foundation of this entire module.

Pivot pattern 1: Cloud-to-endpoint

This is the most common boundary crossing in M365 environments and the pattern that CHAIN-HARVEST follows at NE.

How the attack works: The attacker compromises a cloud identity, typically through AiTM phishing, credential stuffing, or MFA fatigue. They now have a valid session token for that user. The cloud identity has access to services that connect to endpoints: Remote Desktop Gateway, VPN (GlobalProtect, Azure VPN), Windows 365 Cloud PC, Azure Virtual Desktop, or even Intune-enrolled device management. The attacker uses the compromised cloud session to authenticate to one of these endpoint access services, gaining code execution on a Windows machine inside the corporate network.

The pivot indicators in log data:

The cloud side shows the initial compromise. You already know how to find this from TR2, anomalous sign-in in SigninLogs, unfamiliar IP, unusual device fingerprint, risk signals from Entra ID Protection.

The pivot shows in two places simultaneously:

First, in SigninLogs, the compromised user authenticates to a resource that provides endpoint access. The AppDisplayName field shifts from "Microsoft Office" or "Outlook" (typical cloud-only activity) to "Microsoft Remote Desktop" or "Palo Alto Networks GlobalProtect" or "Windows Virtual Desktop." This shift is the pivot signal. The attacker is moving from cloud data access to network/endpoint access.

// PIVOT DETECTION: Cloud identity accessing endpoint services
let targetUser = "j.morrison@northgateeng.com";
let triageWindow = 48h;
let endpointApps = dynamic([
    "Microsoft Remote Desktop",
    "Remote Desktop Client",
    "Windows Virtual Desktop",
    "Azure Virtual Desktop",
    "Palo Alto Networks GlobalProtect",
    "Cisco AnyConnect",
    "Windows 365"
]);
SigninLogs
| where TimeGenerated > ago(triageWindow)
| where UserPrincipalName =~ targetUser
| where ResultType == "0"
| where AppDisplayName has_any (endpointApps)
| project
    TimeGenerated,
    IPAddress,
    AppDisplayName,
    ResourceDisplayName,
    DeviceDetail_displayName = tostring(DeviceDetail.displayName),
    ConditionalAccessStatus,
    RiskLevelDuringSignIn,
    AuthenticationRequirement
| sort by TimeGenerated desc

Line-by-line annotation:

let endpointApps = dynamic([...]): the list of applications that provide endpoint access. This list is environment-specific. At NE, GlobalProtect is the VPN. Your organization may use Cisco AnyConnect, Fortinet FortiClient, or Zscaler Private Access. Add your VPN and remote access applications to this list before saving the query.

where AppDisplayName has_any (endpointApps), filters to only authentication events targeting endpoint access services. If this query returns results for a user whose cloud identity was just compromised, the attacker is attempting or has completed the pivot from cloud to endpoint.

AuthenticationRequirement, shows whether the endpoint access service required additional MFA. If the attacker authenticated using a stolen primary refresh token (PRT), they may bypass MFA for the endpoint service because the PRT satisfies the MFA claim. This is why PRT theft is more dangerous than password theft, it enables cross-service authentication without repeated MFA challenges.

Second, on the Windows endpoint, the arrival shows in DeviceLogonEvents (Defender for Endpoint) or Security event 4624 (Windows Event Log). The logon type and source IP connect the endpoint event to the cloud pivot:

// ENDPOINT ARRIVAL: Logon events from suspected attacker IP
let suspectIPs = dynamic(["185.220.101.42", "192.0.2.50"]);
let triageWindow = 48h;
DeviceLogonEvents
| where Timestamp > ago(triageWindow)
| where RemoteIP has_any (suspectIPs)
| project
    Timestamp,
    DeviceName,
    AccountName,
    AccountDomain,
    RemoteIP,
    LogonType,
    Protocol,
    ActionType
| sort by Timestamp desc

where RemoteIP has_any (suspectIPs): the IP addresses from the anomalous cloud sign-in. If the same IP that performed the AiTM attack also appears in endpoint logon events, the cloud-to-endpoint pivot is confirmed. This is the single strongest cross-environment correlation: same IP, same timeframe, different log source.

LogonType — "RemoteInteractive" indicates RDP. "Network" indicates SMB or other network logon. The logon type tells you HOW the attacker arrived on the endpoint.

The triage decision at NE: When the SOC analyst checked j.morrison's SigninLogs during the CHAIN-HARVEST triage, Query 1 (from TR2) showed the anomalous authentication from 185.220.101.42. Had the analyst then queried DeviceLogonEvents for the same IP, they would have found a RemoteInteractive logon to DESKTOP-NGE042 at 08:17: three minutes after the cloud authentication. The cloud-to-endpoint pivot was sitting in the data. The triage missed it because the analyst did not look beyond the cloud environment.

Pivot pattern 2: Endpoint-to-cloud

This is the CHAIN-PRIVILEGE pattern and it is the most damaging boundary crossing at NE because it converts an on-premises Active Directory compromise into a cloud identity platform takeover.

How the attack works: The attacker compromises Active Directory, through Kerberoasting, AS-REP roasting, password spraying against LDAP, or exploiting a vulnerable domain controller. They escalate to Domain Admin. With Domain Admin, they perform DCSync to extract all password hashes, including the hash for the Azure AD Connect sync account (MSOL_*). They use the sync account credentials to authenticate to Entra ID through the Azure AD Connect synchronization service, gaining the ability to modify cloud identities, reset passwords, and assign roles.

The pivot indicators in log data:

The on-premises side shows AD compromise indicators that you covered in TR3. Defender for Identity alerts for suspected DCSync, Kerberoasting, or golden ticket. The critical alert is the DCSync detection: if an attacker has performed DCSync, they have EVERY password hash in the domain, including the Azure AD Connect service account.

The pivot from on-prem to cloud shows in AuditLogs when the sync account performs unusual operations:

// PIVOT DETECTION: Azure AD Connect sync account performing unusual operations
let syncAccounts = dynamic(["Sync_AADC01_", "MSOL_"]);
let triageWindow = 7d;
AuditLogs
| where TimeGenerated > ago(triageWindow)
| where InitiatedBy has_any (syncAccounts)
| where OperationName !in (
    "Update user",
    "Update group",
    "Add member to group",
    "Remove member from group"
)
| project
    TimeGenerated,
    OperationName,
    Category,
    Result,
    InitiatedBy_user = tostring(InitiatedBy.user.userPrincipalName),
    InitiatedBy_app = tostring(InitiatedBy.app.displayName),
    TargetResources
| sort by TimeGenerated desc

Line-by-line annotation:

let syncAccounts = dynamic(["Sync_AADC01_", "MSOL_"]). Azure AD Connect uses service accounts with predictable naming patterns. The Sync_ prefix followed by the server hostname is the standard format. The legacy MSOL_ prefix appears in older deployments. At NE, the sync server is AADC01, so the account is Sync_AADC01_@northgateeng.onmicrosoft.com.

where OperationName !in (...): the exclusion list filters out normal synchronization operations. Azure AD Connect legitimately updates users and groups during every sync cycle (every 30 minutes by default). The suspicious operations are everything ELSE: password resets, role assignments, application registrations, conditional access modifications. If the sync account is performing these operations, either the sync configuration has been modified (rare, legitimate) or the sync account credentials have been compromised (CHAIN-PRIVILEGE).

The triage decision: When Defender for Identity fires a suspected DCSync alert, the first cross-environment triage action is to query AuditLogs for the Azure AD Connect sync account. If the sync account has performed operations outside its normal synchronization scope within the window following the DCSync, the endpoint-to-cloud pivot is confirmed. Containment must address BOTH environments: disable the compromised AD accounts AND rotate the Azure AD Connect sync account credentials AND apply a conditional access policy blocking the sync account from interactive sign-in.

Pivot pattern 3: On-prem Windows to Linux

This is the final leg of CHAIN-HARVEST at NE and the pattern that completes the three-environment attack chain.

How the attack works: The attacker has code execution on a Windows endpoint or server (from pivot pattern 1 or from direct compromise). They discover SSH keys stored on the Windows filesystem, in the user's .ssh directory, in PuTTY's registry entries, in KeePass databases, or in configuration management tool caches. Alternatively, they use compromised AD credentials that also work on Linux systems where SSSD or Winbind maps AD users to Linux accounts. They SSH from the Windows machine to a Linux server.

The pivot indicators in log data:

On the Windows side, the indicator is outbound SSH activity from a machine that does not normally initiate SSH connections:

// PIVOT DETECTION: Windows endpoint initiating SSH to Linux servers
let linuxServers = dynamic(["10.20.30.10", "10.20.30.11", "10.20.30.20"]);
let triageWindow = 48h;
DeviceNetworkEvents
| where Timestamp > ago(triageWindow)
| where RemotePort == 22
| where RemoteIP has_any (linuxServers)
| project
    Timestamp,
    DeviceName,
    InitiatingProcessAccountName,
    InitiatingProcessFileName,
    InitiatingProcessCommandLine,
    RemoteIP,
    RemotePort,
    ActionType
| sort by Timestamp desc

InitiatingProcessFileName, reveals what tool the attacker used. ssh.exe (built-in OpenSSH client), putty.exe, plink.exe (PuTTY command-line), or python.exe / powershell.exe (script-based SSH via Paramiko or PowerShell SSH module). The tool choice tells you about attacker sophistication: built-in ssh.exe is stealthier than downloading PuTTY.

On the Linux side, the corresponding indicator is the SSH acceptance in auth.log:

# PIVOT DETECTION: SSH logins from Windows endpoint IPs
# Run on the suspected Linux target
grep "Accepted" /var/log/auth.log | grep -E "10\.20\.1\.(50|51|52)" | tail -20

The correlation: if the Windows DeviceNetworkEvents show SSH connections to a Linux server IP, AND that Linux server's auth.log shows accepted SSH from the Windows endpoint's IP at the same timestamp, the pivot is confirmed.

At NE during CHAIN-HARVEST: The attacker compromised j.morrison's endpoint (DESKTOP-NGE042) via the AiTM token replay. j.morrison is a senior IT administrator with SSH access to the RHEL production database servers. The attacker found j.morrison's SSH private key at C:\Users\j.morrison\.ssh\id_rsa (no passphrase: a finding that went into the post-incident hardening recommendations). From DESKTOP-NGE042, the attacker SSH'd to RHEL-DB01 (10.20.30.10) at 08:23, six minutes after landing on the Windows endpoint. DeviceNetworkEvents showed ssh.exe connecting to 10.20.30.10:22. RHEL-DB01's auth.log showed Accepted publickey for j.morrison from 10.20.1.50 at the same timestamp.

Pivot pattern 4: Cloud-to-Linux (direct)

This is the CHAIN-DRIFT pattern. It bypasses Windows entirely by using Azure IaaS or cloud-hosted Linux VMs.

How the attack works: The attacker compromises a cloud identity that has Azure role assignments. Contributor or Owner on a resource group containing Linux VMs. Instead of pivoting through the corporate VPN to reach on-prem endpoints, the attacker authenticates to the Azure portal or Azure CLI and either: (1) uses the "Run Command" extension to execute arbitrary commands on an Azure VM, (2) resets the SSH password or key on the VM through the Azure portal, or (3) accesses a Bastion-connected VM directly through the browser-based SSH console.

The pivot indicators in log data:

The cloud side shows the initial compromise in SigninLogs (identical to CHAIN-HARVEST). The pivot indicator appears in AzureActivity rather than DeviceLogonEvents:

// PIVOT DETECTION: Cloud identity executing commands on Azure VMs
let targetUser = "j.morrison@northgateeng.com";
let triageWindow = 48h;
AzureActivity
| where TimeGenerated > ago(triageWindow)
| where Caller =~ targetUser
| where OperationNameValue has_any (
    "Microsoft.Compute/virtualMachines/runCommand",
    "Microsoft.Compute/virtualMachines/extensions",
    "Microsoft.Network/bastionHosts/createShareableLinks",
    "Microsoft.Compute/virtualMachines/resetPassword"
)
| project
    TimeGenerated,
    Caller,
    OperationNameValue,
    ResourceGroup,
    Resource = _ResourceId,
    ActivityStatusValue,
    CallerIpAddress
| sort by TimeGenerated desc

Line-by-line annotation:

OperationNameValue has_any (...), these are the Azure management operations that provide Linux access. "runCommand" executes scripts directly on the VM through the Azure fabric: no SSH required. "extensions" may indicate the installation of a custom script extension for persistence. "resetPassword" allows the attacker to set a new SSH password for any user on the VM. "bastionHosts/createShareableLinks" creates a shareable SSH link that provides authenticated access to the VM through the browser without any VPN or network path.

CallerIpAddress: the IP from which the Azure management operation was initiated. If this matches the attacker's IP from the cloud sign-in, the pivot is confirmed: the same attacker who compromised the cloud identity is now accessing the Linux VM.

The corresponding Linux-side evidence depends on the pivot method. If "runCommand" was used, the command execution appears in the VM's /var/log/waagent.log (the Azure Linux Agent log) and in /var/log/syslog or journalctl under the run-command handler. If SSH password was reset, auth.log shows a login with the new password. If Bastion was used, the SSH connection appears in auth.log from the Bastion subnet IP.

At NE, CHAIN-DRIFT used this pattern: The attacker compromised an Azure Contributor account and used the Run Command extension to execute a reverse shell on an Ubuntu web server (WEB-UBU-01) hosting the NE customer portal. The Azure management operation appeared in AzureActivity at 10:42. The reverse shell connected outbound to the attacker's infrastructure at 10:42:30. No VPN, no endpoint, no SSH key: the attacker went directly from cloud identity to Linux command execution through the Azure management plane.

Pivot pattern 5: Linux-to-cloud

This is the CHAIN-FACTORY pattern. A compromised Linux server contains credentials or tokens that provide access to cloud services.

How the attack works: The attacker compromises a Linux server, through an application vulnerability, SSH brute force, or container escape. On the compromised server, they find cloud credentials: (1) Azure managed identity tokens (available from the instance metadata service at 169.254.169.254), (2) service principal certificates or secrets stored in application configuration files, (3) Azure CLI cached tokens in ~/.azure/, or (4) environment variables containing cloud API keys or connection strings.

The pivot indicators in log data:

The Linux side shows the initial compromise in auth.log or application logs. The pivot indicator appears in AADServicePrincipalSignInLogs or AzureActivity when a service principal or managed identity performs unusual operations:

// PIVOT DETECTION: Service principal activity from Linux server IPs
let linuxServerIPs = dynamic(["10.20.30.10", "10.20.30.11", "10.20.30.20", "10.20.30.21"]);
let triageWindow = 48h;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(triageWindow)
| where IPAddress has_any (linuxServerIPs)
| project
    TimeGenerated,
    ServicePrincipalName,
    AppId,
    IPAddress,
    ResourceDisplayName,
    ResultType,
    ResultDescription
| sort by TimeGenerated desc

where IPAddress has_any (linuxServerIPs), service principal authentications should originate from known application infrastructure. If a service principal that normally authenticates from an Azure data center IP suddenly authenticates from an on-premises Linux server IP, the attacker is using the service principal credential from the compromised server. Conversely, if the service principal normally authenticates from the Linux server (legitimate application behavior), look for unusual ResourceDisplayName values: the attacker may be using the credential to access resources beyond the application's normal scope.

At NE, CHAIN-FACTORY exploited this pattern: The RHEL manufacturing execution system (MES-RHEL-01) ran a Python application that used an Azure service principal to write production telemetry to an Azure Storage Account. The service principal's client secret was stored in /opt/mes/config.yaml in plaintext. The attacker, who gained access to MES-RHEL-01 through a deserialization vulnerability in the MES web interface, extracted the client secret and used it to authenticate to Entra ID from a different IP. AADServicePrincipalSignInLogs showed the service principal authenticating from 203.0.113.99 (the attacker's infrastructure) rather than MES-RHEL-01's IP: a definitive pivot indicator.

The triage implication: When triaging a Linux server compromise, always check for cloud credentials stored on the server. Query AADServicePrincipalSignInLogs for the server's IP address to see if any service principal is authenticating from it. Then check if that same service principal is authenticating from unexpected IPs, which indicates the credential was exfiltrated and is being used elsewhere.

Compliance myth: "Our cloud and on-prem environments are separate security domains, so a cloud compromise cannot affect on-premises systems." Operational reality: Hybrid identity environments create direct bridges between cloud and on-prem. Azure AD Connect synchronizes credentials bidirectionally. Conditional access policies that permit VPN access from cloud-authenticated sessions create network bridges. SSH keys stored on domain-joined Windows endpoints create credential bridges to Linux. The "separate security domains" concept is an architectural fiction in any hybrid environment. Your triage methodology must reflect the reality that an attacker in any one environment is potentially in all of them.

Anti-Pattern

Triaging each alert only in its home environment

An alert scoped to its own environment looks survivable; the same alert recognized as a pivot is an active breach. Closing the cloud sign-in, the endpoint beacon, and the Linux session as three separate mediums misses the chain connecting them. Before you classify any cross-environment-capable alert, check whether the same entity appears in the next environment along the pivot path.

The pivot recognition framework

When triaging any alert in any environment, ask three questions:

Question 1: Does the compromised entity have access to another environment? A cloud user with VPN access can reach endpoints. A Windows admin with SSH keys can reach Linux. An AD service account with Azure AD Connect can reach Entra ID. If the compromised entity has cross-environment access, the pivot is possible.

Question 2: Is there log evidence of cross-environment activity during the triage window? Check the corresponding log source. Cloud alert → check DeviceLogonEvents for the suspicious IP. Windows alert → check SigninLogs for the compromised account and auth.log for SSH from the endpoint IP. Linux alert → check DeviceNetworkEvents for SSH to the Linux IP and SigninLogs for the account.

Question 3: Does the timeline support a causal connection? The cloud compromise at 08:14, the endpoint logon at 08:17, the SSH to Linux at 08:23: the timestamps show a clear progression. If the events are hours or days apart, the connection is weaker but should still be investigated. If the events are within minutes, the connection is strong.

⚖ Decision Point

Decision point. Single-environment triage vs cross-environment investigation: If all three pivot questions return negative: the compromised entity has no cross-environment access, there is no log evidence of boundary crossing, and the timeline shows activity confined to one environment, your triage can remain within the original environment. Close the triage with the single-environment assessment from TR2, TR3, TR4, or TR5. If ANY of the three questions returns positive, especially Question 2 (log evidence of cross-environment activity), your triage MUST expand to cover the additional environment. Do not close a triage that has cross-environment indicators without triaging all affected environments. This is the most common triage failure in hybrid environments.

Troubleshooting cross-environment pivot detection

Problem: DeviceLogonEvents does not show logons from the suspicious IP. Check whether the endpoint is onboarded to Defender for Endpoint. Unmanaged devices do not report DeviceLogonEvents. Fall back to Windows Security event 4624 in Sentinel (SecurityEvent table) or check the local Windows Event Log via live response.

Problem: auth.log on the Linux server is empty or missing entries. Check the syslog configuration. Some RHEL servers send auth logs to a central syslog server rather than writing locally. Check /etc/rsyslog.conf for forwarding rules. If logs are forwarded to Sentinel via the Azure Monitor Agent, query the Syslog table in Sentinel instead of checking the local file.

Problem: The suspicious IP appears in both cloud and endpoint logs but the timestamps are hours apart. This may indicate two separate attacks using the same infrastructure (common with botnets and VPN providers) rather than a single cross-environment pivot. Check whether the authentication methods differ between the cloud and endpoint sessions. If the cloud session used a stolen token and the endpoint session used password authentication, these may be unrelated events that share an IP. Correlate on additional fields, user account, device fingerprint, and session ID, before confirming the pivot.

Try it: Take the three KQL queries from this subsection and adapt them to your environment. Replace the NE-specific values: the target user, the endpoint access applications list, the sync account names, and the Linux server IPs, with your organization's values. Save them as a "Cross-Environment Pivot Detection" query pack in your Sentinel workspace. The next time a cloud identity alert fires, run these queries as the SECOND step after your single-environment triage (TR2 queries first, then these). Document what you find, does your organization have the same cross-environment visibility that NE has, or are there gaps?