Executive Summary

APT28 (also known as Fancy Bear, Forest Blizzard, and assessed with high confidence to be Russia's GRU Unit 26165) has been actively exploiting vulnerable SOHO routers since 2024, with activity continuing into 2026, to conduct DNS hijacking at scale. By compromising devices such as TP-Link WR841N routers via CVE-2023-50224 and MikroTik devices, the actor modifies DHCP/DNS settings to redirect client devices through attacker-controlled Virtual Private Servers running dnsmasq. Selective DNS poisoning then intercepts traffic to high-value services, principally Microsoft Outlook and Office 365 endpoints, enabling Adversary-in-the-Middle (AitM) attacks that harvest passwords and OAuth tokens. The campaign is assessed to be opportunistic in its first stage, with the actor triaging the resulting credential pool to identify victims of strategic intelligence value.

Key Risks

Credential Theft at Scale

Credential theft at scale is the primary risk, with OAuth tokens particularly dangerous as they can persist beyond password resets and bypass MFA.

Downstream Compromise

Downstream compromise is a significant concern since harvested credentials may be used from infrastructure not listed in this advisory, making attribution and blocking harder.

Broad Attack Surface

The attack surface is broad given that many TP-Link SOHO models are affected and likely unpatched, and router-level poisoning silently affects all devices on a network without any malware being deployed on endpoints.

Detection Difficulty

Detection is difficult as DNS hijacking is transparent to end users, and selective resolution (only poisoning targeted lookups) reduces anomaly signals considerably.

KQL Detection

This KQL is used for enterprises to review exposure. It is particularly useful when working in a hybrid or work from home model.

T1557 T1583.002 T1584.008 T1190

1. APT28 DNS Hijacking and AitM: Full Detection Suite

An eight-section compound query covering network connections to known APT28 IPs across two clusters, suspicious Outlook domain resolution (non-Microsoft IPs), APT28 VPS SSH banner ports, direct DNS port 53 traffic to APT28 servers, Entra ID sign-ins from APT28 IPs, Office 365 activity from APT28 IPs, and a TP-Link device inventory sweep for vulnerable models. Source: NCSC Advisory, April 2026.

// ============================================================
// APT28 DNS Hijacking & AitM Detection Query
// Source: NCSC Advisory - April 2026
// CVE Referenced: CVE-2023-50224
// ============================================================

// -------------------------------------------------------------------
// SECTION 1: Define all APT28 IOC sets
// -------------------------------------------------------------------

let APT28_Cluster1_IPs = dynamic([
    "5.226.137.151","5.226.137.230","5.226.137.231","5.226.137.232",
    "5.226.137.234","5.226.137.235","5.226.137.242","5.226.137.243",
    "5.226.137.244","5.226.137.245","23.106.120.119","37.221.64.77",
    "37.221.64.78","37.221.64.93","37.221.64.101","37.221.64.116",
    "37.221.64.131","37.221.64.148","37.221.64.149","37.221.64.150",
    "37.221.64.151","37.221.64.163","37.221.64.173","37.221.64.199",
    "37.221.64.208","37.221.64.224","37.221.64.254","64.120.31.96",
    "64.120.31.97","64.120.31.98","64.120.31.99","64.120.31.100",
    "77.83.197.37","77.83.197.38","77.83.197.39","77.83.197.40",
    "77.83.197.41","77.83.197.42","77.83.197.43","77.83.197.44",
    "77.83.197.45","77.83.197.46","77.83.197.47","77.83.197.48",
    "77.83.197.49","77.83.197.50","77.83.197.51","77.83.197.52",
    "77.83.197.53","77.83.197.54","77.83.197.55","77.83.197.56",
    "77.83.197.57","77.83.197.58","77.83.197.59","77.83.197.60",
    "79.141.160.78","79.141.161.66","79.141.161.67","79.141.161.68",
    "79.141.161.69","79.141.161.70","79.141.161.71","79.141.161.72",
    "79.141.161.73","79.141.161.74","79.141.161.75","79.141.161.76",
    "79.141.161.77","79.141.161.78","79.141.161.79","79.141.161.80",
    "79.141.161.81","79.141.161.82","79.141.161.83","79.141.161.84",
    "79.141.161.85","79.141.173.70","79.141.173.96","79.141.173.97",
    "79.141.173.98","79.141.173.103","79.141.173.119","79.141.173.120",
    "79.141.173.121","79.141.173.122","79.141.173.211","79.141.173.231",
    "79.141.173.232","79.141.173.233","185.117.88.22","185.117.88.28",
    "185.117.88.29","185.117.88.30","185.117.88.31","185.117.88.50",
    "185.117.88.60","185.117.88.61","185.117.88.62","185.117.89.32",
    "185.117.89.46","185.117.89.47","185.237.166.55","185.237.166.56",
    "185.237.166.57","185.237.166.58","185.237.166.59","185.237.166.60",
    "185.237.166.61","185.237.166.62","185.237.166.63","185.237.166.64",
    "185.237.166.65","185.237.166.66","185.237.166.67","185.237.166.68",
    "185.237.166.69","185.237.166.70","185.237.166.71","185.237.166.72",
    "185.237.166.73","185.237.166.74","185.237.166.75","185.237.166.224",
    "185.237.166.225","185.237.166.226","185.237.166.227","185.237.166.228",
    "185.237.166.229","185.237.166.230","185.237.166.231","185.237.166.232",
    "185.237.166.233","185.237.166.234","185.237.166.235","185.237.166.236",
    "185.237.166.237","185.237.166.238","185.237.166.239","185.237.166.240",
    "185.237.166.241","185.237.166.242","185.237.166.243","185.237.166.244",
    "185.237.166.245","185.237.166.246","185.237.166.247","185.237.166.248",
    "185.237.166.249"
]);

let APT28_Cluster2_IPs = dynamic([
    "64.44.154.227","64.44.154.237","64.44.154.238","64.44.154.239",
    "64.44.154.240","77.83.198.39","79.141.173.123","79.141.173.200",
    "79.141.173.210","79.141.173.246","79.141.173.247","79.141.173.248",
    "79.141.173.249","79.141.173.250","79.141.173.251","79.141.173.252",
    "79.141.173.253","79.141.173.254","79.143.87.229","79.143.87.232",
    "79.143.87.240","79.143.87.243","79.143.87.249","88.80.148.49",
    "88.80.148.53","89.150.40.43","89.150.40.86","103.140.186.148",
    "103.140.186.149","103.140.186.155","185.234.73.58","185.234.73.61",
    "185.234.73.62"
]);

let APT28_All_IPs = array_concat(APT28_Cluster1_IPs, APT28_Cluster2_IPs);

let APT28_Targeted_Domains = dynamic([
    "autodiscover-s.outlook.com",
    "imap-mail.outlook.com",
    "outlook.live.com",
    "outlook.office.com",
    "outlook.office365.com"
]);

// APT28 infrastructure SSH ports (banner patterns)
let APT28_SSH_Ports = dynamic([56777, 35681]);

// -------------------------------------------------------------------
// SECTION 2: Network connections to known APT28 IPs (all clusters)
// -------------------------------------------------------------------

let NetworkHits =
    CommonSecurityLog
    | where TimeGenerated > ago(30d)
    | where DestinationIP in (APT28_All_IPs) or SourceIP in (APT28_All_IPs)
    | extend
        Direction = iff(DestinationIP in (APT28_All_IPs), "Outbound-to-APT28", "Inbound-from-APT28"),
        Cluster = iff(
            DestinationIP in (APT28_Cluster1_IPs) or SourceIP in (APT28_Cluster1_IPs),
            "Cluster1-DNS-AitM",
            "Cluster2-RouterOps"
        ),
        DetectionType = "NetworkConnection-APT28-IP"
    | project TimeGenerated, DeviceName, SourceIP, DestinationIP, DestinationPort,
              Direction, Cluster, DetectionType, Activity;

// -------------------------------------------------------------------
// SECTION 3: Suspicious DNS responses for APT28-targeted Outlook domains
// Legitimate Microsoft IP ranges excluded - any other IP is suspicious
// -------------------------------------------------------------------

let LegitMicrosoftRanges = dynamic([
    "13.107.", "20.190.", "40.96.", "40.104.",
    "52.96.", "104.47.", "23.103.", "131.253."
]);

let PoisonedOutlookDns =
    DnsEvents
    | where TimeGenerated > ago(30d)
    | where Name in~ (APT28_Targeted_Domains)
    | where not(
        IPAddresses has_any (LegitMicrosoftRanges)
    )
    | where isnotempty(IPAddresses)
    | extend DetectionType = "DNS-Hijack-OutlookDomain-SuspiciousResolution"
    | project TimeGenerated, Computer, Name, QueryType, IPAddresses, DetectionType;

// -------------------------------------------------------------------
// SECTION 4: Connections on APT28 SSH banner ports (56777 or 35681)
// indicative of C2 or VPS infrastructure reachability checks
// -------------------------------------------------------------------

let SuspiciousSSHPorts =
    CommonSecurityLog
    | where TimeGenerated > ago(30d)
    | where DestinationPort in (APT28_SSH_Ports)
    | extend DetectionType = "APT28-VPS-BannerPort-SSH"
    | project TimeGenerated, DeviceName, SourceIP, DestinationIP,
              DestinationPort, DetectionType, Activity;

// -------------------------------------------------------------------
// SECTION 5: Firewall/Proxy traffic to APT28 IPs on DNS port 53
// Devices querying APT28 IPs directly over UDP/TCP 53
// -------------------------------------------------------------------

let MaliciousDnsServerTraffic =
    CommonSecurityLog
    | where TimeGenerated > ago(30d)
    | where DestinationPort == 53
    | where DestinationIP in (APT28_All_IPs)
    | extend
        DetectionType = "DirectDNS-Query-to-APT28-Server",
        Cluster = iff(
            DestinationIP in (APT28_Cluster1_IPs),
            "Cluster1-DNS-AitM",
            "Cluster2-RouterOps"
        )
    | project TimeGenerated, DeviceName, SourceIP, DestinationIP, DestinationPort,
              Cluster, DetectionType, Activity;

// -------------------------------------------------------------------
// SECTION 6: Azure AD / Entra ID sign-ins from APT28 IPs
// Covers stolen credentials being replayed post-AitM
// -------------------------------------------------------------------

let StolenCredSignIns =
    SigninLogs
    | where TimeGenerated > ago(30d)
    | where IPAddress in (APT28_All_IPs)
    | extend
        DetectionType = "EntraSignIn-From-APT28-IP",
        Cluster = iff(
            IPAddress in (APT28_Cluster1_IPs),
            "Cluster1-DNS-AitM",
            "Cluster2-RouterOps"
        )
    | project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName,
              AuthenticationRequirement, ResultType, ResultDescription,
              ConditionalAccessStatus, DetectionType, Cluster;

// -------------------------------------------------------------------
// SECTION 7: Office 365 / Exchange activity from APT28 IPs
// Covers IMAP/autodiscover abuse and OAuth token replay
// -------------------------------------------------------------------

let O365APT28Activity =
    OfficeActivity
    | where TimeGenerated > ago(30d)
    | where ClientIP in (APT28_All_IPs)
    | extend
        DetectionType = "O365Activity-From-APT28-IP",
        Cluster = iff(
            ClientIP in (APT28_Cluster1_IPs),
            "Cluster1-DNS-AitM",
            "Cluster2-RouterOps"
        )
    | project TimeGenerated, UserId, ClientIP, Operation, OfficeWorkload,
              DetectionType, Cluster;

// -------------------------------------------------------------------
// SECTION 8: TP-Link router model strings in device inventory
// Identifies vulnerable hardware that may have been targeted
// -------------------------------------------------------------------

let VulnerableTPLinkDevices =
    DeviceInfo
    | where TimeGenerated > ago(7d)
    | where DeviceType =~ "NetworkDevice" or OSPlatform =~ "Router"
    | where DeviceName has_any (
        "WR841N","WR841ND","WR840N","WR841HP","WR842N","WR842ND","WR845N",
        "WR941ND","WR945N","WR740N","WR741ND","WR749N","WR1043ND","WR1045ND",
        "WDR3500","WDR3600","WDR4300","WA801ND","WA901ND",
        "MR6400","MR3420","ARCHER C5","ARCHER C7"
    )
    | extend DetectionType = "InventoryHit-VulnerableTPLink-Model"
    | project TimeGenerated, DeviceName, DeviceType, OSPlatform, DetectionType;

// -------------------------------------------------------------------
// UNION ALL DETECTIONS - Final unified output
// -------------------------------------------------------------------

union isfuzzy=true
    NetworkHits,
    PoisonedOutlookDns,
    SuspiciousSSHPorts,
    MaliciousDnsServerTraffic,
    StolenCredSignIns,
    O365APT28Activity,
    VulnerableTPLinkDevices
| extend Severity = case(
    DetectionType has "EntraSignIn" or DetectionType has "O365Activity", "High",
    DetectionType has "DNS-Hijack" or DetectionType has "DirectDNS", "High",
    DetectionType has "NetworkConnection", "Medium",
    DetectionType has "BannerPort", "Medium",
    DetectionType has "Inventory", "Informational",
    "Medium"
)
| extend ThreatActor = "APT28 / Fancy Bear / Forest Blizzard (GRU Unit 26165)"
| extend AdvisoryReference = "NCSC Advisory - April 2026 - CVE-2023-50224"
| extend MITREReference = case(
    DetectionType has "DNS", "T1557 AitM / T1583.002 DNS Server / T1584.008 Network Devices",
    DetectionType has "SignIn" or DetectionType has "O365", "T1557 AitM / T1586 Compromise Accounts",
    DetectionType has "Network" or DetectionType has "SSH", "T1583.003 VPS / T1190 Exploit Public-Facing",
    DetectionType has "Inventory", "T1584.008 Compromise Infrastructure: Network Devices",
    "See NCSC Advisory"
)
| sort by Severity asc, TimeGenerated desc