[{"content":" TL;DR\nA client\u0026rsquo;s website landed on a reputation blocklist because its hosting provider\u0026rsquo;s whole netblock was listed. The link was in every email signature, so Microsoft started treating the client\u0026rsquo;s own mail as phishing: zero-hour auto purge (ZAP) pulled already-delivered messages out of mailboxes, and new mail went straight to quarantine. We confirmed it was ZAP (not AIR or an admin action) with Advanced Hunting, worked out how long we had before quarantine deleted everything, and released about 1,600 messages with a piloted, chunked PowerShell run. The release only holds once the URL itself is allowed and the reputation problem is fixed outside Microsoft 365.\nThe situation The first report was the kind that gets an incident opened fast: mail is disappearing. Users had messages they had read that morning, and now those messages were gone from the Inbox. At the same time, customers on Microsoft 365 said our client\u0026rsquo;s mail was landing in their quarantine, not their inbox.\nThe common thread was the client\u0026rsquo;s own website address. It was in every email signature, and on most of their outbound templates. The site sat on shared hosting, and the provider\u0026rsquo;s entire netblock had been listed. Once the domain\u0026rsquo;s reputation flipped, every message carrying the link looked like phishing to Microsoft\u0026rsquo;s URL detonation and reputation checks.\nThat produced two populations of affected mail:\nMail already delivered before the verdict changed. ZAP reached back into mailboxes and moved it to quarantine. Mail arriving after the verdict changed. It was quarantined at delivery and never reached a mailbox. Where ZAP puts delivered mail depends on the verdict. High confidence phish is always quarantined. A plain Phishing verdict follows the anti-spam policy\u0026rsquo;s action: Move to Junk on the default policy and policies created in PowerShell, Quarantine in the Standard and Strict presets and policies created in the Defender portal. If your policy moves phish to Junk, check Junk Email folders as well as quarantine.\nHere both populations ended up in the same quarantine and are recovered the same way. The investigation still needs to tell them apart, and tell both apart from other things that can remove delivered mail.\nBlocklisted client URL: from reputation hit to recovery Outside the tenant: the hosting provider's netblock is listed, so the client's site is flagged, and because the link is in every email signature the client's mail gets a phishing verdict from URL reputation and detonation checks. Mail already delivered in the last 48 hours is pulled back out of mailboxes by ZAP; mail arriving afterwards is quarantined at delivery. Both land in quarantine, where messages are kept 15 days by default and then permanently deleted. Recovery in the tenant: a URL allow entry from an admin submission stops new mail piling up, a CSV snapshot and a single-recipient pilot come next, then a chunked release with the soonest-expiring messages first, and finally the messages are back in inboxes, verified with a hunting query. What happened Netblock listed shared hosting, so every site in range Site flagged client's URL, in every email signature Phishing verdict URL reputation and detonation checks Already delivered ZAP pulls it back out of mailboxes (last 48 hours of delivered mail) Arriving after quarantined at delivery, never reaches a mailbox Recovery Quarantine 15 days by default, then gone for good URL allow entry admin submission; stops the pile-up Snapshot + pilot CSV of everything, then one recipient Chunked release 100 at a time, expiring first Back in inboxes verified with a hunting query The real fix stayed outside: the host had to delist the netblock, and the link came out of every signature. The whole incident. The cause sat outside the tenant; everything inside it was containment and recovery. Investigation Rule out the usual suspects \u0026ldquo;Mail is vanishing\u0026rdquo; usually means a malicious inbox rule or a compromised account. That\u0026rsquo;s worth ruling out in a couple of minutes before going further:\nConnect-ExchangeOnline # Rules that delete, move or forward are the classic post-compromise signal Get-InboxRule -Mailbox user@contoso.com | Where-Object { $_.DeleteMessage -or $_.MoveToFolder -or $_.ForwardTo -or $_.RedirectTo } | Format-List Name, Enabled, MoveToFolder, DeleteMessage, ForwardTo, RedirectTo Nothing there, and the pattern was tenant-wide rather than one mailbox, which points at the filtering stack.\nWho actually moved the mail? Three things can pull a delivered message back out of a mailbox in Microsoft 365:\nMechanism What it is Where it shows up ZAP The filtering stack changes its verdict after delivery and acts on its own Phish ZAP / Malware ZAP in EmailPostDeliveryEvents AIR Automated investigation and response (Defender for Office 365 Plan 2) proposes actions; an admin approves them Manual remediation, plus an investigation in the Action center Manual remediation An admin soft- or hard-deletes or moves mail from Threat Explorer Manual remediation The difference matters. If AIR or an admin did it, someone made a decision and there\u0026rsquo;s a record of why. If it\u0026rsquo;s ZAP, the service changed its mind about the content, and the fix is about the verdict.\nThe EmailPostDeliveryEvents table answers this directly. Its ActionType column records Manual remediation, Phish ZAP or Malware ZAP, and ActionTrigger says whether an admin or a special mechanism such as ZAP triggered it. Summarize first rather than scrolling individual rows:\nEmailPostDeliveryEvents | where Timestamp \u0026gt; ago(7d) | summarize Messages = dcount(NetworkMessageId), Recipients = dcount(RecipientEmailAddress) by ActionType, ActionTrigger, ActionResult, ThreatTypes | order by Messages desc Phish ZAP dominated, with nothing in the Action center tied to these messages. To confirm the flagged URL was the common factor, join to EmailUrlInfo:\nEmailPostDeliveryEvents | where Timestamp \u0026gt; ago(7d) | where ActionType has \u0026#34;ZAP\u0026#34; | join kind=inner ( EmailUrlInfo | where UrlDomain endswith \u0026#34;contoso.com\u0026#34; // the flagged site ) on NetworkMessageId | summarize Messages = dcount(NetworkMessageId), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) Advanced Hunting and Threat Explorer need Defender for Office 365 Plan 2, where Threat Explorer can also filter on ZAP in the Additional action column. On Plan 1, Real-time detections gives a similar view. With Exchange Online Protection alone, the Mailflow status report gives you a count for a date range.\nThe two clocks: 48 hours and retention This is where most of the confusion sits. There are two different windows, and they answer different questions.\nWindow What it controls Value ZAP lookback How far back ZAP reaches into delivered mail Last 48 hours of delivered email Quarantine retention: anti-spam verdicts (spam, phish, high confidence phish, bulk) How long before quarantined copies are permanently deleted 15 days in the default anti-spam policy and new custom policies; 30 days in the Standard and Strict presets; configurable 1 to 30 Quarantine retention: malware Same 30 days, not configurable Get-QuarantineMessage query range How far back you can search Last 16 days by default, 30 days maximum The 48-hour lookback sets the blast radius; quarantine retention sets your deadline. Mail delivered more than 48 hours before the verdict flipped stayed in mailboxes. Everything else is in quarantine, and when its retention runs out it\u0026rsquo;s gone for good.\nCheck the retention actually applied to your users rather than assuming 30 days:\n# Retention in days for each anti-spam policy Get-HostedContentFilterPolicy | Format-Table Name, QuarantineRetentionPeriod Root cause Nothing in the tenant was misconfigured. The client\u0026rsquo;s hosting provider had a netblock on reputation blocklists, the client\u0026rsquo;s site lived in that block, and Microsoft\u0026rsquo;s URL reputation and detonation checks started returning a phishing verdict for the domain. Because the link was in every signature, every message the client sent, internally and externally, carried it. ZAP handled what was already delivered, and delivery-time filtering handled the rest.\nThe real fix was outside Microsoft 365. Everything in the tenant was containment and recovery.\nRemediation 1. Fix the verdict before recovering the mail Releasing mail while the URL is still flagged means new mail keeps landing in quarantine behind you. The tenant-side lever is an admin submission of the URL as a false positive:\nDefender portal › Actions \u0026amp; submissions › Submissions › URLs › Submit to Microsoft for analysis › I\u0026rsquo;ve confirmed it\u0026rsquo;s clean › Allow this URL\nThat creates a URL allow entry in the Tenant Allow/Block List. By default it stays for 45 days after it was last used, and while it exists, Safe Links detonation and URL reputation checks are skipped for that URL. Microsoft\u0026rsquo;s ZAP documentation also lists admin false-positive submissions among the cases where ZAP doesn\u0026rsquo;t act on malware and high confidence phishing.\nA wildcard such as https://www.contoso.com/* covers variations of the URL. Admin submissions are throttled (150 per 15 minutes, and the same item only three times in 24 hours), so one well-scoped URL submission does more than any number of per-message reports.\nIn parallel, the client had to work the problem upstream:\nThe hosting provider had to get the netblock delisted or move the site. In the meantime, the client removed the website link from signatures and templates, because their customers\u0026rsquo; tenants were quarantining it too, and nothing in our tenant changes another tenant\u0026rsquo;s verdict. 2. Scope the quarantine Get-QuarantineMessage returns at most 1,000 results per page, so page until a short page comes back:\n# Window covering the incident. Must fall within the last 30 days. $start = (Get-Date).AddDays(-4).Date $end = Get-Date $q = @() $page = 1 do { $batch = Get-QuarantineMessage -StartReceivedDate $start.ToUniversalTime() ` -EndReceivedDate $end.ToUniversalTime() ` -ReleaseStatus NotReleased -PageSize 1000 -Page $page $q += $batch $page++ } while ($batch.Count -eq 1000) # Narrow to the affected sender domain $hits = $q | Where-Object { $_.SenderAddress -like \u0026#34;*@contoso.com\u0026#34; } $hits.Count $hits | Measure-Object ReceivedTime -Minimum -Maximum # Which verdicts are we dealing with? This decides which switches apply later. $hits | Group-Object QuarantineTypes | Select-Object Count, Name 3. Snapshot before touching anything A release can\u0026rsquo;t be undone, so record what\u0026rsquo;s there first. The CSV is your evidence for the incident record and your reference if anything goes wrong.\n$hits | Select-Object ReceivedTime, Expires, SenderAddress, @{n = \u0026#39;Recipient\u0026#39;; e = { $_.RecipientAddress -join \u0026#39;;\u0026#39; }}, Subject, QuarantineTypes, Identity | Sort-Object ReceivedTime | Export-Csv \u0026#34;.\\pre-release-$(Get-Date -Format yyyyMMdd-HHmm).csv\u0026#34; -NoTypeInformation 4. Pilot against one recipient Release only one user\u0026rsquo;s copies and confirm they land in the inbox and stay there before going wide:\n$testUser = \u0026#34;user@contoso.com\u0026#34; # Filter by recipient on the server rather than relying on RecipientAddress in the paged results $testIds = @(Get-QuarantineMessage -RecipientAddress $testUser ` -StartReceivedDate $start.ToUniversalTime() -EndReceivedDate $end.ToUniversalTime() ` -ReleaseStatus NotReleased | Where-Object { $_.SenderAddress -like \u0026#34;*@contoso.com\u0026#34; } | Select-Object -ExpandProperty Identity) Release-QuarantineMessage -Identities $testIds -User $testUser You don\u0026rsquo;t need to exclude the pilot user from the full run. -ReleaseToAll skips recipients the message was already released to.\n5. Full release, chunked and logged Sorting by Expires releases the messages closest to deletion first, which matters if the run gets interrupted.\n$ids = @($hits | Sort-Object Expires | Select-Object -ExpandProperty Identity) $failed = @() $chunkSize = 100 for ($i = 0; $i -lt $ids.Count; $i += $chunkSize) { $chunk = $ids[$i..([Math]::Min($i + $chunkSize - 1, $ids.Count - 1))] try { Release-QuarantineMessage -Identities $chunk -ReleaseToAll -ErrorAction Stop Write-Host \u0026#34;Released $([Math]::Min($i + $chunkSize, $ids.Count)) / $($ids.Count)\u0026#34; } catch { Write-Warning \u0026#34;Chunk starting at $i failed: $($_.Exception.Message)\u0026#34; $failed += $chunk } Start-Sleep -Seconds 3 # be polite to the service } \u0026#34;Failed IDs: $($failed.Count)\u0026#34; $failed | Set-Content \u0026#34;.\\failed-ids-$(Get-Date -Format yyyyMMdd-HHmm).txt\u0026#34; # retry list If your module version complains that -Identity is missing, add -Identity 000. Microsoft documents it as required alongside -Identities, with the value ignored.\nRelease-QuarantineMessage also has a -ReportFalsePositive switch, but Microsoft documents it as applying only to messages quarantined as spam. For phish and high confidence phish verdicts, the URL submission in step 1 is the feedback that counts.\nVerification Prove the quarantine is empty for the affected set and nothing errored:\n# Expect 0 for both Get-QuarantineMessage -StartReceivedDate $start.ToUniversalTime() -EndReceivedDate $end.ToUniversalTime() ` -ReleaseStatus NotReleased, Error | Where-Object { $_.SenderAddress -like \u0026#34;*@contoso.com\u0026#34; } | Measure-Object Then prove the released mail stayed delivered. Set the release time and check for any new ZAP action on messages carrying the URL:\nEmailPostDeliveryEvents | where Timestamp \u0026gt; datetime(2026-01-01T00:00:00Z) // replace with the time the release finished | where ActionType has \u0026#34;ZAP\u0026#34; | join kind=inner (EmailUrlInfo | where UrlDomain endswith \u0026#34;contoso.com\u0026#34;) on NetworkMessageId | count Zero is the result you want. Finally, once the provider confirms the delisting, send a test message containing the link to an external Microsoft 365 mailbox you control and confirm it reaches the inbox.\nCaveats The retention clock is the real deadline\nOn the default anti-spam policy, phish and spam verdicts are kept for 15 days, not 30. Messages that expire from quarantine are permanently deleted and can\u0026rsquo;t be recovered. Check QuarantineRetentionPeriod at the start of the incident, not at the end.\nThe allow entry is a filter bypass. For as long as it exists, URL reputation and detonation are skipped for that URL. Scope it to the domain you own, set an expiry, and remove it once reputation recovers. If the site is ever compromised for real, that entry will wave the payload through. Record it as an exception. For CMMC, SOC 2, HIPAA or PCI clients, the allow entry and the bulk release belong in the change record with an owner, a reason and an expiry date. An auditor asking why mail filtering was bypassed should find the answer written down. Bulk release trusts your filter. Scope by sender and confirm the URL with hunting before releasing. A broad date-range release can put real phishing back into inboxes. You can\u0026rsquo;t fix other tenants. Recipients on Microsoft 365 quarantine your mail in their tenant. Your options are getting the reputation fixed, removing the link, or asking their admins to release and submit on their side. Licensing shapes the investigation. ZAP runs for every Exchange Online mailbox, but EmailPostDeliveryEvents needs Defender for Office 365, and AIR needs Plan 2. Key takeaways Identify who moved the mail before you act. ZAP, AIR and admin remediation look the same to users; ActionType and ActionTrigger in EmailPostDeliveryEvents tell them apart. 48 hours is the blast radius; retention is the deadline. And on the default anti-spam policy that deadline is 15 days. Fix the verdict, then recover the mail. One admin URL submission with an allow entry does more than a flood of per-message reports, and stops new mail from piling up behind you. Snapshot, pilot, chunk, verify. A CSV before, one recipient first, expiring messages first, and a hunting query after. The root cause may not be in the tenant at all. A shared-hosting netblock can take down a company\u0026rsquo;s mail without a single setting changing. References Microsoft Learn: Zero-hour auto purge in Microsoft Defender for Office 365 Microsoft Learn: Quarantined email messages in Microsoft 365 (quarantine retention) Microsoft Learn: EmailPostDeliveryEvents table in the advanced hunting schema Microsoft Learn: Submit messages, URLs, and attachments for analysis (admin submissions) Microsoft Learn: Get-QuarantineMessage Microsoft Learn: Release-QuarantineMessage ","permalink":"https://secderr.tech/posts/zap-quarantine-mass-release/","summary":"\u003caside class=\"callout callout-tldr\" role=\"note\"\u003e\n  \u003cp class=\"callout-title\"\u003eTL;DR\u003c/p\u003e\n  \u003cdiv class=\"callout-body\"\u003e\u003cp\u003eA client\u0026rsquo;s website landed on a reputation blocklist because its hosting provider\u0026rsquo;s whole netblock was listed. The link was in every email signature, so Microsoft started treating the client\u0026rsquo;s own mail as phishing: zero-hour auto purge (ZAP) pulled already-delivered messages out of mailboxes, and new mail went straight to quarantine. We confirmed it was ZAP (not AIR or an admin action) with Advanced Hunting, worked out how long we had before quarantine deleted everything, and released about 1,600 messages with a piloted, chunked PowerShell run. The release only holds once the URL itself is allowed and the reputation problem is fixed outside Microsoft 365.\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e","title":"The Client's Website Got Blocklisted, Then ZAP Came for Their Inbox"},{"content":" TL;DR\nA phishing email led to a fake Microsoft sign-in page that relayed the username, password and MFA prompt to the real login service, handing the attacker a valid session. During that same sign-in, the attacker registered a new device on the account, which gave them persistence that outlived the phishing page. Containment means revoking sessions, removing the rogue device and checking mailbox rules, because a password reset alone leaves both in place. The durable fix is phishing-resistant authentication, token protection and tighter device registration.\nThe situation The lure came from a sender the recipient already trusted. The tone was familiar, the wording was routine, and the link asked for a quick review of something that seemed urgent. Nothing about it looked wrong at a glance, and that is the point. Phishing has moved well past the misspelled-sender era, so \u0026ldquo;look for typos\u0026rdquo; is no longer a defense you can rely on.\nHow the attack worked The link didn\u0026rsquo;t go to the real site. It went to a reverse proxy styled to look like a Microsoft sign-in screen. Anything typed there was forwarded to the real service in real time, so the attacker completed the sign-in while the victim was still on the page. This is an adversary-in-the-middle (AiTM) attack, which maps to T1557 for the interception and T1566.002 for the delivery via link.\nThe part that matters most for response is what happened after sign-in. The attacker registered a new device under the account. A registered device stays bound to the identity after the phishing page is closed and the original session ends, so it works as persistence. This maps to T1098.005 (Account Manipulation: Device Registration).\nFrom there, the account itself became a delivery channel. The same kind of lure can go out from a compromised mailbox to people who trust that sender. That is internal spearphishing (T1534), and it is why one click can reach more people than the one who clicked.\nStage What the user sees What the attacker gets Lure Familiar sender, routine request, urgent tone A click Proxy page A Microsoft-looking sign-in screen Username and password as typed Relay The normal MFA prompt A valid session after MFA completes Device registration Nothing A device bound to the account Outbound mail An email from a known contact The next lure, delivered from a trusted account Fake Microsoft login: device registration and containment The attack: a lure from a trusted sender leads to a proxy page styled like Microsoft sign-in. The proxy relays the password and MFA to Microsoft in real time, the attacker finishes the sign-in live and gets a valid session, and then registers a rogue device to the user that outlives the session. The same account sends the next lure to people who trust the sender. Containment, in order: revoke sessions, remove the device, then reset the password and re-enroll MFA. A password reset on its own leaves both the session and the device in place. The attack Trusted lure familiar sender, urgent review Proxy page styled like a Microsoft sign-in Relay password and MFA sent on in real time Valid session attacker finishes the sign-in live Rogue device registered to user, outlives the session From the same account, the next lure goes out to people who trust the sender. Containment, in order Revoke sessions Revoke-MgUserSignInSession ends the stolen session Remove the device Remove-MgDevice ends the persistence Reset and re-enroll new password, fresh MFA methods A password reset on its own leaves both the session and the device in place. Sign-in is relayed, then a device is registered. Containment has to undo both, and a password reset alone does neither. First hour: triage Start with three questions: when did the account sign in from somewhere unexpected, what devices are registered to it, and what has the mailbox sent or changed. The queries below use the Microsoft Graph and Exchange Online PowerShell modules.\nConnect-MgGraph -Scopes \u0026#39;AuditLog.Read.All\u0026#39;,\u0026#39;Directory.Read.All\u0026#39; # Sign-ins for the account over the last seven days: time, app, IP, location, result Get-MgAuditLogSignIn -All -Filter \u0026#34;userPrincipalName eq \u0026#39;user@example.com\u0026#39; and createdDateTime ge $((Get-Date).AddDays(-7).ToString(\u0026#39;yyyy-MM-ddTHH:mm:ssZ\u0026#39;))\u0026#34; | Select-Object CreatedDateTime, AppDisplayName, IpAddress, Location, Status # Devices registered to the account. Anything you can\u0026#39;t tie to a real machine is a suspect. # The cmdlet returns bare directory objects, so resolve each one to get the device details. Get-MgUserRegisteredDevice -UserId user@example.com -All | ForEach-Object { Get-MgDevice -DeviceId $_.Id } | Select-Object Id, DisplayName, OperatingSystem, RegistrationDateTime # Id = the object ID used below # Inbox rules: forwarding, redirects, and rules that delete or move mail. Attackers hide rules here. # -IncludeHidden catches rules that don\u0026#39;t show in Outlook Get-InboxRule -Mailbox user@example.com -IncludeHidden | Select-Object Name, Enabled, ForwardTo, RedirectTo, DeleteMessage, MoveToFolder Also review the sent items for the lure and any replies, and check for app consents the user granted recently. Those are the attacker\u0026rsquo;s second and third stages.\nContainment Order matters here: cut access first, then repair the account.\nRevoke sessions. This invalidates the refresh tokens and sign-in sessions the attacker may be holding. Connect-MgGraph -Scopes \u0026#39;User.ReadWrite.All\u0026#39;,\u0026#39;Device.ReadWrite.All\u0026#39; Revoke-MgUserSignInSession -UserId user@example.com Disable the unknown device, then remove it, after confirming its ID from the earlier query. Disabling cuts it off straight away and keeps the record for the investigation. # Replace \u0026lt;device-id\u0026gt; with the object ID you confirmed as unrecognized Update-MgDevice -DeviceId \u0026lt;device-id\u0026gt; -AccountEnabled:$false # Once the investigation has what it needs Remove-MgDevice -DeviceId \u0026lt;device-id\u0026gt; Remove malicious inbox rules and revoke suspicious app consents.\nReset the password and re-register MFA methods, so the user is enrolling fresh methods rather than trusting the ones that may have been changed.\nA password reset by itself does not end the session or remove the device. That gap is why this attack keeps coming back on accounts that were \u0026ldquo;fixed.\u0026rdquo;\nRoot cause In this pattern, two controls usually fail together:\nMFA that can be relayed. Push approvals and one-time codes pass through the proxy along with the password. Only phishing-resistant methods, such as FIDO2 security keys and passkeys, bind the authentication to the real site. Open device registration. If any user can register devices without review, a single successful sign-in becomes permanent access. Check your own tenant against both before assuming either control is in place. Don\u0026rsquo;t assume a default is the setting you want.\nRemediation The durable fixes live in policy, not in one-off cleanup.\nControl Recommended setting Why Authentication strength Conditional Access requires phishing-resistant MFA (FIDO2, passkeys or certificate-based) for admin roles first, then all users Codes and push approvals can be relayed by AiTM proxies. FIDO2 is bound to the real origin. Token protection Enable for the sign-in scenarios your tenant supports Binds tokens to the device, so a stolen token is much less useful elsewhere Device registration Limit who can join or register devices, and cap devices per user Removes the easy persistence path Mailbox and consent monitoring Alert on new inbox rules, forwarding and new app consents Catches the attacker\u0026rsquo;s second stage early Configure authentication strengths in the Microsoft Entra admin center under Protection › Authentication methods › Authentication strengths. Device registration settings are under Devices › Device settings.\nRoll out Conditional Access in report-only mode first. Review the sign-in logs for break-glass accounts, service accounts and legitimate users who would be blocked, then enforce. Skipping this step is how a security change becomes an outage.\nVerification Use the Conditional Access What If tool with a pilot account to confirm the policy evaluates the way you intended. In a lab tenant, attempt a sign-in with a password plus an app-based code. The expected result is a block wherever a phishing-resistant strength is required. Anything else means the gap is still open. Re-run the device and inbox queries after the changes to confirm nothing reappeared. Don\u0026rsquo;t stand up a live phishing proxy against production users to test this. A lab tenant gives the same answer without the risk to real people.\nCaveats Involve legal and compliance before any notice goes out\nBreach notification and contractual obligations depend on what data the mailbox held and who it touched.\nMap the remediation to the framework your clients are audited against:\nCMMC Level 2 / NIST SP 800-171 Rev. 2: IA.L2-3.5.3 (multifactor authentication) and IA.L2-3.5.4 (replay-resistant authentication) PCI DSS v4.0: Requirement 8.4.2 (MFA for all access into the cardholder data environment) HIPAA Security Rule: 45 CFR 164.312(d) (person or entity authentication) SOC 2: CC6.1 (logical access) If phishing-resistant MFA can\u0026rsquo;t roll out this quarter, document the compensating controls and the risk acceptance, with a named owner and an end date. An open-ended exception is a finding waiting to happen.\nWhat to tell users The best user-side control is a fast report. Keep the message short:\nReport immediately, even if you\u0026rsquo;re not sure. A fast report is what limits the damage, and nobody should be punished for reporting a mistake. Don\u0026rsquo;t just delete the email. Tell the security team first so they can check the account. Watch for signs of compromise: sign-in or verification prompts you didn\u0026rsquo;t trigger, sent messages you don\u0026rsquo;t recognize, and security methods or devices you don\u0026rsquo;t recognize. Hover over links before clicking. If the domain isn\u0026rsquo;t the vendor\u0026rsquo;s real domain, don\u0026rsquo;t click. Don\u0026rsquo;t sign in from a link in an email. Go to the site directly, or contact the sender on a phone number you already have. When in doubt, don\u0026rsquo;t click. Checking a hundred harmless emails costs far less than missing one that isn\u0026rsquo;t. Key takeaways A convincing sign-in page is enough. AiTM doesn\u0026rsquo;t break the login; it relays it, so the defense has to be phishing-resistant authentication. The attacker\u0026rsquo;s real target is the session. Containment has to revoke sessions and remove devices, not just reset a password. Device registration is persistence. Restrict who can register devices, and review what is registered on a schedule. Check inbox rules and sent mail for the account. A compromised mailbox becomes the next lure. Fast reporting limits the damage. Train users to report first and ask questions later. References MITRE ATT\u0026amp;CK: T1557 Adversary-in-the-Middle, T1566.002 Spearphishing Link, T1098.005 Device Registration, T1534 Internal Spearphishing Microsoft Learn: Conditional Access authentication strengths, Token Protection in Conditional Access, What is a device identity?, Revoke user access in Microsoft Entra ID, Get-MgUserRegisteredDevice NIST SP 800-171 Rev. 2, requirements 3.5.3 and 3.5.4 (the revision CMMC Level 2 assesses against) ","permalink":"https://secderr.tech/posts/fake-microsoft-login-aitm-device-registration/","summary":"\u003caside class=\"callout callout-tldr\" role=\"note\"\u003e\n  \u003cp class=\"callout-title\"\u003eTL;DR\u003c/p\u003e\n  \u003cdiv class=\"callout-body\"\u003e\u003cp\u003eA phishing email led to a fake Microsoft sign-in page that relayed the username, password and MFA prompt to the real login service, handing the attacker a valid session. During that same sign-in, the attacker registered a new device on the account, which gave them persistence that outlived the phishing page. Containment means revoking sessions, removing the rogue device and checking mailbox rules, because a password reset alone leaves both in place. The durable fix is phishing-resistant authentication, token protection and tighter device registration.\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e","title":"The Fake Microsoft Login Page That Registered a Device"},{"content":" TL;DR\nA user clicked a \u0026ldquo;shared file\u0026rdquo; notification from a real, already-compromised vendor account. The link led to an adversary-in-the-middle (AiTM) proxy that captured their password and their completed MFA, and the attacker replayed the resulting session into our tenant. From there they read the user\u0026rsquo;s sent mail, staged their own phishing PDF in the user\u0026rsquo;s OneDrive and blasted it to a couple hundred external contacts. MFA did not fail; the session token was stolen after MFA succeeded. Cleanup took more than a password reset: session revocation, MFA re-registration, inbox rule and OAuth consent removal, and purging the staged file.\nThe situation A user\u0026rsquo;s account started sending a shared-file invitation to a couple hundred external contacts. Nobody in our tenant had been targeted by a stranger. The lure came from a vendor the user corresponded with regularly, from that vendor\u0026rsquo;s real mailbox, as a standard Microsoft 365 \u0026ldquo;is inviting you to collaborate\u0026rdquo; share notification. The shared file lived on the vendor\u0026rsquo;s own SharePoint. Every reputation check along the way passed, because everything was legitimate right up to the link inside the PDF.\nThe attacker never needed to break our controls; they borrowed a trusted relationship and walked through them.\nAiTM vendor phish: attack chain Initial access: a compromised vendor mailbox sends a share notification with a PDF hosted on the vendor's own SharePoint. The PDF links to an AiTM proxy that relays the user's password and real MFA prompt to Microsoft. Microsoft issues a session cookie, the proxy keeps a copy, and the attacker replays it with MFA already satisfied. Inside the tenant, in one session on the same day, the attacker runs recon, staging, distribution, concealment and persistence. Distribution to the user's contacts starts the same chain again for the next ring of victims. Initial access Compromised vendor mailbox a real, trusted sender Share notification PDF on the vendor's own SharePoint AiTM proxy relays password and the real MFA prompt Session cookie issued by Microsoft, copied by the proxy Cookie replayed from attacker infra, MFA already satisfied In our tenant: one session, same day Recon mailbox search, Sent Items Staging phishing PDF in OneDrive, shared Distribution a couple hundred external contacts Concealment inbox rule created Persistence enterprise app consent Each recipient who falls for it becomes the next \"compromised vendor\" for their own contacts. The chain end to end. Nothing in the first row touched our tenant\u0026rsquo;s controls: the theft happened on the attacker\u0026rsquo;s proxy. Investigation Step 1: Find the lure A 7-day message trace on the vendor\u0026rsquo;s sender address showed the share notification arriving for our user and a handful of other internal recipients. Get-MessageTrace was deprecated in September 2025, so use the V2 cmdlets (Exchange Online module 3.7.0 or later). V2 returns at most 10 days per query.\nConnect-ExchangeOnline # Everything the compromised vendor account sent into the tenant Get-MessageTraceV2 -SenderAddress \u0026#39;vendor.user@example.com\u0026#39; ` -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) | Select-Object Received, SenderAddress, RecipientAddress, Subject, Status | Export-Csv .\\lure-trace.csv -NoTypeInformation That gave us the full internal recipient list. Each of those users was contacted, their sign-in logs were checked for anything unusual, and they reset their passwords as a precaution while the investigation continued.\nStep 2: Confirm the session theft in the sign-in logs The compromised user\u0026rsquo;s sign-in log is where the attacker\u0026rsquo;s IPs come from. In an AiTM compromise, the sign-in the user completed is relayed through the proxy, and the attacker\u0026rsquo;s follow-on sessions show up from infrastructure the user has never used, with MFA already satisfied.\nThe MFA detail fields (AuthenticationRequirement, AuthenticationDetails) only exist on the beta sign-in resource. The v1.0 Get-MgAuditLogSignIn returns the rows without them, so use the beta cmdlet:\n# Install-Module Microsoft.Graph.Beta.Reports Connect-MgGraph -Scopes \u0026#39;AuditLog.Read.All\u0026#39;,\u0026#39;Directory.Read.All\u0026#39; $start = (Get-Date).ToUniversalTime().AddDays(-7).ToString(\u0026#39;yyyy-MM-ddTHH:mm:ssZ\u0026#39;) Get-MgBetaAuditLogSignIn -All -Filter \u0026#34;userPrincipalName eq \u0026#39;user@contoso.com\u0026#39; and createdDateTime ge $start\u0026#34; | Select-Object CreatedDateTime, IpAddress, AppDisplayName, ClientAppUsed, @{n=\u0026#39;Country\u0026#39;;e={$_.Location.CountryOrRegion}}, AuthenticationRequirement, @{n=\u0026#39;MfaDetail\u0026#39;;e={$_.AuthenticationDetails.AuthenticationStepResultDetail -join \u0026#39;; \u0026#39;}} | Export-Csv .\\signins.csv -NoTypeInformation Look for sign-ins where MFA shows as satisfied by a claim in the token instead of an interactive prompt, coming from an IP or ASN the user has never used. Once you have the attacker IPs, run the same query tenant-wide with ipAddress eq '\u0026lt;ip\u0026gt;' to make sure no one else is affected.\nStep 3: Reconstruct what they did The unified audit log filled in the attacker\u0026rsquo;s activity. Filter on the operations that matter for this pattern instead of pulling everything:\n$ops = @( \u0026#39;MailItemsAccessed\u0026#39;, # what they read (now logged for Audit Standard too) \u0026#39;SearchQueryInitiatedExchange\u0026#39;, # mailbox searches (Audit Premium, enabled per mailbox) \u0026#39;Send\u0026#39;, # what they sent \u0026#39;New-InboxRule\u0026#39;,\u0026#39;Set-InboxRule\u0026#39;,\u0026#39;UpdateInboxRules\u0026#39;, \u0026#39;FileUploaded\u0026#39;, # staging in OneDrive \u0026#39;SharingSet\u0026#39;,\u0026#39;AnonymousLinkCreated\u0026#39;,\u0026#39;CompanyLinkCreated\u0026#39;,\u0026#39;SecureLinkCreated\u0026#39; ) Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) ` -UserIds \u0026#39;user@contoso.com\u0026#39; -Operations $ops -ResultSize 5000 | Select-Object CreationDate, Operations, AuditData | Export-Csv .\\ual.csv -NoTypeInformation SearchQueryInitiatedExchange is an Audit (Premium) event, and it is only logged once it has been switched on for the mailbox with Set-Mailbox \u0026lt;user\u0026gt; -AuditOwner @{Add=\u0026quot;SearchQueryInitiated\u0026quot;}. If it was never enabled, an empty result means no data, not no searches.\nThe sequence, all in a single session on the same day:\nStage Attacker activity Where it showed up Recon Searched the mailbox through the API and browsed Sent Items to learn who the user talks to SearchQueryInitiatedExchange, MailItemsAccessed Staging Uploaded a credential-harvesting PDF to the user\u0026rsquo;s OneDrive and opened sharing on it FileUploaded, sharing operations Distribution Mass-mailed the share link to external contacts, plus a few targeted direct messages Send, message trace Concealment Created an inbox rule in the user\u0026rsquo;s mailbox New-InboxRule Persistence Consented to an enterprise application on the user\u0026rsquo;s behalf Entra audit log, consent operations What was not there mattered too. No sign of data exfiltration, business disruption or movement into other accounts or endpoints. This account was a launch pad, not a target: the attacker wanted the user\u0026rsquo;s address book and their reputation, and nothing else.\nStep 4: Scope the outbound blast A Purview compliance search against the user\u0026rsquo;s mailbox counted the outbound phishing messages: a couple hundred, nearly all external, with a couple sent internally. Keep these messages until notifications are done. They are your recipient list.\nConnect-IPPSSession New-ComplianceSearch -Name \u0026#39;IR-Outbound-Phish\u0026#39; ` -ExchangeLocation \u0026#39;user@contoso.com\u0026#39; ` -ContentMatchQuery \u0026#39;subject:\u0026#34;is inviting you to collaborate\u0026#34; AND sent\u0026gt;=2026-01-01\u0026#39; # set to the incident window Start-ComplianceSearch -Identity \u0026#39;IR-Outbound-Phish\u0026#39; Get-ComplianceSearch -Identity \u0026#39;IR-Outbound-Phish\u0026#39; | Select-Object Status, Items Pair it with a message trace on the user as sender, export the external recipient domains, and hand that list to whoever owns outside communication.\nStep 5: Detonate the link The staged PDF was pulled into a sandbox and the embedded link was opened there. The page behaved like a textbook AiTM kit: it proxied the real Microsoft sign-in, relayed the username and password to Microsoft, presented the real MFA prompt, and captured the session cookie that came back. The landing domain was a recently created throwaway with a long list of subdomains, several of them named after other organizations. We were not the only victim.\nWe still ran a full EDR scan on the user\u0026rsquo;s machine and reviewed recently installed applications, because you have to rule out an infostealer. With AiTM, the theft happens on the attacker\u0026rsquo;s server, so the user\u0026rsquo;s machine is usually a dead end. Check it, but don\u0026rsquo;t let it hold up the cloud-side containment.\nRoot cause The user completed a real Microsoft sign-in, including MFA, through a reverse proxy. Microsoft issued a valid session cookie, the proxy kept a copy, and the attacker replayed it from their own infrastructure. Microsoft is explicit that this is not a vulnerability in MFA. Once the cookie is stolen, the attacker is authenticated regardless of how the user signed in.\nThe vendor angle is what got the user to click. Microsoft has documented the same supplier-to-supplier chain: a compromised vendor shares a file from a legitimate SharePoint, victims are phished through it, and each new victim\u0026rsquo;s mailbox is used to phish the next ring of contacts.\nRemediation Order matters. Contain first, then remove what the attacker left behind, then reset trust.\nConnect-MgGraph -Scopes \u0026#39;User.ReadWrite.All\u0026#39;,\u0026#39;UserAuthenticationMethod.ReadWrite.All\u0026#39;, \u0026#39;DelegatedPermissionGrant.ReadWrite.All\u0026#39;,\u0026#39;Directory.ReadWrite.All\u0026#39; $u = Get-MgUser -UserId \u0026#39;user@contoso.com\u0026#39; # 1. Contain: block sign-in if the business can tolerate it, then kill refresh tokens Update-MgUser -UserId $u.Id -AccountEnabled:$false Revoke-MgUserSignInSession -UserId $u.Id # 2. Review registered MFA methods; remove anything the user didn\u0026#39;t set up Get-MgUserAuthenticationMethod -UserId $u.Id | Select-Object Id, @{n=\u0026#39;Type\u0026#39;;e={$_.AdditionalProperties.\u0026#39;@odata.type\u0026#39;}} # 3. Find delegated consent grants made by this user Get-MgOauth2PermissionGrant -All -Filter \u0026#34;principalId eq \u0026#39;$($u.Id)\u0026#39;\u0026#34; | Select-Object Id, ClientId, Scope, ConsentType # Map ClientId to an app name with Get-MgServicePrincipal -ServicePrincipalId \u0026lt;ClientId\u0026gt;, then: # Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId \u0026lt;Id\u0026gt; # 4. Inbox rules: -IncludeHidden catches rules that don\u0026#39;t show in Outlook Get-InboxRule -Mailbox \u0026#39;user@contoso.com\u0026#39; -IncludeHidden | Select-Object Name, Enabled, From, SubjectOrBodyContainsWords, MoveToFolder, MarkAsRead, DeleteMessage, ForwardTo, RedirectTo # Remove-InboxRule -Mailbox \u0026#39;user@contoso.com\u0026#39; -Identity \u0026#39;\u0026lt;rule\u0026gt;\u0026#39; Then the rest:\nAction Why Reset the password The attacker captured it in cleartext at the proxy. Re-register MFA Treat the existing registration as untrusted until verified. Remove sharing links, delete the staged PDF, purge it from the second-stage recycle bin Deleting the file isn\u0026rsquo;t enough if it can be restored and the link resurrected. Sandbox a copy first. Notify internal users and the vendor The vendor still has a compromised account phishing everyone they know. Notify external recipients Your user\u0026rsquo;s address is now the lure. Give recipients something specific to look for. Report the phishing domain Registrar abuse contacts don\u0026rsquo;t always work. Ours had a full mailbox and a looping voicemail. Report through more than one channel. Re-enable the account Only after the steps above, with the user signing in fresh. Locking down user consent Removing the malicious app grant fixes this account. The tenant setting that allowed the grant in the first place is what keeps it from happening on the next compromise. If a user can consent to an app reading their mail, so can anyone holding that user\u0026rsquo;s session.\nThe setting lives at Entra ID › Enterprise apps › Consent and permissions › User consent settings. Here are your options, from tightest to loosest:\nOption Policy ID What users can consent to Do not allow user consent none assigned Nothing. Every app needs an admin. Allow user consent for apps from verified publishers, for selected permissions microsoft-user-default-low Only permissions you classify as low impact, and only for verified publishers or apps registered in your tenant. Let Microsoft manage your consent settings microsoft-user-default-recommended Most delegated permissions, excluding mail, files, sites, calendars, chat, contacts and legacy protocol access such as EWS and IMAP. Microsoft updates the list over time. This is the default for new tenants. Allow user consent for apps microsoft-user-default-legacy Any permission that doesn\u0026rsquo;t require admin consent, from any publisher. Move off this one. For a small tenant, I\u0026rsquo;d go with Do not allow user consent, paired with the admin consent workflow (Entra ID › Enterprise apps › Consent and permissions › Admin consent settings). With the workflow on, users who hit a blocked app can request it instead of shadow-IT\u0026rsquo;ing around it, and your reviewers get an email. This is CIS Microsoft 365 Foundations 5.1.5.2. Two details to know: designating a reviewer doesn\u0026rsquo;t grant them any privileges, so pick people who already hold an app admin role, and only a Global Administrator can approve requests for apps asking for Microsoft Graph application permissions. If blocking all consent is too much friction for a client, the Microsoft-managed option is the minimum. It blocks exactly the mail and file permissions a mailbox-hijacking attacker wants.\nTo check and change it in PowerShell:\nConnect-MgGraph -Scopes \u0026#39;Policy.ReadWrite.Authorization\u0026#39; # sign in as Privileged Role Administrator # managePermissionGrantsForSelf.* is the user consent policy $current = (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions.PermissionGrantPoliciesAssigned $current # Drop the user consent policy, but keep any managePermissionGrantsForOwnedResource.* entries $keep = @($current | Where-Object { $_ -notlike \u0026#39;managePermissionGrantsForSelf.*\u0026#39; }) # Uncomment to use the Microsoft-managed policy instead of blocking consent outright # $keep += \u0026#39;managePermissionGrantsForSelf.microsoft-user-default-recommended\u0026#39; Update-MgPolicyAuthorizationPolicy -BodyParameter @{ defaultUserRolePermissions = @{ permissionGrantPoliciesAssigned = $keep } } # Confirm the result (Get-MgPolicyAuthorizationPolicy).DefaultUserRolePermissions | Format-List This change only applies to future consents. Grants that already exist stay in place, so run the Get-MgOauth2PermissionGrant review from the remediation step tenant-wide, not just on the compromised user.\nVerification Sign-ins: re-run the sign-in query. Nothing from the attacker IPs after the revocation, and the user\u0026rsquo;s next sign-in shows a fresh interactive MFA. Tenant-wide IP sweep: the attacker IPs appear on no other account. Audit log: no Send, FileUploaded or inbox rule operations from non-user IPs after containment. Mailbox and apps: Get-InboxRule -IncludeHidden is clean, and the user has no consent grants you can\u0026rsquo;t explain. Mail flow: message trace shows no further outbound sends matching the campaign. Reproduce the technique: the sandbox detonation is your proof of mechanism. If your controls are in place (see below), replaying the same flow against a test account should now fail. Caveats Revocation is not instant\nRevoke-MgUserSignInSession invalidates refresh tokens and browser session cookies, and Microsoft notes it can take a few minutes to apply. Access tokens already issued stay valid until they expire, one hour by default. Continuous Access Evaluation shortens that for supported clients and resources. It does not remove OAuth grants or inbox rules. Those survive a password reset and need their own cleanup.\nCompliance impact. The attacker read mail. Whether that triggers a notification obligation depends on what was in it:\nHIPAA: use MailItemsAccessed to determine which messages were opened, then complete the breach risk assessment for any PHI. CMMC / DFARS 252.204-7012: if CUI was in the mailbox or OneDrive, the 72-hour reporting clock applies. PCI DSS and SOC 2: document the incident against your IR plan and the related control (Requirement 12.10, CC7.3–CC7.5), including how you scoped the access. MailItemsAccessed is throttled when a mailbox generates very high access volume, so treat an absence of records as \u0026ldquo;unknown,\u0026rdquo; not \u0026ldquo;untouched.\u0026rdquo;\nFixes have limits too. Token Protection binds sign-in session tokens to a device, but it covers native applications, not browser sessions in general, which is exactly what this attack used. The control that breaks AiTM at the source is phishing-resistant authentication: FIDO2 security keys, passkeys, Windows Hello for Business or certificate-based authentication, enforced with a Conditional Access authentication strength. A FIDO2 authenticator won\u0026rsquo;t complete a challenge for the proxy\u0026rsquo;s origin. If a full rollout isn\u0026rsquo;t feasible, start with admins and the people vendors email most: finance, AP and executives. Pair it with a Conditional Access policy requiring a compliant or registered device, so a cookie replayed from attacker infrastructure has nothing to satisfy.\nKey takeaways Trusted senders are the delivery mechanism. A vendor\u0026rsquo;s real mailbox and real SharePoint will pass every reputation check. Train users that \u0026ldquo;shared a file\u0026rdquo; from a known contact still warrants a check when it\u0026rsquo;s unexpected. MFA completed is not MFA protected. Push and code-based MFA can be proxied. Phishing-resistant methods can\u0026rsquo;t be. A password reset is the start of cleanup, not the end. Revoke sessions, re-register MFA, and remove inbox rules (with -IncludeHidden) and OAuth grants every time. Scope outward. The compromised account\u0026rsquo;s sent mail is the list of the next victims. Notifying them is part of containment. Hunt by IP across the tenant. The attacker\u0026rsquo;s infrastructure is usually shared across victims. Check whether it touched anyone else before you close the ticket. References Microsoft Security Blog: From cookie theft to BEC: Attackers use AiTM phishing sites as entry point to further financial fraud Microsoft Security Blog: Detecting and mitigating a multi-stage AiTM phishing and BEC campaign Microsoft Learn: Revoke user access in Microsoft Entra ID Microsoft Learn: Continuous access evaluation Microsoft Learn: Token Protection in Conditional Access Microsoft Learn: Conditional Access authentication strengths Microsoft Learn: Get-InboxRule Microsoft Learn: Set up Audit (Premium) Microsoft Learn: Get-MessageTraceV2 Microsoft Learn: Detect and remediate illicit consent grants Microsoft Learn: Configure how users consent to applications Microsoft Learn: Manage app consent policies Microsoft Learn: Configure the admin consent workflow CIS Microsoft 365 Foundations Benchmark, 5.1.5.2: Ensure the admin consent workflow is enabled MITRE ATT\u0026amp;CK: T1586.002 Compromise Accounts: Email Accounts, T1566.002 Spearphishing Link, T1557 Adversary-in-the-Middle, T1539 Steal Web Session Cookie, T1550.004 Web Session Cookie, T1114.002 Remote Email Collection, T1608.005 Link Target, T1564.008 Email Hiding Rules, T1534 Internal Spearphishing ","permalink":"https://secderr.tech/posts/aitm-vendor-phish-microsoft-365/","summary":"\u003caside class=\"callout callout-tldr\" role=\"note\"\u003e\n  \u003cp class=\"callout-title\"\u003eTL;DR\u003c/p\u003e\n  \u003cdiv class=\"callout-body\"\u003e\u003cp\u003eA user clicked a \u0026ldquo;shared file\u0026rdquo; notification from a real, already-compromised vendor account. The link led to an adversary-in-the-middle (AiTM) proxy that captured their password and their completed MFA, and the attacker replayed the resulting session into our tenant. From there they read the user\u0026rsquo;s sent mail, staged their own phishing PDF in the user\u0026rsquo;s OneDrive and blasted it to a couple hundred external contacts. MFA did not fail; the session token was stolen after MFA succeeded. Cleanup took more than a password reset: session revocation, MFA re-registration, inbox rule and OAuth consent removal, and purging the staged file.\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e","title":"The Vendor Was the Phish: Responding to an AiTM Compromise in Microsoft 365"},{"content":" TL;DR\nSweet32 (CVE-2016-2183) flags any server still offering a 64-bit block cipher, which on Windows means 3DES and DES. Older Windows builds still ship TLS_RSA_WITH_3DES_EDE_CBC_SHA enabled by default, so it shows up on almost every external or PCI scan of a Server 2016–2022 box. Get-TlsCipherSuite finds it and Disable-TlsCipherSuite removes it, no third-party tools required. For a fleet, push the same result through the SSL Cipher Suite Order GPO or Intune, reboot, and rescan.\nThe situation If you run external or PCI ASV scans against Windows servers, you have seen this one. A quarterly scan came back with Sweet32 on a handful of internet-facing Windows hosts: IIS, an RD Gateway and a couple of boxes nobody remembered were exposed. Nothing exotic, just default Schannel configuration doing what it has done since 2016.\nThe finding is easy to fix once you know where Windows keeps the cipher suite list. The trap is fixing it the wrong way: a .reg file someone found on a forum, IIS Crypto run by hand on each box, or a GPO that quietly overrides everything you just did.\nWhat Sweet32 is Sweet32 is a birthday attack against block ciphers with a 64-bit block size. NVD describes the issue as a birthday bound of roughly four billion blocks for DES and Triple DES, which makes cleartext recovery practical against a long-lived encrypted session. It is scored CVSS 3.1 7.5 (confidentiality high).\nThe attack needs volume, not a clever exploit. The attacker has to observe a large amount of traffic on one session (collisions start around 32 GB, and the researchers needed 600 to 800 GB on one session to recover a cookie) and usually needs a way to make the victim generate it, such as injected JavaScript in a browser. That maps to T1557 Adversary-in-the-Middle and T1040 Network Sniffing. Practical risk on a typical server is low; scan and compliance risk is high, because a CVSS 7.5 finding fails a PCI ASV scan.\nOn Windows, these are the suites in scope:\nCipher suite Notes TLS_RSA_WITH_3DES_EDE_CBC_SHA The one you will almost always see TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA Supported, not enabled by default TLS_RSA_WITH_DES_CBC_SHA Single DES, same block size and a far weaker key TLS_DHE_DSS_WITH_DES_CBC_SHA Single DES SSL_CK_DES_192_EDE3_CBC_WITH_MD5 SSL 2.0 legacy Investigation Which builds are affected by default Microsoft\u0026rsquo;s per-version cipher suite tables answer this. TLS_RSA_WITH_3DES_EDE_CBC_SHA is enabled by default on Server 2016 through Server 2022 and on Windows 10 releases before 22H2. It was moved to supported-but-not-enabled in Windows 10 22H2, Server 2022 23H2, Windows 11 22H2 and Server 2025.\nOS 3DES suite enabled by default Windows 10 before 22H2, Server 2016, Server 2019, Server 2022 Yes Windows 10 22H2, Server 2022 23H2, Windows 11 22H2 and later, Server 2025 No If a newer build is flagged, something turned it back on: a GPO, an old hardening script, or an application that sets its own list. That is worth chasing before you just disable it again.\nWhat the host is actually offering Get-TlsCipherSuite returns every enabled suite in priority order. You only need the names:\n# Clean list of enabled suite names, in priority order (Get-TlsCipherSuite).Name Select-Object Name also works, but it returns objects with a header and can truncate long names in the console. The dot syntax gives you plain strings you can filter.\nOn some older builds (Server 2016 and early Windows 10), Get-TlsCipherSuite returns the list as a single collection object instead of one object per suite, and piping it to Select-Object Name comes back blank. The (Get-TlsCipherSuite).Name form avoids that.\nFilter straight to the Sweet32 suspects:\n# Anything with a 64-bit block cipher (Get-TlsCipherSuite).Name -match \u0026#39;3DES|_DES_\u0026#39; On a default Server 2016–2022 build this usually returns a single hit: TLS_RSA_WITH_3DES_EDE_CBC_SHA.\nCheck for a policy before you change anything # If this returns a Functions value, a GPO or MDM policy owns the suite list Get-ItemProperty \u0026#39;HKLM:\\SOFTWARE\\Policies\\Microsoft\\Cryptography\\Configuration\\SSL\\00010002\u0026#39; -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Functions If that returns a list, local changes will not stick. Microsoft\u0026rsquo;s documentation is explicit that a configured priority list overrides the default order and any suite not in it is not used. Fix it in the policy (see Remediation).\nRoot cause Nothing is broken. Older Windows builds include 3DES in the default list for backward compatibility with clients that cannot negotiate AES. Scanners connect, offer 3DES, Schannel accepts it, and the finding fires. The fix is a configuration change, not a patch.\nRemediation Single host or RMM script Disable the suite by name:\n# Run elevated Disable-TlsCipherSuite -Name \u0026#39;TLS_RSA_WITH_3DES_EDE_CBC_SHA\u0026#39; For an RMM script where you do not want to care what each box has, filter first and then loop:\n# Only disable what is actually present (Get-TlsCipherSuite).Name -match \u0026#39;3DES|_DES_\u0026#39; | ForEach-Object { Disable-TlsCipherSuite -Name $_ } Filter first. Disable-TlsCipherSuite errors when handed a suite that is not in the current list, so a hard-coded list of every 3DES name will throw on most machines and make your RMM output useless.\nThe TLS module requires Windows 10 or Server 2016 or later, and an elevated session. Microsoft notes that cipher suite order changes take effect on the next boot, so schedule a reboot.\nFleet: Group Policy or Intune For anything you manage centrally, put the list in policy so it survives rebuilds and drift:\nGPO: Computer Configuration › Policies › Administrative Templates › Network › SSL Configuration Settings › SSL Cipher Suite Order. Enable it and paste a comma-separated list of the suites you want. Intune: the Policy CSP setting Cryptography/TLSCipherSuites, via a settings catalog profile. Build the list from a known-good reference host instead of typing it:\n# Current enabled suites minus anything weak, ready to paste into the GPO ((Get-TlsCipherSuite).Name | Where-Object { $_ -notmatch \u0026#39;3DES|_DES_|NULL|RC4\u0026#39; }) -join \u0026#39;,\u0026#39; The GPO field is limited to 1,023 characters, so trim suites you do not need (CBC-mode and DHE suites are the usual candidates) if the string runs long. Anything not in the list is disabled, so this one setting removes 3DES from every host in scope.\nOptional: disable 3DES in Schannel The Schannel Ciphers key turns 3DES off for everything that uses Schannel, independent of the suite list. Microsoft now recommends controlling ciphers through the suite order rather than these keys, so treat this as belt and suspenders for a scan, not the primary fix.\n# .NET API because \u0026#39;Triple DES 168/168\u0026#39; contains a slash, which the registry provider treats as a path separator $base = [Microsoft.Win32.Registry]::LocalMachine.CreateSubKey(\u0026#39;SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\SCHANNEL\\Ciphers\u0026#39;) foreach ($c in \u0026#39;Triple DES 168\u0026#39;, \u0026#39;Triple DES 168/168\u0026#39;, \u0026#39;DES 56/56\u0026#39;) { $k = $base.CreateSubKey($c) $k.SetValue(\u0026#39;Enabled\u0026#39;, 0, \u0026#39;DWord\u0026#39;) # 0 = disabled $k.Close() } $base.Close() Key Applies to Value Ciphers\\Triple DES 168 Windows Vista / Server 2008 and later Enabled = 0 Ciphers\\Triple DES 168/168 Pre-Vista Enabled = 0 Ciphers\\DES 56/56 All Enabled = 0 Setting both 3DES key names is harmless and covers the odd legacy host. Reboot after changing Schannel keys.\nWhile you are in there: NULL suites You will notice suites like TLS_RSA_WITH_NULL_SHA256 in the list. NULL means no encryption, only integrity. Microsoft\u0026rsquo;s cipher suite documentation marks them as used only when an application explicitly requests them, and Schannel filters RC4, DES, export and NULL suites out when the application asks for strong crypto (SCH_USE_STRONG_CRYPTO). They are rarely negotiated, but they are still in the list. If a scanner flags them anyway or you want a clean list:\n(Get-TlsCipherSuite).Name -match \u0026#39;NULL\u0026#39; | ForEach-Object { Disable-TlsCipherSuite -Name $_ } Verification On the host, after the reboot:\n# No output means nothing weak is left in the offer list (Get-TlsCipherSuite).Name -match \u0026#39;3DES|_DES_|NULL\u0026#39; Then reproduce what the scanner does, from another machine:\n# Enumerate offered suites; 3DES should be gone and nothing should grade below A nmap --script ssl-enum-ciphers -p 443 \u0026lt;host\u0026gt; # Try to force a 3DES handshake; this should fail with a handshake error openssl s_client -connect \u0026lt;host\u0026gt;:443 -tls1_2 -cipher DES-CBC3-SHA Repeat for every TLS listener, not just 443: RDP (3389), LDAPS (636), SMTP with STARTTLS and anything else the scan touched. Then request a rescan to close the finding.\nCaveats Check for legacy clients first\nAnything that can only speak 3DES will stop connecting: Windows XP and Server 2003, old embedded devices, some printers, scanners and line-of-business appliances. Pull Schannel or load balancer logs for negotiated suites before pushing this fleet-wide, and pilot on a small group.\nGroup Policy wins. If SSL Cipher Suite Order is set by policy, local cmdlet changes are overridden. Change the GPO. Not everything uses Schannel. Java, OpenSSL-based apps, Apache and many appliances bring their own TLS stack and their own cipher configuration. If the finding is on a port served by one of those, this post does not fix it. Compliance. PCI DSS v4.0 Requirement 4.2.1 requires strong cryptography for cardholder data over public networks, and a CVSS 4.0 or higher finding fails an ASV scan. NIST SP 800-131A Rev. 2 disallows Triple DES for encryption after 2023, which matters for CMMC (SC.L2-3.13.11) and anything else that leans on NIST guidance. HIPAA transmission security (164.312(e)(1)) does not name ciphers, but auditors will. Risk acceptance. If a legacy dependency keeps 3DES alive on one host, document a compensating control (network segmentation, short session lifetimes, allow-listing the client) and get a formal exception. Do not leave it enabled fleet-wide for one printer. Sweet32 is rarely alone. TLS 1.0/1.1 and RC4 usually sit right next to it on the report. Plan those in the same change window. Key takeaways Sweet32 on Windows is a default-configuration finding on Server 2016–2022 and Windows 10 releases before 22H2, not a missing patch. (Get-TlsCipherSuite).Name -match '3DES|_DES_' tells you exactly what a host offers in one line. Filter before you disable; hard-coded lists throw errors and hide real failures in RMM output. Use the SSL Cipher Suite Order GPO or Intune for anything managed centrally, and check for an existing policy before touching a host locally. Prove the fix from outside with the same technique the scanner uses, on every TLS port, not just 443. References CVE-2016-2183 (NVD) Sweet32: Birthday attacks on 64-bit block ciphers in TLS and OpenVPN Disable-TlsCipherSuite (Microsoft Learn) Manage Transport Layer Security (TLS) (Microsoft Learn) TLS registry settings (Microsoft Learn) Restrict cryptographic algorithms and protocols in Schannel.dll (Microsoft Learn) TLS cipher suites in Windows Server 2022 (Microsoft Learn) TLS cipher suites in Windows 10 v22H2 (Microsoft Learn) NIST SP 800-131A Rev. 2 MITRE ATT\u0026amp;CK T1557 ","permalink":"https://secderr.tech/posts/disabling-sweet32-cipher-suites-powershell/","summary":"\u003caside class=\"callout callout-tldr\" role=\"note\"\u003e\n  \u003cp class=\"callout-title\"\u003eTL;DR\u003c/p\u003e\n  \u003cdiv class=\"callout-body\"\u003e\u003cp\u003eSweet32 (CVE-2016-2183) flags any server still offering a 64-bit block cipher, which on Windows means 3DES and DES. Older Windows builds still ship \u003ccode\u003eTLS_RSA_WITH_3DES_EDE_CBC_SHA\u003c/code\u003e enabled by default, so it shows up on almost every external or PCI scan of a Server 2016–2022 box. \u003ccode\u003eGet-TlsCipherSuite\u003c/code\u003e finds it and \u003ccode\u003eDisable-TlsCipherSuite\u003c/code\u003e removes it, no third-party tools required. For a fleet, push the same result through the SSL Cipher Suite Order GPO or Intune, reboot, and rescan.\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e","title":"Killing Sweet32 on Windows with PowerShell"},{"content":" TL;DR\nDirect Send lets anything on the internet deliver mail to your M365 tenant as your own domain, unauthenticated. Attackers abuse it for internal-looking phishing. Microsoft\u0026rsquo;s RejectDirectSend switch kills it, but you need to find your legitimate senders (printers, scanners, LOB apps) first and give them an inbound connector. Find them in Defender Advanced Hunting, create connectors scoped to their IPs, flip the switch, and verify your critical mail still flows.\nThe vulnerability: why Direct Send is a spoofing magnet Direct Send was built for convenience. It lets devices such as printers and scanners, as well as legacy applications, send email without a dedicated account. The catch is in that last part: no account means no authentication. Anything that can reach your tenant\u0026rsquo;s MX endpoint on port 25 can hand it a message, and Exchange Online will deliver it.\nExploiting it doesn\u0026rsquo;t take much. Your MX endpoint follows a predictable pattern (contoso-com.mail.protection.outlook.com), so an attacker only needs your organization\u0026rsquo;s domain and a valid recipient to send phishing mail, without ever logging in to the tenant. They don\u0026rsquo;t need a compromised mailbox, stolen credentials, or malware. A one-line Send-MailMessage from any internet-connected box is enough, and defenders have seen this in the wild.\nWhy it works so well:\nIt looks internal. The From address is your own domain, so the message reads like it came from HR, payroll, or the CEO. It rides Microsoft\u0026rsquo;s infrastructure. Because messages are routed through Microsoft\u0026rsquo;s own systems, they can evade initial filters by being treated as internal email. Users trust internal mail. People are trained to be suspicious of external senders, not their coworkers. This is actively exploited. Varonis Threat Labs uncovered a campaign targeting more than 70 organizations, where emails looked like voicemail notifications with PDF attachments containing QR codes that led to credential-harvesting pages. The QR code is deliberate: it pushes victims onto their phones, outside managed endpoint protections. KnowBe4\u0026rsquo;s Threat Lab observed 29,785 confirmed phishing emails abusing Direct Send across July and August 2026.\nHere\u0026rsquo;s what Direct Send abuse looks like in message headers:\nX-MS-Exchange-Organization-AuthAs: Anonymous received-spf: Fail (protection.outlook.com: domain of \u0026lt;domain\u0026gt; does not designate \u0026lt;ip\u0026gt; as permitted sender) authentication-results: spf=fail smtp.mailfrom=\u0026lt;domain\u0026gt;; dkim=none; dmarc=fail action=none x-ms-exchange-crosstenant-fromentityheader: Internet The sender is your domain, but the connecting IP is external and unknown. SPF, DKIM, and DMARC fail. No inbound connector is attributed. That combination means someone outside your tenant is spoofing your domain.\nMicrosoft\u0026rsquo;s answer is the RejectDirectSend switch, introduced in April 2025. Here\u0026rsquo;s how to roll it out without breaking the equipment that actually needs it.\n1. Check where you stand Connect-ExchangeOnline Get-OrganizationConfig | Select-Object Name, RejectDirectSend False means you\u0026rsquo;re still accepting anonymous mail spoofed as your domain. True means you\u0026rsquo;re already protected.\n2. Find out who\u0026rsquo;s actually using Direct Send Before you flip the switch, inventory your legitimate senders: the copiers, scanners, and LOB apps that rely on Direct Send. In Defender Advanced Hunting, run this query across the past 30 days:\nlet myDomains = dynamic([\u0026#34;contoso.com\u0026#34;, \u0026#34;contoso.mail.onmicrosoft.com\u0026#34;]); EmailEvents | where Timestamp \u0026gt; ago(30d) | where EmailDirection == \u0026#34;Inbound\u0026#34; | where SenderFromDomain in~ (myDomains) | where isempty(Connectors) | summarize Count=count(), Senders=make_set(SenderFromAddress, 25) by SenderIPv4 | sort by Count desc The results show inbound mail spoofing your domain without an inbound connector. The IPs you recognize are your devices. The ones you don\u0026rsquo;t are the problem.\nPay attention to the sender addresses too. If you see scanner@contoso.com or printer-hq@contoso.com repeatedly from the same IP, that\u0026rsquo;s a Direct Send appliance. If you see random mailboxes from odd IPs, that\u0026rsquo;s abuse.\n3. Give legitimate senders a front door For each known sender IP, create an inbound connector so the tenant can attribute its mail and bypass the Direct Send block:\nNew-InboundConnector -Name \u0026#34;DirectSend-Allow-Scanner-HQ\u0026#34; ` -ConnectorType Partner ` -SenderDomains * ` -SenderIPAddresses 203.0.113.10 ` -RestrictDomainsToIPAddresses $true ` -RequireTls $false Keep the IP list tight. A connector covering a /24 or wider just reopens the hole you\u0026rsquo;re closing.\nIf the device also needs to send mail to external recipients, use -ConnectorType OnPremises instead, which lets it relay through the tenant to internet destinations. If the appliance can present a TLS certificate, use cert-based authentication instead of IP:\n# TlsSenderCertificateName = the SMTP domain on a publicly trusted cert, not a CN= subject # RestrictDomainsToCertificate = only mail presenting that cert can use the connector New-InboundConnector -Name \u0026#34;DirectSend-Allow-Printer-Cert\u0026#34; ` -ConnectorType Partner ` -SenderDomains * ` -TlsSenderCertificateName \u0026#34;printer.contoso.com\u0026#34; ` -RestrictDomainsToCertificate $true ` -RequireTls $true -TlsSenderCertificateName takes the SMTP domain on the certificate (wildcards such as *.contoso.com work), so an internal .local name won\u0026rsquo;t match. Without -RestrictDomainsToCertificate, the certificate doesn\u0026rsquo;t decide which senders can use the connector. With it, cert-based connectors are stronger than IP pinning because the appliance has to prove its identity on every connection.\n4. Flip the switch Set-OrganizationConfig -RejectDirectSend $true Get-OrganizationConfig | Select-Object RejectDirectSend Give the change about 30 minutes to propagate across all edge servers. Any unauthorized sender will now bounce with:\n550 5.7.68 TenantInboundAttribution; Direct Send not allowed for this organization from unauthorized sources 5. Verify the fix Send a test from a device behind an allowed IP:\nSend-MailMessage -SmtpServer \u0026#34;contoso-com.mail.protection.outlook.com\u0026#34; -Port 25 ` -From \u0026#34;scanner@contoso.com\u0026#34; -To \u0026#34;you@contoso.com\u0026#34; ` -Subject \u0026#34;Direct Send test\u0026#34; -Body \u0026#34;If you see this, the connector works.\u0026#34; Check Advanced Hunting to confirm the Connectors column shows your connector name. If you get a 5.7.68 bounce instead, the egress IP the tenant sees doesn\u0026rsquo;t match the connector—usually NAT on your network. Work with your infrastructure team to find the real public IP the appliance uses.\nIf something critical breaks, rolling back takes one line:\nSet-OrganizationConfig -RejectDirectSend $false But don\u0026rsquo;t stop there.\nDon\u0026rsquo;t leave the door cracked open RejectDirectSend only covers spoofing of your accepted domains. While you\u0026rsquo;re in there, tighten your mail authentication:\nAudit and remove legacy allow lists. Transport rules that set SCL to -1 (skip spam filtering) or allow lists that include your own domain weaken your defenses. Remove them. Lock down SPF. Make sure your SPF record ends with -all, not ~all. The tilde is a soft fail; the dash is a hard fail. Move DMARC to enforcement. Change p=none to p=quarantine (safer for a first rollout) or p=reject (if you\u0026rsquo;re confident in your SPF and DKIM). Monitor rua reports for a few weeks before you reject. Enable anti-phish impersonation protection. Make sure Microsoft\u0026rsquo;s anti-phishing rules cover lookalike domains and unusual mailbox activity from your own domain. Direct Send made sense when every office had a copier and spoofing wasn\u0026rsquo;t a threat. That time is over. Turn it off, keep your printers working, and move on.\nKey takeaways Direct Send is unauthenticated SMTP to your tenant\u0026rsquo;s MX endpoint, and it\u0026rsquo;s actively exploited for internal-looking phishing. Find your legitimate senders first in Defender Advanced Hunting before you enable RejectDirectSend. Inbound connectors let you whitelist specific IPs or certificates so your devices can still send mail. IP-based connectors are good; cert-based connectors are better. RejectDirectSend is one layer. SPF, DKIM, DMARC, and anti-phish rules are the rest. References Introducing more control over Direct Send in Exchange Online (Microsoft Tech Community) MC1093237: New Reject Direct Send parameter (Microsoft 365 Message Center) How to set up a multifunction device or application to send email using Microsoft 365 or Office 365 (Microsoft Learn) Set-OrganizationConfig (Microsoft Learn) New-InboundConnector (Microsoft Learn) EmailEvents table in the advanced hunting schema (Microsoft Learn) Set up SPF to identify valid email sources for your Microsoft 365 domain (Microsoft Learn) Set up DMARC to validate email in Microsoft 365 (Microsoft Learn) Ongoing Campaign Abuses Microsoft 365\u0026rsquo;s Direct Send to Deliver Phishing Emails (Varonis Threat Labs) Phishing campaign leverages Microsoft 365 Direct Send feature (SC Media, reporting KnowBe4 Threat Lab data) MITRE ATT\u0026amp;CK T1566.001: Spearphishing Attachment ","permalink":"https://secderr.tech/posts/blocking-direct-send/","summary":"\u003caside class=\"callout callout-tldr\" role=\"note\"\u003e\n  \u003cp class=\"callout-title\"\u003eTL;DR\u003c/p\u003e\n  \u003cdiv class=\"callout-body\"\u003e\u003cp\u003eDirect Send lets anything on the internet deliver mail to your M365 tenant as your own domain, unauthenticated. Attackers abuse it for internal-looking phishing. Microsoft\u0026rsquo;s \u003ccode\u003eRejectDirectSend\u003c/code\u003e switch kills it, but you need to find your legitimate senders (printers, scanners, LOB apps) first and give them an inbound connector. Find them in Defender Advanced Hunting, create connectors scoped to their IPs, flip the switch, and verify your critical mail still flows.\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e","title":"Shutting Down Direct Send in Microsoft 365 (Without Breaking Your Printers)"},{"content":" TL;DR\nA scan flagged a Windows host for null session access to IPC$ (CVE-2000-1200) and labeled it a NetBIOS issue. NetBIOS over TCP/IP was already disabled. The actual exposure was anonymous SMB on port 445 reaching the lsarpc named pipe, which remained on the server\u0026rsquo;s anonymous exception list because our hardening GPO never defined it. The fix belongs in Group Policy, and the proof belongs on the host.\nThe finding A PCI-relevant vulnerability scan reported a Windows host for null session / IPC$ access (CVE-2000-1200). Both the title and the category referenced NetBIOS, on a host where NetBIOS over TCP/IP (NetBT) was supposed to be disabled.\nThe natural assumption was that NetBT was still enabled somewhere: a hidden adapter, a virtual switch, or a binding nobody had checked. That assumption turned out to be wrong, but it was still worth ruling out properly. \u0026ldquo;I assumed\u0026rdquo; is not an answer an assessor accepts.\nRuling out NetBIOS over TCP/IP The host was a Hyper-V server running on KVM, with a VirtIO NIC bound to the virtual switch. That meant several adapters to account for: physical, virtual, ISATAP and Teredo. I validated NetBT state three independent ways.\nCheck What it shows Result WMI (Win32_NetworkAdapterConfiguration) NetBT setting per IP-enabled adapter Hyper-V vEthernet set to 2 (disabled) Registry (NetBT\\Parameters\\Interfaces) Every interface, including hidden ones All interfaces set to 2 nbtstat -n The local NetBIOS name table Node IP 0.0.0.0, empty table # Per adapter (0 = DHCP, 1 = enabled, 2 = disabled) Get-CimInstance Win32_NetworkAdapterConfiguration | Select-Object Index, Description, IPEnabled, TcpipNetbiosOptions # Every interface in the registry, including hidden and virtual Get-ChildItem \u0026#39;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\NetBT\\Parameters\\Interfaces\u0026#39; | ForEach-Object { [pscustomobject]@{ Interface = $_.PSChildName NetbiosOptions = (Get-ItemProperty $_.PSPath).NetbiosOptions } } # Local name table: no names means NetBT is not bound nbtstat -n # Listeners on the NetBIOS and SMB ports Get-NetTCPConnection -LocalPort 139,445 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, OwningProcess Get-NetUDPEndpoint -LocalPort 137,138 -ErrorAction SilentlyContinue Blank WMI values on the ISATAP, Teredo and VirtIO adapters are expected. WMI only reports this property for IP-enabled adapters, and the VirtIO NIC carried no IP of its own once bound to the vSwitch.\nVerify before you chase\nAt one point a column-aligned table made it look like the Hyper-V adapter was still enabled. It wasn\u0026rsquo;t; the values had shifted under the wrong header. When output looks surprising, re-run it with Format-List or fewer properties before acting on it.\nNetBT was off and port 139 was closed. The finding had to be coming from somewhere else.\nReading the finding in full The answer was in the finding\u0026rsquo;s full description, not its title:\nThe session was anonymous SMB to IPC$ over TCP 445. The scanner uses \u0026ldquo;NetBIOS\u0026rdquo; loosely, as a label for Windows file and printer sharing in general. There are two distinct enumeration paths. User enumeration through the Net* APIs is reported as a separate, companion finding. SID brute-forcing through LsarLookupSids is reported as this one. Only the SID-lookup finding fired. That distinction matters, because the classic RestrictAnonymous controls address the first path but not the second.\nThe hardening was in place, mostly Every control you would expect had been deployed:\nGet-ItemProperty \u0026#39;HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa\u0026#39; | Select-Object RestrictAnonymous, RestrictAnonymousSAM, EveryoneIncludesAnonymous Get-ItemProperty \u0026#39;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters\u0026#39; | Select-Object RestrictNullSessAccess, NullSessionPipes, NullSessionShares Setting Expected Found RestrictAnonymous 1 1 RestrictAnonymousSAM 1 1 EveryoneIncludesAnonymous 0 0 RestrictNullSessAccess 1 1 NullSessionPipes Minimal or empty netlogon, samr, lsarpc NullSessionShares Empty COMCFG The last two rows are the problem. RestrictNullSessAccess only blocks anonymous access to pipes and shares that are not on the exception lists. lsarpc was on the list, and lsarpc is exactly the pipe LsarLookupSids uses. A legacy COMCFG share was also still exposed to anonymous users.\nThe hardening had been deployed and was largely effective. One exception list left open the exact path the scanner was testing.\nWhy Group Policy didn\u0026rsquo;t catch it The GPO setting Network access: Named Pipes that can be accessed anonymously was effectively not defined, so the local default persisted.\nThere is a useful tell here. Ticking Define this policy setting in the editor pre-fills a Windows 2000-era list (COMNAP, COMNODE, SQL\\QUERY, LLSRPC, BROWSER, netlogon, samr). That list did not match the registry. When the registry doesn\u0026rsquo;t match what the GPO would write, the GPO isn\u0026rsquo;t what\u0026rsquo;s setting it.\nThe related setting Network access: Allow anonymous SID/Name translation is worth checking at the same time:\nsecedit /export /cfg \u0026#34;$env:TEMP\\sec.cfg\u0026#34; /areas SECURITYPOLICY | Out-Null Select-String -Path \u0026#34;$env:TEMP\\sec.cfg\u0026#34; -Pattern \u0026#39;LSAAnonymousNameLookup\u0026#39; # expect 0 Checking dependencies first Before tightening an anonymous access list, establish who actually relies on anonymous logons. The Security log answers that directly. Run this on the affected server and on your domain controllers:\nGet-WinEvent -FilterHashtable @{ LogName = \u0026#39;Security\u0026#39;; Id = 4624 } -MaxEvents 20000 | Where-Object { $_.Properties[5].Value -eq \u0026#39;ANONYMOUS LOGON\u0026#39; } | Group-Object { $_.Properties[18].Value } | Sort-Object Count -Descending | Select-Object Count, Name In event 4624, Properties[5] is the target user name and Properties[18] is the source IP, so the output is a ranked list of systems making anonymous logons.\nThe usual suspect in our case was a domain-joined Synology NAS. Current DSM releases authenticate with the device\u0026rsquo;s machine account through winbind and shouldn\u0026rsquo;t need anonymous access, but \u0026ldquo;shouldn\u0026rsquo;t\u0026rdquo; is not evidence. Check the logs.\nOn domain controllers, also confirm that ANONYMOUS LOGON is not a member of Pre-Windows 2000 Compatible Access:\n(Get-CimInstance Win32_ComputerSystem).DomainRole # 4 or 5 = domain controller Get-ADGroupMember \u0026#39;Pre-Windows 2000 Compatible Access\u0026#39; | Select-Object Name, SID Remediation through Group Policy A manual registry edit lasts until something reapplies the default, and it leaves no audit trail. Make the change in Group Policy under Computer Configuration › Policies › Windows Settings › Security Settings › Local Policies › Security Options:\nPolicy Setting Network access: Named Pipes that can be accessed anonymously Defined: netlogon, samr Network access: Shares that can be accessed anonymously Defined and empty Network access: Allow anonymous SID/Name translation Disabled Network access: Restrict anonymous access to Named Pipes and Shares Enabled Network access: Do not allow anonymous enumeration of SAM accounts and shares Enabled Retaining samr is acceptable here because RestrictAnonymousSAM = 1 already blocks the Net* API enumeration path. On member servers, the CIS baseline goes further and expects the anonymous pipes list to be empty.\nTo prove the theory in a lab before changing the GPO, the lists can be cleared locally. Restarting the Server service drops active SMB sessions, so keep this out of production:\n$p = \u0026#39;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters\u0026#39; Set-ItemProperty $p -Name NullSessionPipes -Value ([string[]]@()) -Type MultiString Set-ItemProperty $p -Name NullSessionShares -Value ([string[]]@()) -Type MultiString Restart-Service LanmanServer -Force Verifying the fix Confirm that the policy applied and that the effective value on the host changed:\ngpupdate /force gpresult /h \u0026#34;$env:TEMP\\gp.html\u0026#34;; Start-Process \u0026#34;$env:TEMP\\gp.html\u0026#34; Get-ItemProperty \u0026#39;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters\u0026#39; | Select-Object NullSessionPipes Microsoft documents no restart requirement for the anonymous pipes policy. If the rescan still sees the pipe, restart the Server service during a maintenance window. Then reproduce the scanner\u0026rsquo;s test from a Linux host with Impacket, using an empty username. That is what a null session is; anonymous is sent as a literal account name and fails for reasons that have nothing to do with the fix:\n# Empty username = null session lookupsid.py \u0026#39;\u0026#39;@\u0026lt;ip\u0026gt; -no-pass # Kali packaging: impacket-lookupsid \u0026#39;\u0026#39;@\u0026lt;ip\u0026gt; -no-pass Run it before the change as well, so the later failure means something. Before the fix, this walks the SID range and returns account names. Afterward, it should fail with access denied. Once you have seen that yourself, the rescan is a formality, and you have evidence on file if the finding ever resurfaces.\nCaveats Domain controllers are different\nDISA STIG and CIS baselines expect netlogon, samr and lsarpc in the anonymous pipes list on domain controllers. Removing lsarpc from a DC is a documented baseline deviation. In CMMC environments, that requires a written justification, not just a GPO change.\nThere is also no complete system-level fix for LsarLookupSids. The scanner vendor\u0026rsquo;s own guidance acknowledges that Microsoft treats it as required functionality. Where the pipe cannot be removed, a legitimate outcome is to:\nrestrict TCP 445 to trusted management and server segments, document the risk acceptance alongside the compensating controls, and track it through your normal exception process. A finding that has been understood, scoped and formally accepted is a better outcome than a change that breaks authentication the following week.\nKey takeaways Read the full finding. Scanner titles and categories can mislead; here, \u0026ldquo;NetBIOS\u0026rdquo; meant SMB on 445. Deployed is not the same as effective. A single exception list undid most of the hardening. Validate on the host, not just in the GPO. If the registry doesn\u0026rsquo;t match what the policy would write, the policy isn\u0026rsquo;t in control. Reproduce the technique yourself. It is the fastest way to confirm a fix or prove a false positive. Check dependencies in the logs, and document deviations. Your future self, and your assessor, will thank you. References CVE-2000-1200 (NVD) IPC$ share and null session behavior in Windows (Microsoft Learn) Network access: Named Pipes that can be accessed anonymously (Microsoft Learn) Network access: Restrict anonymous access to Named Pipes and Shares (Microsoft Learn) Network access: Allow anonymous SID/Name translation (Microsoft Learn) [MS-LSAT] LsarLookupSids (Opnum 15) (Microsoft Learn) 4624(S): An account was successfully logged on (Microsoft Learn) Impacket lookupsid.py (GitHub) MITRE ATT\u0026amp;CK T1087.002: Account Discovery: Domain Account ","permalink":"https://secderr.tech/posts/netbios-finding-that-wasnt/","summary":"\u003caside class=\"callout callout-tldr\" role=\"note\"\u003e\n  \u003cp class=\"callout-title\"\u003eTL;DR\u003c/p\u003e\n  \u003cdiv class=\"callout-body\"\u003e\u003cp\u003eA scan flagged a Windows host for null session access to \u003ccode\u003eIPC$\u003c/code\u003e (CVE-2000-1200) and labeled it a NetBIOS issue. NetBIOS over TCP/IP was already disabled. The actual exposure was anonymous SMB on port 445 reaching the \u003ccode\u003elsarpc\u003c/code\u003e named pipe, which remained on the server\u0026rsquo;s anonymous exception list because our hardening GPO never defined it. The fix belongs in Group Policy, and the proof belongs on the host.\u003c/p\u003e\n\u003c/div\u003e\n\u003c/aside\u003e\n\n\u003ch2 id=\"the-finding\"\u003eThe finding\u003c/h2\u003e\n\u003cp\u003eA PCI-relevant vulnerability scan reported a Windows host for \u003cstrong\u003enull session / \u003ccode\u003eIPC$\u003c/code\u003e access (CVE-2000-1200)\u003c/strong\u003e. Both the title and the category referenced NetBIOS, on a host where NetBIOS over TCP/IP (NetBT) was supposed to be disabled.\u003c/p\u003e","title":"The NetBIOS Finding That Wasn't About NetBIOS"}]