Incident responseRansomwareWindows October 2026

Every 17 minutes: finding the foothold a ransomware investigation missed

In May 2020, about two months after I moved from desktop engineering onto the security team, REvil (also known as Sodinokibi) ransomware hit a food manufacturing company whose infrastructure my team managed. About three dozen of its servers, on premises and in the cloud, were encrypted. The parent organization behind it had more than 8,000 hosts and around 22,000 users.

An outside incident response firm confirmed it was ransomware but could not find how the attackers got in. This is how the rest of it was found, and the detections that would have caught it days earlier.

The night it started

CrowdStrike Falcon alerted another analyst and me in the middle of the night. By the time we were looking at it, the encryption was under way on the subsidiary's servers.

The immediate questions were the usual ones: what is encrypted, is it still spreading, and could it reach the rest of the environment. The subsidiary was small. What it was connected to was not.

What the first investigation found

The outside firm confirmed it was ransomware. What it could not establish was the point of entry, and without that there was no way to be sure the attacker was gone. Rebuilding servers while the attacker still has a way back in only resets the clock.

So I kept looking, using what we had: Elastic for log search and CrowdStrike for endpoint data.

Every 17 minutes

Two Windows servers were making outbound connections at a regular interval. Not roughly regular: exactly every 17 minutes.

Software that checks in on a timer is normal, so the interval alone proved nothing. What it gave me was two hosts to look at closely. On both, I found a scheduled task that made the connection, and the destination was a command-and-control server that threat intelligence tied to REvil activity.

REvil ran as ransomware-as-a-service: the core group supplied the ransomware, and affiliates broke into victims and deployed it. A foothold like this is typically the intruders' own tooling, there to keep access before and after the encryption, rather than part of the ransomware itself.

The scheduled tasks were the attacker's way back in. They would have survived any cleanup that missed them, and they had not shown up in the first investigation.

How they got in

With the persistence found, the rest came together. The attacker had signed in over RDP with a domain admin account. That account had been missed in regular account audits, and its password had not been changed in several years.

The logins from that account and the creation time of the scheduled tasks put the attacker inside for six days before the encryption started.

Six days is a long time. It was also six days in which every step left a log entry: an RDP logon by a domain admin, a scheduled task created on a server, and outbound connections on a fixed beat. None of them alerted.

Recovery

The affected servers were wiped and restored from backups. Finding the scheduled tasks and the account mattered here: restored servers are only clean if the way back in is closed too.

What changed afterward

Before the incident, I had led the removal of local administrator rights across the parent organization, but I had not been allowed to extend that work to the subsidiaries. The incident prompted a review of why, and I was cleared to start in the subsidiary environments immediately.

What would have caught it earlier

Looking back, the cheapest early warning was the scheduled task. It is easy to alert on, and on a server, a new task from an interactive session should be rare enough to review every time. Each of the queries below would have flagged something during those six days.

A scheduled task created

Windows logs event 4698 when a scheduled task is created. It needs "Audit Other Object Access Events" enabled in Advanced Audit Policy.

Splunk, with the Splunk Add-on for Microsoft Windows:

index=<windows_index> EventCode=4698
| rex field=Message "(?s)<Command>(?<task_command>[^<]+)</Command>"
| rex field=Message "(?s)<Arguments>(?<task_args>[^<]*)</Arguments>"
| table _time, host, user, Task_Name, task_command, task_args

Microsoft Sentinel:

SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4698
| extend TaskName = extract(@"<Data Name=""TaskName"">([^<]+)<", 1, EventData),
         Command = extract(@"&lt;Command&gt;([^&]+)&lt;/Command&gt;", 1, EventData)
| project TimeGenerated, Computer, SubjectUserName, TaskName, Command

Tuning. Software updaters and management agents create tasks constantly. Allowlist them by task path and command, not by name alone. Raise the severity for tasks on servers, tasks created by a person rather than SYSTEM, and commands that run PowerShell, a script interpreter or anything from a user-writable folder.

Connections at a fixed interval

Beaconing shows up as low variation in the time between connections from one host to one destination. This needs connection-level logs from a firewall or proxy.

index=<network_index> earliest=-24h
| sort 0 src, dest, _time
| streamstats current=f last(_time) as prev_time by src, dest
| eval interval = _time - prev_time
| stats count avg(interval) as avg_interval stdev(interval) as jitter by src, dest
| where count >= 20 AND avg_interval > 60 AND jitter < 30
| eval avg_minutes = round(avg_interval / 60, 1)
| sort jitter

Tuning. Monitoring agents, update checks and time sync all beacon too. Allowlist known destinations and look at what is left. Many command-and-control frameworks add random jitter, so a low threshold catches the careless ones and a looser one catches more at the cost of noise.

Privileged accounts nobody is watching

The account that let them in was a domain admin with a password several years old. A scheduled report on privileged accounts with stale passwords would have put it in front of someone.

$cutoff = (Get-Date).AddDays(-365)
"Domain Admins", "Enterprise Admins", "Administrators" | ForEach-Object {
    Get-ADGroupMember -Identity $_ -Recursive | Where-Object objectClass -eq "user"
} | Sort-Object SamAccountName -Unique | ForEach-Object {
    Get-ADUser $_ -Properties PasswordLastSet, LastLogonDate, Enabled
} | Where-Object { $_.Enabled -and $_.PasswordLastSet -lt $cutoff } |
  Select-Object SamAccountName, PasswordLastSet, LastLogonDate

Pair it with an alert on RDP logons (event 4624, logon type 10) by privileged accounts, so that an old account suddenly in use gets noticed.

What I took from it

For the step REvil takes right before encrypting, see Catching shadow copy deletion before the encryption starts.

← All writing