top of page

Able2Shell: Patch N-able N-central to HF2, Then Hunt

Writer: Inception Security
Inception Security
2 hours ago
8 min read
Four-panel hunt comic: a pixel threat reaches one MSP console wired to many client offices, the defender patches N-central to HF2 while it stays offline, Agents are upgraded first then Probes, then the defender hunts the appliance and the agents.

What this means for your clients: A line you can pass along to them: if your IT provider uses N-central to manage your computers, ask them today whether that server is patched, because whoever controls the console can push software to every machine it manages.


On November 9, a researcher published Able2Shell, a write-up of 33 alleged vulnerabilities in N-able N-central with proof of concept code. Tenable traces it to a GitHub account named Oracle-Security. N-able's incident page reports scanning tied to that code. On November 10 it shipped N-central 2026.4 HF2, build 2026.4.1.26, and published seven CVEs. As of this morning no customer compromise has been confirmed, no threat actor has been named, and none of the seven CVEs were on CISA's Known Exploited Vulnerabilities (KEV) catalog when we checked at 9:55 AM MST on November 11.


Why an RMM flaw is a fleet problem


N-central is the remote monitoring and management (RMM) console an MSP uses to run client endpoints. It installs software, runs scripts and opens remote sessions everywhere it manages, which makes the server tier 0. Whoever drives it can use tools the MSP already trusts on every client.


HF2 fixes seven CVEs, scored by N-able:


  • CVE-2026-125883, multiple XXE vulnerabilities, CVSS 4.0 9.3 Critical

  • CVE-2026-125881, schema-hint SSRF in service import validation, 9.3 Critical

  • CVE-2026-125878, multiple OGNL injection vulnerabilities, 8.7 High

  • CVE-2026-125879, local privilege escalation to root through a writable syslog-ng config, 8.6 High

  • CVE-2026-125882, remote code execution through file repository upload path traversal, 8.6 High

  • CVE-2026-91000, insecure IPC channels leading to local privilege escalation, 8.4 High

  • CVE-2026-125880, stored XSS through patch metadata in an admin context, 8.3 High


Rapid7, Huntress and Tenable describe the disclosure as covering unauthenticated code execution, privilege escalation to root and unauthorized account creation. N-able has not mapped its seven CVEs to the 33 findings, and it promises a follow-up hotfix with no date. HF2 is the current emergency build, not the end of the story.


What this means for a company


For an MSP, the risk runs downstream to every client on that server. For a company running its own, it is a path onto the whole fleet. Tenable reports attempts against honeypots and decoys, not customer servers. Huntress notes that in an earlier, separate N-central incident, operators pushed scripts into downstream environments, which tells you what to look for, not that it is happening now. Older, separate N-central bugs are on KEV, and that says nothing about whether these seven are being exploited.


Upgrade in the right order


If your on-premises server is not on 2026.4.1.26, N-able says to keep it offline until the upgrade succeeds and to follow its upgrade guidance before restoring access. Supported paths start at 2025.4. N-able says every hosted server in every region is upgraded.


Everyone still upgrades Agents and Probes, Agents first, because an upgraded Probe cannot talk to older Agents or an older Patch Management Engine (PME). PME updates itself, but confirm it is current. Built-in remote control is disabled in HF2, and Take Control still works.


Our own advice is to remove internet exposure the console does not need.


Before you hunt


We wrote these from vendor documentation and haven't run them in a live SIEM, so test each one first.


Start in the console, because N-central's audit, session and job logs reach your SIEM only if you forward them. Following Huntress, review everything since November 8: new or changed admins, unusual sign-ins, new scripts, automation policies, scheduled jobs and remote sessions.


Hunts 2 and 4 apply only on-premises, since hosted shops do not control the firewall or EDR on N-able's servers. Every block runs from 2026-11-08T07:00:00Z, midnight November 8 Phoenix time (MST), to now, and YOUR_ names are placeholders.


Coverage test: can you see what the agent does


Every managed endpoint should appear here. A device in your N-central inventory but missing from this list is a blind spot or an agent idle since November 8, so check the console.


Field mapping: All four match the N-able Technologies and MspPlatform install folders in any letter case. Falcon has no parent path on process events, so it counts only the agent's own processes.


Splunk


