move open esc close Searches the full text of every issue.

SOC Weekly Brief The week in the Microsoft security stack, distilled

← Home

Week 34 · 11 min read

August 17 – August 24, 2026

Act by

  • 16 Aug 2027 — the classic Defender for SQL APIs for Vulnerability Assessment and Advanced Threat Protection retire. Microsoft posted the retirement notice on 17 August 2026, giving twelve months of notice. If you have scripts, runbooks or automation that call those APIs to read SQL vulnerability assessment results or manage Advanced Threat Protection settings, migrate them to the supported configuration model before the date; anything left behind stops returning results rather than failing loudly. (Microsoft Defender for Cloud release notes)

What changed

Microsoft disclosed CVE-2026-69836 on 20 August, a remote code execution vulnerability in Microsoft Entra ID with a CVSS 3.1 base score of 10.0. The weakness is deserialization of untrusted data, CWE-502, and the advisory text is blunt: deserialization of untrusted data in Microsoft Entra ID allows an unauthorized attacker to execute code over a network. The vector is AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — network reachable, no privileges, no user interaction, scope changed. There is nothing to install: the service is Microsoft-hosted, Microsoft mitigated it server-side before publication, and customer action required is recorded as No, with the CVE issued for transparency. Two details are worth carrying into any conversation about this one. First, the advisory was revised on 21 August, the day after release, and the exploited status now reads No with an exploitability assessment of Exploitation Less Likely and a temporal vector of E:U/RL:O/RC:C, which is why coverage written on 20 and 21 August describes it as exploited in the wild and the advisory today does not. Second, an unauthenticated code execution issue in the identity plane is the kind of thing worth a retrospective look at sign-in and audit logs even when the fix was applied for you, because you cannot patch your way to certainty about what happened before the mitigation landed. (MSRC Security Update Guide)

That advisory arrived inside a much larger 20 August release: 36 CVEs, of which 12 are Chromium issues inherited by Edge and two are go-chi flaws in Azure Linux packages, leaving 22 Microsoft entries. Almost all of the Microsoft entries are cloud-service disclosures with customer action required set to No, and six of them carry a CVSS base score of 10.0 — the Entra ID RCE above, an Exchange Online elevation of privilege (CVE-2026-65801), two Azure Arc elevation of privilege issues (CVE-2026-69555 and CVE-2026-65816), an Azure SQL Database elevation of privilege (CVE-2026-69502) and a remote code execution flaw in Azure Managed Instance for Apache Cassandra (CVE-2026-65770). Below those sit a second Entra ID elevation of privilege at 9.9 (CVE-2026-69851), further Azure SQL Database issues at 9.9 and 9.1, a Microsoft Fabric elevation of privilege at 9.9, Azure Logic Apps at 9.6, Azure Data Factory at 9.3 and 8.6, Partner Center and Azure Stack HCI information disclosure at 8.6, Azure Virtual Machines elevation of privilege at 8.5, an Azure Data Manager for Energy RCE at 8.5, and an information disclosure in Microsoft Copilot in Azure at 7.7. None of these generate patching work, and that is exactly why they get skipped — but they are the closest thing you get to a public record of which managed service surfaces were reachable and when they were fixed, so log the batch somewhere your team can find it the next time an auditor or an incident timeline asks what Microsoft disclosed about a service you depend on. (MSRC Security Update Guide)

Three items in the same release do require action. CVE-2026-70105 is a Microsoft Word information disclosure rated 6.5, an improper input validation issue that lets an unauthenticated attacker read small portions of heap memory over a network with user interaction; it was actually fixed in the August 2026 updates and inadvertently omitted from the bulletin, so anyone who has already deployed August Office updates is covered and everyone else has a reason to check. The other two are Windows Remote Help: CVE-2026-55013, a defense spoofing issue rated 7.1, is an uncontrolled search path element (CWE-427) that lets a locally authorized attacker with low privileges spoof, and CVE-2026-55015 is a denial of service rated 5.5. Remote Help is the supported helpdesk assistance tool, which means it runs in a support context on endpoints and a search-path weakness there is a straightforward escalation hook for anyone who already has a foothold. Neither is flagged as exploited. (MSRC Security Update Guide)

Microsoft Threat Intelligence published a hunting teardown of MacSync Stealer on 18 August, and the framing is the useful part: the operators rotate domains fast enough that static blocking decays, so the guidance is built around request shapes and process chains that survive an infrastructure change rather than around a domain list. Initial access is ClickFix — the victim is talked into pasting a command into Terminal. From there the durable pivots are payload retrieval through curl against /curl/ paths, command and control over recurring URI patterns such as /dynamic?txd= and /gate?buildtxd= with consistent User-Agent strings and API-key headers, exfiltration as chunked HTTP PUT uploads using --data-binary with upload_id, chunk_index and total_chunks parameters, and AppleScript-assisted shell execution where osascript launches suspicious command sequences. Microsoft says behavioural correlation on those patterns surfaced more than 30 domains, and the post ships advanced hunting queries for each pivot. Defender for Endpoint alerts on suspicious shell command execution, obfuscation and deobfuscation activity, and suspicious AppleScript activity; antivirus coverage is Trojan:MacOS/SuspMalScript, Behavior:MacOS/SuspOsascriptExec and Behavior:MacOS/SuspInfoExfil. The mitigation worth acting on beyond detections is the macOS 26.4 and later platform control that blocks malicious pastes, which cuts the ClickFix delivery path off at the source. (Microsoft Security Blog)

Azure Firewall entered public preview for dual stack mode on 18 August, meaning it can now run with both IPv4 and IPv6 address spaces, IPv6 subnets, public IPv6 addresses, user-defined routes and network rules. IPv6-only firewalls are not supported. Read the limitations before anyone treats this as IPv6 parity, because they define what actually gets inspected: application rules and DNAT are not supported yet, and threat intelligence, IDPS, Explicit Proxy and IP Groups based scenarios are not supported at all for IPv6. In practice a dual stack firewall gives IPv6 traffic network-rule filtering and nothing else — no signature inspection, no threat intel deny, no FQDN-based egress control — while IPv4 traffic keeps every feature it had. Classic Azure Firewall and virtual hub firewalls are excluded, and the change is one-way: once a firewall is upgraded to dual stack it cannot be reverted to IPv4-only until the capability reaches general availability. If an application team asks for IPv6 on a segment you rely on for inspection, that gap is the thing to raise. (Deploy Azure Firewall in dual stack mode)

Defender for Cloud changed how vulnerability assessment CVE details are consumed in Azure Resource Graph on 21 August. CVE information is still available, but the detailed CVE properties now come from the microsoft.security/cvedetails resource type rather than being carried inline on vulnerability assessment records, a change Microsoft attributes to query performance, scalability and support for larger result sets. The practical consequence is quiet and easy to miss: existing API or Resource Graph queries that read CVE details from vulnerability assessment records keep running but might not return the expected detailed properties. If you have exposure reporting, exception tracking or a vulnerability dashboard built on Resource Graph, check what it returns now rather than waiting for someone to notice a column has gone empty. Migration guidance and sample queries are in the remediate machine vulnerabilities article. (Microsoft Defender for Cloud release notes)

KQL to try

Hunts Defender XDR telemetry for the macOS Screen Sharing attack path behind CVE-2026-65400, covering privileged Screen Sharing file transfer helpers, SSH session persistence, LaunchDaemon writes, process masquerading, Packet Filter changes and XMRig artefacts — useful for answering whether any of it was already happening before you patched. (Hunting CVE-2026-65400 Exploitation on macOS — Sergio Albea)