index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1 earliest=1794121200 latest=now
| eval img=lower(Image), pimg=lower(ParentImage)
| where match(img, "n-able technologies|mspplatform") OR match(pimg, "n-able technologies|mspplatform")
| stats count AS events min(_time) AS first_seen max(_time) AS last_seen BY host

Elastic


FROM logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-11-08T07:00:00Z")
| EVAL img = TO_LOWER(COALESCE(process.executable, "")), pimg = TO_LOWER(COALESCE(process.parent.executable, ""))
| WHERE img LIKE "*n-able technologies*" OR img LIKE "*mspplatform*" OR pimg LIKE "*n-able technologies*" OR pimg LIKE "*mspplatform*"
| STATS events = COUNT(*), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY host.name

CrowdStrike


#event_simpleName=ProcessRollup2
| test(@timestamp >= 1794121200000)
| ImageFileName=/\\(N-able Technologies|MspPlatform)\\/i
| groupBy([aid], function=[count(as=events), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])

Microsoft KQL


DeviceProcessEvents
| where Timestamp >= datetime(2026-11-08T07:00:00Z)
| where FolderPath contains @"\N-able Technologies\" or FolderPath contains @"\MspPlatform\" or InitiatingProcessFolderPath contains @"\N-able Technologies\" or InitiatingProcessFolderPath contains @"\MspPlatform\"
| summarize Events = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName

Hunt 1: Agents and Probes still on old builds


This lists the version or hash of every N-able binary that started since November 8. A service running nonstop on an old build never shows up, so the console or your scanner is the source of truth for versions, and this catches stragglers.


Field mapping: Splunk and Elastic read the binary's file version and KQL reads its product version, either of which may not match the console's format. Falcon process events carry no version, so CrowdStrike stacks SHA256 hashes, and a hash on few hosts is the one to check.


Splunk


index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1 earliest=1794121200 latest=now
| eval img=lower(Image)
| where match(img, "n-able technologies|mspplatform")
| stats values(FileVersion) AS file_versions values(Hashes) AS hashes max(_time) AS last_seen BY host Image
| sort host

Elastic


FROM logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-11-08T07:00:00Z")
| EVAL img = TO_LOWER(COALESCE(process.executable, ""))
| WHERE img LIKE "*n-able technologies*" OR img LIKE "*mspplatform*"
| STATS file_versions = VALUES(process.pe.file_version), hashes = VALUES(process.hash.sha256), last_seen = MAX(@timestamp) BY host.name, process.executable
| SORT host.name

CrowdStrike


#event_simpleName=ProcessRollup2
| test(@timestamp >= 1794121200000)
| ImageFileName=/\\(N-able Technologies|MspPlatform)\\/i
| groupBy([FileName, SHA256HashData], function=[count(aid, distinct=true, as=hosts), max(@timestamp, as=last_seen)])
| sort(hosts, order=asc)

Microsoft KQL


DeviceProcessEvents
| where Timestamp >= datetime(2026-11-08T07:00:00Z)
| where FolderPath contains @"\N-able Technologies\" or FolderPath contains @"\MspPlatform\"
| summarize Versions = make_set(ProcessVersionInfoProductVersion), Hashes = make_set(SHA256), LastSeen = max(Timestamp) by DeviceName, FileName
| order by DeviceName asc

Hunt 2: who reaches your on-prem server from the internet


This counts public addresses reaching the server on 80 or 443. Client-site agents check in over the internet, so separate known client addresses from everything else.


Field mapping: Splunk, Elastic and KQL read firewall logs (CommonSecurityLog in Sentinel), and CrowdStrike reads inbound accepts from Falcon on the appliance. Swap source and destination in the firewall queries to check the server's outbound traffic. In Falcon, use NetworkConnectIP4 with RemoteAddressIP4 and RemotePort instead.


Splunk


index=YOUR_FIREWALL_INDEX dest_ip="YOUR_NCENTRAL_IP" dest_port IN (80, 443) earliest=1794121200 latest=now
| where NOT (cidrmatch("10.0.0.0/8", src_ip) OR cidrmatch("172.16.0.0/12", src_ip) OR cidrmatch("192.168.0.0/16", src_ip))
| stats count AS connections values(action) AS actions min(_time) AS first_seen max(_time) AS last_seen BY src_ip
| sort - connections

Elastic