let Days = 30d;
let KnownSHA256 = "84006055916e267f7c2f9324f1848563e589e4526a296d4e9e9ce8e2112d357c";
union
( // 1. Initial access / execution: Root Screen Sharing activity, SSH persistence, masquerading and PF modification.
DeviceInfo| where OSPlatform has 'MacOs' | join kind=inner DeviceProcessEvents on  DeviceId
    | where Timestamp > ago(Days)
    | where
        FileName =~ "SSFileCopyReceiver" or ProcessCommandLine has "sshd-session -i -R" or ProcessCommandLine has "exec -a com.apple.airportd"
        or ProcessCommandLine has "pfctl" or ProcessCommandLine has "com.xmr.miner.plist" or ProcessCommandLine has "sysmond"
    | extend HuntingSignal = case(
        FileName =~ "SSFileCopyReceiver", "Screen Sharing file transfer", ProcessCommandLine has "sshd-session -i -R",
            "SSH remote session activity", ProcessCommandLine has "exec -a com.apple.airportd", "Process masquerading",
        ProcessCommandLine has "pfctl", "Packet Filter modification", ProcessCommandLine has "com.xmr.miner.plist",
            "LaunchDaemon persistence", ProcessCommandLine has "sysmond", "Hidden miner execution", "Suspicious process activity"
    )
    | project Timestamp,DeviceName, ActionType, HuntingSignal, FileName, FolderPath, SHA256, ProcessCommandLine, InitiatingProcessFileName
),(// 2. File / persistence activity: SSH keys, hidden files, privileged paths and LaunchDaemon persistence
DeviceInfo| where OSPlatform has 'MacOs' | join kind=inner DeviceFileEvents on DeviceId
    | where Timestamp > ago(Days)
    | where SHA256 == KnownSHA256
        or FolderPath has "/private/var/root/.config" or FolderPath has "/Library/LaunchDaemons" or FolderPath has "/private/var/root/.ssh"
    | extend HuntingSignal = case(
        SHA256 == KnownSHA256, "Known XMRig SHA256",
        FolderPath has "/private/var/root/.config", "Hidden root file activity",
        FolderPath has "/Library/LaunchDaemons", "LaunchDaemon activity",
        FolderPath has "/private/var/root/.ssh", "Root SSH activity", "Suspicious file activity"
    )
    | project Timestamp, DeviceName, ActionType, HuntingSignal, FileName, FolderPath, SHA256,
        ProcessCommandLine = InitiatingProcessCommandLine,InitiatingProcessFileName
),
(// 3. Network activity: Observed mining pool plus SSH and Screen Sharing traffic.Ports 22/5900 are context only, not malicious by themselves.
 DeviceInfo| where OSPlatform has 'MacOs' | join kind=inner DeviceNetworkEvents on DeviceId
    | where Timestamp > ago(Days)
    | where RemoteUrl =~ "auto.c3pool.org" or RemotePort in (22, 5900)
    | extend HuntingSignal = case(
        RemoteUrl =~ "auto.c3pool.org", "Observed mining pool",
        RemotePort == 5900, "Screen Sharing / VNC traffic",
        RemotePort == 22, "SSH traffic","Suspicious network activity"
    )
    | project Timestamp, DeviceName, ActionType, HuntingSignal, FileName = InitiatingProcessFileName, FolderPath = "", SHA256 = InitiatingProcessSHA256,
        ProcessCommandLine = InitiatingProcessCommandLine, InitiatingProcessFileName
)

Scores SSH port forwarding and tunnelling against a 30-day per-device baseline, weighting listener scope and persistent-shell flags, and pairs it with unauthorised sshd_config writes and GatewayPorts or PermitTunnel appearing on a command line. (Suspicious SSH Tunneling and Config Exposure — Benjamin Zulliger)

let Lookback    = 7d;
let BaselineWin = 30d;
// Set to true to filter out noisy local development tunnels on loopback and focus on higher-risk activity
let HighFidelityOnly = true;
let FwdRegex = @"(^|\s)-[LDR] ?(\d{1,5}(:|\s|$)|\*:|0\.0\.0\.0:|localhost:|\[)";
let SshTunnelEvents = (StartTime:datetime, EndTime:datetime) {
    DeviceProcessEvents
    | where Timestamp between (StartTime .. EndTime)
    // Ignore sshd here, as -D and -R are standard daemon flags there
    | where FileName in~ ("ssh", "ssh.exe", "plink.exe", "putty.exe")
    | where ProcessCommandLine matches regex FwdRegex
    | extend TunnelFlag   = extract(@"(^|\s)-([LDR]) ?(\d|\*:|0\.0\.0\.0:|localhost:|\[)", 2, ProcessCommandLine)
    | extend ForwardSpecs = extract_all(@"(?:^|\s)-[LDR] ?(\S+)", ProcessCommandLine)
    | extend AllSpecs     = strcat_array(ForwardSpecs, " ")
    | extend SshTarget    = extract(@"\s([\w\.\-]+@[\w\.\-]+)", 1, ProcessCommandLine)
    | extend CmdPattern   = replace_regex(ProcessCommandLine, @"\b\d{4,5}\b", "<port>")
};
let TunnelBaseline =
    SshTunnelEvents(ago(BaselineWin + Lookback), ago(Lookback))
    | distinct DeviceName, AccountName, CmdPattern;
let TunnelFindings =
    SshTunnelEvents(ago(Lookback), now())
    | extend TunnelType = case(
        TunnelFlag == "L", "Local Port Forwarding (-L)",
        TunnelFlag == "D", "Dynamic Port Forwarding / SOCKS (-D)",
        TunnelFlag == "R", "Remote Port Forwarding (-R)",
        "SSH Tunneling / Other")
    // Explicit binding requires all four fields (bind:port:host:hostport)
    | extend ListenerScope = case(
        AllSpecs matches regex @"(^|\s)(\*|0\.0\.0\.0|::):",           "Bind to all interfaces",
        AllSpecs matches regex @"[\w\.\-]+:\d{1,5}:[\w\.\-]+:\d{1,5}", "Bind to explicit address",
        "Loopback (Default)")
    | extend PersistentTunnel = ProcessCommandLine matches regex @"(^|\s)-(fN|Nf|N|f)(\s|$)"
    // Regular local dev tunnels on loopback are excluded in High-Fidelity mode
    | where HighFidelityOnly == false
        or ListenerScope != "Loopback (Default)"
        or TunnelType startswith "Remote"
    | summarize
        EventCount     = count(),
        FirstSeen      = min(Timestamp),
        LastSeen       = max(Timestamp),
        SampleCmd      = any(ProcessCommandLine),
        Parents        = make_set(InitiatingProcessFileName, 8),
        SshTargets     = make_set_if(SshTarget, isnotempty(SshTarget), 8),
        ForwardSpecSet = make_set(AllSpecs, 8),
        ReportId       = any(ReportId),
        DeviceId       = any(DeviceId)
        by DeviceName, AccountName, InitiatingProcessAccountName, FileName,
           TunnelType, ListenerScope, PersistentTunnel, CmdPattern
    | join kind=leftanti TunnelBaseline on DeviceName, AccountName, CmdPattern
    | extend RiskScore =
          iff(ListenerScope == "Bind to all interfaces",   40, 0)
        + iff(ListenerScope == "Bind to explicit address", 20, 0)
        + iff(TunnelType startswith "Remote",                30, 0)
        + iff(PersistentTunnel,                               15, 0)
        + iff(TunnelType startswith "Dynamic",               10, 0)
    | project
        Category  = "SSH Port Forwarding",
        RiskScore,
        Signal    = TunnelType,
        Evidence  = strcat(TunnelType, " | ", ListenerScope,
                        iff(PersistentTunnel, " | persistent tunnel without shell (-N/-f)", "")),
        DeviceName,
        Account   = coalesce(AccountName, InitiatingProcessAccountName),
        EventCount,
        FirstSeen,
        LastSeen,
        CLI       = SampleCmd,
        SampleCmd,
        Details   = strcat("Specs: ", strcat_array(ForwardSpecSet, ", "),
                        iff(array_length(SshTargets) > 0, strcat(" | Targets: ", strcat_array(SshTargets, ", ")), ""),
                        " | Parent: ", strcat_array(Parents, ", ")),
        ReportId,
        DeviceId;
let ConfigWrites =
    DeviceFileEvents
    | where Timestamp > ago(Lookback)
    | where FileName =~ "sshd_config" or FolderPath has "/etc/ssh/sshd_config.d"
    | where ActionType in ("FileCreated", "FileModified", "FileRenamed")
    | project Timestamp, DeviceName, Account = InitiatingProcessAccountName,
              Cmd = InitiatingProcessCommandLine, Path = FolderPath,
              Signal = "sshd_config modified", Score = 35, ReportId, DeviceId;
let GatewayPortsCmd =
    DeviceProcessEvents
    | where Timestamp > ago(Lookback)
    | where ProcessCommandLine has_cs "GatewayPorts" or ProcessCommandLine has_cs "PermitTunnel"
    | where ProcessCommandLine !contains "GatewayPorts=no" and ProcessCommandLine !contains "GatewayPorts no"
    | project Timestamp, DeviceName, Account = InitiatingProcessAccountName,
              Cmd = ProcessCommandLine, Path = FolderPath,
              Signal = "GatewayPorts/PermitTunnel in command line", Score = 45, ReportId, DeviceId;
let ConfigFindings =
    union ConfigWrites, GatewayPortsCmd
    // Adjust admin and deployment accounts based on your environment
    | where Account !in~ ("puppet", "ansible", "salt")
        and Cmd !has "cloud-init" and Cmd !has "unattended-upgrade"
    | summarize
        EventCount = count(),
        FirstSeen  = min(Timestamp),
        LastSeen   = max(Timestamp),
        SampleCmd  = any(Cmd),
        Paths      = make_set(Path, 4),
        RiskScore  = max(Score),
        ReportId   = any(ReportId),
        DeviceId   = any(DeviceId)
        by DeviceName, Account, Signal
    | project
        Category  = "sshd_config Exposure",
        RiskScore,
        Signal,
        Evidence  = strcat(Signal, " by ", Account),
        DeviceName,
        Account,
        EventCount,
        FirstSeen,
        LastSeen,
        CLI       = SampleCmd,
        SampleCmd,
        Details   = strcat("Paths: ", strcat_array(Paths, ", ")),
        ReportId,
        DeviceId;
union TunnelFindings, ConfigFindings
| extend Severity = case(RiskScore >= 40, "High", RiskScore >= 20, "Medium", "Low")
| project-reorder Severity, RiskScore, Category, Evidence, DeviceName, Account,
              EventCount, FirstSeen, LastSeen, CLI, SampleCmd, Details
| sort by RiskScore desc, LastSeen desc