FROM YOUR_FIREWALL_INDEX
| WHERE @timestamp >= TO_DATETIME("2026-11-08T07:00:00Z") AND destination.ip == TO_IP("YOUR_NCENTRAL_IP") AND destination.port IN (80, 443)
| WHERE NOT CIDR_MATCH(source.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
| STATS connections = COUNT(*), actions = VALUES(event.action), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY source.ip
| SORT connections DESC

CrowdStrike


#event_simpleName=NetworkReceiveAcceptIP4
| aid=YOUR_NCENTRAL_AID
| test(@timestamp >= 1794121200000)
| in(LocalPort, values=["80", "443"])
| !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"])
| groupBy([RemoteAddressIP4], function=[count(as=connections), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])
| sort(connections, order=desc)

Microsoft KQL


CommonSecurityLog
| where TimeGenerated >= datetime(2026-11-08T07:00:00Z)
| where DestinationIP == "YOUR_NCENTRAL_IP" and DestinationPort in (80, 443)
| where not(ipv4_is_private(SourceIP))
| summarize Connections = count(), Actions = make_set(DeviceAction), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SourceIP
| order by Connections desc

Hunt 3: what the agent launched on your endpoints


This stacks shells, script hosts and account commands started by N-able agent processes, rarest first, and flags commands that add users or local administrators. A command on few machines that nobody wrote is the lead.


Field mapping: Splunk, Elastic and KQL read the parent path directly. Falcon records only the parent's name, so CrowdStrike matches children to an untimed table of N-able processes, because the agent service often started before November 8. Set the time picker to 30 days or more so that table reaches back.


Splunk


index=YOUR_SYSMON_INDEX sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1 earliest=1794121200 latest=now
| eval pimg=lower(ParentImage), img=lower(Image), cmd=lower(CommandLine)
| where match(pimg, "n-able technologies|mspplatform") AND match(img, "\\\\(powershell|pwsh|cmd|wscript|cscript|mshta|rundll32|regsvr32|certutil|bitsadmin|curl|net|net1)\.exe$")
| eval account_change=if(match(cmd, "user\s+\S+.*/add") OR match(cmd, "localgroup\s+administrators.*/add"), "yes", "no")
| stats dc(host) AS hosts count AS runs min(_time) AS first_seen max(_time) AS last_seen values(host) AS sample_hosts BY Image CommandLine account_change
| sort hosts

Elastic


FROM logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-11-08T07:00:00Z")
| EVAL pimg = TO_LOWER(COALESCE(process.parent.executable, "")), pname = TO_LOWER(COALESCE(process.name, "")), cmd = TO_LOWER(COALESCE(process.command_line, ""))
| WHERE (pimg LIKE "*n-able technologies*" OR pimg LIKE "*mspplatform*") AND pname IN ("powershell.exe", "pwsh.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe", "certutil.exe", "bitsadmin.exe", "curl.exe", "net.exe", "net1.exe")
| EVAL account_change = CASE(cmd RLIKE ".*user +[^ ]+.*/add.*" OR cmd RLIKE ".*localgroup +administrators.*/add.*", "yes", "no")
| STATS hosts = COUNT_DISTINCT(host.name), runs = COUNT(*), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp) BY pname, cmd, account_change
| SORT hosts ASC

CrowdStrike


defineTable(name="nable_procs", query={#event_simpleName=ProcessRollup2 | ImageFileName=/\\(N-able Technologies|MspPlatform)\\/i | rename(field=TargetProcessId, as=ParentProcessId)}, include=[aid, ParentProcessId])
| #event_simpleName=ProcessRollup2
| test(@timestamp >= 1794121200000)
| FileName=/^(powershell|pwsh|cmd|wscript|cscript|mshta|rundll32|regsvr32|certutil|bitsadmin|curl|net|net1)\.exe$/i
| match(table="nable_procs", field=[aid, ParentProcessId])
| case {
    CommandLine=/(user\s+\S+.*\/add|localgroup\s+administrators.*\/add)/i | account_change := "yes" ;
    * | account_change := "no" ;
  }
| groupBy([FileName, CommandLine, account_change], function=[count(aid, distinct=true, as=hosts), count(as=runs), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])
| sort(hosts, order=asc)

Microsoft KQL


DeviceProcessEvents
| where Timestamp >= datetime(2026-11-08T07:00:00Z)
| where InitiatingProcessFolderPath contains @"\N-able Technologies\" or InitiatingProcessFolderPath contains @"\MspPlatform\"
| where FileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe", "certutil.exe", "bitsadmin.exe", "curl.exe", "net.exe", "net1.exe")
| extend AccountChange = iff(ProcessCommandLine matches regex @"(?i)(user\s+\S+.*/add|localgroup\s+administrators.*/add)", "yes", "no")
| summarize Hosts = dcount(DeviceName), Runs = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp), SampleHosts = make_set(DeviceName, 10) by FileName, ProcessCommandLine, AccountChange
| order by Hosts asc

Hunt 4: shells and download tools under a java parent on the appliance


If your on-premises appliance forwards process events, look for shells or download tools whose parent is a java process. Many shops can't put EDR on the appliance, so an empty result may mean no data, not a clean server.


Field mapping: Splunk assumes Sysmon for Linux, and the others need their sensor on the appliance. Without one, lean on Hunt 2.


Splunk


index=YOUR_LINUX_INDEX host=YOUR_NCENTRAL_HOST EventCode=1 earliest=1794121200 latest=now
| eval pimg=lower(ParentImage), img=lower(Image)
| where match(pimg, "/java$") AND match(img, "/(sh|bash|dash|curl|wget|python[0-9.]*|perl|nc|ncat|socat|busybox)$")
| stats count AS runs min(_time) AS first_seen max(_time) AS last_seen values(CommandLine) AS commands BY Image User

Elastic


FROM logs-endpoint.events.process-*
| WHERE @timestamp >= TO_DATETIME("2026-11-08T07:00:00Z") AND host.name == "YOUR_NCENTRAL_HOST"
| EVAL ppname = TO_LOWER(COALESCE(process.parent.name, "")), pname = TO_LOWER(COALESCE(process.name, ""))
| WHERE ppname == "java" AND (pname IN ("sh", "bash", "dash", "curl", "wget", "perl", "nc", "ncat", "socat", "busybox") OR pname RLIKE "python[0-9.]*")
| STATS runs = COUNT(*), first_seen = MIN(@timestamp), last_seen = MAX(@timestamp), commands = VALUES(process.command_line) BY pname, user.name

CrowdStrike


#event_simpleName=ProcessRollup2
| aid=YOUR_NCENTRAL_AID
| test(@timestamp >= 1794121200000)
| ParentBaseFileName=/^java$/i
| FileName=/^(sh|bash|dash|curl|wget|python[0-9.]*|perl|nc|ncat|socat|busybox)$/i
| groupBy([FileName, UserName], function=[count(as=runs), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen), collect([CommandLine])])

Microsoft KQL


DeviceProcessEvents
| where Timestamp >= datetime(2026-11-08T07:00:00Z)
| where DeviceName =~ "YOUR_NCENTRAL_HOST"
| where InitiatingProcessFileName =~ "java"
| where FileName in~ ("sh", "bash", "dash", "curl", "wget", "perl", "nc", "ncat", "socat", "busybox") or FileName matches regex @"^(?i)python[0-9.]*$"
| summarize Runs = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp), Commands = make_set(ProcessCommandLine, 50) by FileName, AccountName

What the results mean and when to escalate


A device that never shows agent activity is unproven, not clean. An old Agent or Probe build is a patch task unless it sits next to a Hunt 3 finding. An unknown public address hitting the server since November 9 fits the scanning N-able describes, which is not proof of compromise.


Escalate any agent-launched command that adds a user or administrator, downloads a tool, or runs on a few machines when nobody wrote it. A shell under a java process on the appliance warrants investigation, not an automatic compromise finding. If the activity is unauthorized or other evidence points to compromise, escalate to incident response: isolate the server, preserve its logs, assess which credentials need rotation, and investigate the clients it reaches.


Protect the console and every client behind it


Inception Protection watches the endpoint and Microsoft telemetry from the machines an RMM manages. If your clients run on Microsoft 365, start with a free Inception Foresight M365 Assessment.


bg-map-white.png

INCEPTION SECURITY™

A cybersecurity company with in depth knowledge of the threat landscape and security controls.

NAVIGATION

GET IN TOUCH

© 2025 All Rights Reserved by INCEPTION SECURITY™ .

bottom of page