2026-08-16 T1219 detection ~15 min

One RMM Is IT. Two Is An Incident.

Johnny Meintel · 2026-08-16 · detection engineering

Attackers have largely stopped bringing their own malware. Instead they install the same remote-access software the IT department already uses, because it is real software, signed by the vendor, and it looks exactly like someone doing support. There is nothing to scan for.

Which is a problem, because one of these tools running on a machine is completely normal. Every managed service provider on earth has one. So the useful question is not whether a remote tool is there. It is whether a second one showed up, and whether the first one put it there.

This is how I built that detection in a home lab, what it catches, and where it falls apart.

Confirm AnyDesk and TeamViewer binary path and running processes

[WS01] C:\WINDOWS\system32 > Get-Process *anydesk*

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
    444      26    45000      62496       3.61   7188   1 AnyDesk
    477      30    44068      69432       1.13   9604   0 AnyDesk


[WS01] C:\WINDOWS\system32 > Get-Process *team*

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
    487      91    19380      26176       9.75   3628   0 TeamViewer_Service


[WS01] C:\WINDOWS\system32 > (Get-ChildItem 'C:\Program Files','C:\Program Files (x86)' -Recurse -Filter TeamViewer.exe -ErrorAction SilentlyContinue | Select-Object -First 1).FullName

C:\Program Files\TeamViewer\TeamViewer.exe

[WS01] C:\WINDOWS\system32 > (Get-ChildItem 'C:\Program Files','C:\Program Files (x86)' -Recurse -Filter AnyDesk.exe -ErrorAction SilentlyContinue | Select-Object -First 1).FullName

C:\Program Files (x86)\AnyDesk\AnyDesk.exe

Confirm WS01 is active in Wazuh and Velociraptor dashboards

WS01 reporting to both platforms.
Fig 1. WS01 reporting to both platforms.

Wazuh dashboard columns

rule.level
rule.description
agent.name
data.win.eventdata.company
data.win.eventdata.parentImage
data.win.eventdata.image
data.win.eventdata.integrityLevel
data.win.eventdata.processGuid

Wazuh detection ruleset

<!--
rmm_detection.xml - RMM abuse detection - 100210 identify | 100211 multiplicity (correlational) | 100212-214 causal (RMM-A spawns RMM-B)
-->
<group name="sysmon,rmm,">

  <rule id="100210" level="3">
    <if_group>sysmon_event1</if_group>
    <field name="win.eventdata.company">AnyDesk Software GmbH|TeamViewer|Atera Networks|Splashtop|MeshCentral|ConnectWise|Level|ScreenConnect|RealVNC|UltraVNC|Ninja|Action1|Syncro|Pulseway|Tiflux</field>
    <description>RMM process launched: $(win.eventdata.company)</description>
    <mitre><id>T1219</id></mitre>
    <group>rmm_detected,</group>
  </rule>

  <rule id="100211" level="12" frequency="2" timeframe="600">
    <if_matched_sid>100210</if_matched_sid>
    <same_field>win.system.computer</same_field>
    <different_field>win.eventdata.company</different_field>
    <description>RMM MULTIPLICITY: 2+ distinct vendors on $(win.system.computer), latest: $(win.eventdata.company) ($(win.eventdata.image))</description>
    <mitre><id>T1219</id></mitre>
    <group>rmm_detected,rmm_multiplicity,</group>
  </rule>

  <!-- 100212: a different-vendor RMM process spawned TeamViewer (causal, cross-vendor). -->
  <rule id="100212" level="13">
    <if_sid>100210</if_sid>
    <field name="win.eventdata.company">TeamViewer</field>
    <field name="win.eventdata.parentImage" type="pcre2">(?i)(anydesk|screenconnect|connectwise|atera|splashtop|meshagent)</field>
    <description>Cross-vendor RMM spawn: TeamViewer launched by $(win.eventdata.parentImage)</description>
    <mitre><id>T1219</id></mitre>
    <group>rmm_detected,rmm_secondary_deploy,</group>
  </rule>

  <!-- 100213: a different-vendor RMM process spawned AnyDesk (causal, cross-vendor). -->
  <rule id="100213" level="13">
    <if_sid>100210</if_sid>
    <field name="win.eventdata.company">AnyDesk</field>
    <field name="win.eventdata.parentImage" type="pcre2">(?i)(teamviewer|screenconnect|connectwise|atera|splashtop|meshagent)</field>
    <description>Cross-vendor RMM spawn: AnyDesk launched by $(win.eventdata.parentImage)</description>
    <mitre><id>T1219</id></mitre>
    <group>rmm_detected,rmm_secondary_deploy,</group>
  </rule>

  <!-- 100214: escalate when the spawned RMM runs as SYSTEM. -->
  <rule id="100214" level="14">
    <if_sid>100212,100213</if_sid>
    <field name="win.eventdata.integrityLevel">System</field>
    <description>HIGH FIDELITY secondary RMM deploy under SYSTEM: $(win.eventdata.company) spawned by $(win.eventdata.parentImage)</description>
    <mitre><id>T1219</id></mitre>
    <group>rmm_detected,rmm_secondary_deploy,</group>
  </rule>

</group>

1Questions to answer

1. Rule 100210 (identify, level 3)

You copy AnyDesk.exe to totally_legit.exe and launch the copy. Does the rule fire?

2. Rule 100211 (multiplicity, level 12)

You launch TeamViewer once, and nothing else. It starts three processes: TeamViewer.exe, tv_w32.exe, tv_x64.exe. Does the rule fire?

3. Rule 100211 again

You launch AnyDesk, then TeamViewer eleven minutes later. Does it fire?

4. Rule 100212 (causal, level 13)

A genuine AnyDesk binary, renamed to svc-helper.exe, spawns TeamViewer. Does it fire?

5. Rule 100214 (SYSTEM, level 14)

You run the causal test interactively from your own shell, instead of as a scheduled task set to run as SYSTEM. Does it fire?

6. Rule precedence

You stage the decoy in C:\Windows\Temp instead of C:\Lab and launch it with Start-Process. Built-in Wazuh rule 92066 fires at level 4 on executables in that folder launched by PowerShell. Rule 100210 is level 3. What appears in the alert stream?

7. Operation BlueDash

A single PowerShell command downloads and launches Level and ConnectWise ScreenConnect in parallel. Which of these rules fire, and which stay silent?

2Metadata

# AnyDesk

# Pull full version information from the PE header of AnyDesk.exe
[WS01] C:\WINDOWS\system32 > (Get-Item 'C:\Program Files (x86)\AnyDesk\AnyDesk.exe').VersionInfo | Format-List *

FileVersionRaw : 9.7.8.0
ProductVersionRaw : 0.0.0.0
Comments :
CompanyName : AnyDesk Software GmbH
FileBuildPart : 8
FileDescription : AnyDesk
FileMajorPart : 9
FileMinorPart : 7
FileName : C:\Program Files (x86)\AnyDesk\AnyDesk.exe
FilePrivatePart : 0
FileVersion : 9.7.8
InternalName :
IsDebug : False
IsPatched : False
IsPrivateBuild : False
IsPreRelease : False
IsSpecialBuild : False
Language : English (United States)
LegalCopyright : (C) 2026 AnyDesk Software GmbH
LegalTrademarks :
OriginalFilename :
PrivateBuild :
ProductBuildPart : 0
ProductMajorPart : 0
ProductMinorPart : 0
ProductName : AnyDesk
ProductPrivatePart : 0
ProductVersion : 9.7
SpecialBuild :

# Check whether the binary is digitally signed and if the signature is valid
[WS01] C:\WINDOWS\system32 > Get-AuthenticodeSignature 'C:\Program Files (x86)\AnyDesk\AnyDesk.exe'

    Directory: C:\Program Files (x86)\AnyDesk

SignerCertificate                         Status      Path
-----------------                         ------      ----
D056FB932C5A4821C328214E4F4988436650583A  Valid       AnyDesk.exe

# Extract just the Subject field from the signing certificate (shows the real company that signed it)
[WS01] C:\WINDOWS\system32 > (Get-AuthenticodeSignature 'C:\Program Files (x86)\AnyDesk\AnyDesk.exe').SignerCertificate.Subject
CN=AnyDesk Software GmbH, O=AnyDesk Software GmbH, L=Stuttgart, S=Baden-Württemberg, C=DE

#           /
#          / /
#         / / /
#        / / / /
#       / / / / /
#      / / / / / /
#     / / / / / / /
#    / / / / / / / /
#   / / / / / / / / / https://purplepyram1d.com

# TeamViewer

# Pull full version information from the PE header of TeamViewer.exe
[WS01] C:\WINDOWS\system32 > (Get-Item 'C:\Program Files\TeamViewer\TeamViewer.exe').VersionInfo | Format-List *

FileVersionRaw : 15.80.4.0
ProductVersionRaw : 15.80.0.0
Comments :
CompanyName : TeamViewer Germany GmbH
FileBuildPart : 4
FileDescription : TeamViewer
FileMajorPart : 15
FileMinorPart : 80
FileName : C:\Program Files\TeamViewer\TeamViewer.exe
FilePrivatePart : 0
FileVersion : 15.80.4.0
InternalName : TeamViewer
IsDebug : False
IsPatched : False
IsPrivateBuild : True
IsPreRelease : False
IsSpecialBuild : False
Language : English (United Kingdom)
LegalCopyright : TeamViewer Germany GmbH
LegalTrademarks : TeamViewer
OriginalFilename : TeamViewer.exe
PrivateBuild : TeamViewer Remote Control Application
ProductBuildPart : 0
ProductMajorPart : 15
ProductMinorPart : 80
ProductName : TeamViewer Full
ProductPrivatePart : 0
ProductVersion : 15.80.4.0
SpecialBuild :

# Check whether the binary is digitally signed and if the signature is valid
[WS01] C:\WINDOWS\system32 > Get-AuthenticodeSignature 'C:\Program Files\TeamViewer\TeamViewer.exe'

    Directory: C:\Program Files\TeamViewer

SignerCertificate                         Status      Path
-----------------                         ------      ----
DA9105E8E66F11E8030B05B20EE9E1164ADCF893  Valid       TeamViewer.exe

# Extract just the Subject field from the signing certificate (shows the real company that signed it)
[WS01] C:\WINDOWS\system32 > (Get-AuthenticodeSignature 'C:\Program Files\TeamViewer\TeamViewer.exe').SignerCertificate.Subject
CN=TeamViewer Germany GmbH, O=TeamViewer Germany GmbH, L=Göppingen, C=DE

3Fire Each Alert

Rule 100210 - RMM launch detected via win.eventdata.company

# Launch AnyDesk briefly so it emits one process-create event, then close it
Start-Process "C:\Program Files (x86)\AnyDesk\AnyDesk.exe" -ArgumentList "--control"; Start-Sleep 4; Stop-Process -Name AnyDesk -Force -ErrorAction SilentlyContinue

# Confirm the AnyDesk process-create is in the local Sysmon log
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1} -MaxEvents 40 | Where-Object { $_.Message -like '*Company: AnyDesk*' } | Select-Object -First 1 | Format-List Message

Message : Process Create:
          RuleName: -
          UtcTime: 2026-08-16 06:47:37.980
          ProcessGuid: {74a4dd39-5d09-6a81-5a07-000000002300}
          ProcessId: 9476
          Image: C:\Program Files (x86)\AnyDesk\AnyDesk.exe
          FileVersion: 9.7.8
          Description: AnyDesk
          Product: AnyDesk
          Company: AnyDesk Software GmbH
          OriginalFileName: -
          CommandLine: "C:\Program Files (x86)\AnyDesk\AnyDesk.exe" --control
          CurrentDirectory: C:\WINDOWS\system32\
          User: CORP\Administrator
          LogonGuid: {74a4dd39-81f0-6a80-bec7-6a0000000000}
          LogonId: 0x6AC7BE
          TerminalSessionId: 1
          IntegrityLevel: High
          Hashes: MD5=1B55EA91167B5743ACCE16969821CA8A,SHA256=E6B3182A15C35AB18D8F8E8FA79A8FA06958841
          4B826280DF9CB1B733ED971CB,IMPHASH=00000000000000000000000000000000
          ParentProcessGuid: {74a4dd39-8237-6a80-5402-000000002300}
          ParentProcessId: 9564
          ParentImage: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
          ParentCommandLine: "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
          ParentUser: CORP\Administrator
# On SIEM01 - confirm the Wazuh alert fired with the right level and vendor
sudo grep '"id":"100210"' /var/ossec/logs/alerts/alerts.json | grep WS01 | tail -1 | jq -rc '{level:.rule.level, company:.data.win.eventdata.company}'

{"level":3,"company":"AnyDesk Software GmbH"}
Rule 100210 at level 3. The vendor name comes from inside the binary, not the filename.
Fig 2. Rule 100210 at level 3. The vendor name comes from inside the binary, not the filename.

Rule 100210 negative test - rename-proof identity

# Staging directory for every decoy below
New-Item -ItemType Directory -Path 'C:\Lab' -Force | Out-Null

# Copy the real AnyDesk binary to a name that hides what it is
Copy-Item -Path 'C:\Program Files (x86)\AnyDesk\AnyDesk.exe' -Destination 'C:\Lab\totally_legit.exe' -Force

# Launch the renamed copy - the filename lies, the embedded vendor name does not
Start-Process 'C:\Lab\totally_legit.exe' -ArgumentList '--control'; Start-Sleep 4; Stop-Process -Name totally_legit -Force -ErrorAction SilentlyContinue

# Confirm the AnyDesk process-create is in the local Sysmon log
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1} -MaxEvents 40 | Where-Object { $_.Message -like '*Company: AnyDesk*' } | Select-Object -First 1 | Format-List Message

Message : Process Create:
          RuleName: -
          UtcTime: 2026-08-16 06:55:06.017
          ProcessGuid: {74a4dd39-5eca-6a81-5e07-000000002300}
          ProcessId: 124
          Image: C:\Lab\totally_legit.exe
          FileVersion: 9.7.8
          Description: AnyDesk
          Product: AnyDesk
          Company: AnyDesk Software GmbH
          OriginalFileName: -
          CommandLine: "C:\Lab\totally_legit.exe" --control
          CurrentDirectory: C:\WINDOWS\system32\
          User: CORP\Administrator
          LogonGuid: {74a4dd39-81f0-6a80-bec7-6a0000000000}
          LogonId: 0x6AC7BE
          TerminalSessionId: 1
          IntegrityLevel: High
          Hashes: MD5=1B55EA91167B5743ACCE16969821CA8A,SHA256=E6B3182A15C35AB18D8F8E8FA79A8FA06958841
          4B826280DF9CB1B733ED971CB,IMPHASH=00000000000000000000000000000000
          ParentProcessGuid: {74a4dd39-5ec8-6a81-5c07-000000002300}
          ParentProcessId: 5240
          ParentImage: C:\Lab\totally_legit.exe
          ParentCommandLine: "C:\Lab\totally_legit.exe"
          ParentUser: CORP\Administrator
# The alert still says AnyDesk even though the file on disk is called totally_legit.exe
sudo grep '"id":"100210"' /var/ossec/logs/alerts/alerts.json | grep WS01 | tail -1 | jq -rc '{company:.data.win.eventdata.company, image:.data.win.eventdata.image}'

{"company":"AnyDesk Software GmbH","image":"C:\\Lab\\totally_legit.exe"}
The same alert for a file called totally_legit.exe. Renaming changes the label, not the metadata.
Fig 3. The same alert for a file called totally_legit.exe. Renaming changes the label, not the metadata.

Rule 100211 - two distinct vendors on one host inside ten minutes

# Launch vendor one from its installed location
Start-Process "C:\Program Files (x86)\AnyDesk\AnyDesk.exe" -ArgumentList "--control"; Start-Sleep 5

# Launch vendor two from its installed location, well inside the 600 second window
Start-Process "C:\Program Files\TeamViewer\TeamViewer.exe"; Start-Sleep 5

# Close both
Stop-Process -Name AnyDesk,TeamViewer -Force -ErrorAction SilentlyContinue
# Confirm the correlation fired at level 12 - note it fires once per qualifying event, not once per incident
sudo grep '"id":"100211"' /var/ossec/logs/alerts/alerts.json | grep WS01 | tail -3 | jq -rc '{level:.rule.level, desc:.rule.description}'

{"level":12,"desc":"RMM MULTIPLICITY: 2+ distinct vendors on WS01.corp.local, latest: TeamViewer Germany GmbH (C:\\Program Files\\TeamViewer\\TeamViewer.exe)"}
{"level":12,"desc":"RMM MULTIPLICITY: 2+ distinct vendors on WS01.corp.local, latest: TeamViewer Germany GmbH (C:\\Program Files\\TeamViewer\\tv_w32.exe)"}
{"level":12,"desc":"RMM MULTIPLICITY: 2+ distinct vendors on WS01.corp.local, latest: TeamViewer Germany GmbH (C:\\Program Files\\TeamViewer\\tv_x64.exe)"}

Three alerts, one incident. TeamViewer launches as three processes (TeamViewer.exe, tv_w32.exe, tv_x64.exe) and each carries the vendor name, so each fires 100210 in turn. A composite rule re-evaluates on every qualifying event, so each arrival satisfies "2+ distinct vendors in 600 seconds" again and alerts again. It still cannot name the first vendor. A composite rule builds its alert from the event that completes the correlation, and the field substitutions only reach that event. Wazuh knows the two differed, that is what different_field evaluated, but the earlier value is not carried forward. The honest ceiling is "this host, this window, this vendor arrived, and a different one preceded it."

Rule 100211 at level 12. Two distinct vendors on one host inside ten minutes, fired once per process.
Fig 4. Rule 100211 at level 12. Two distinct vendors on one host inside ten minutes, fired once per process.

Rule 100211 negative test - one vendor, however many processes, must stay silent

# On SIEM01 FIRST - restarting the manager clears composite-rule state, so no earlier vendor is left in the window
sudo systemctl restart wazuh-manager && sleep 20 && sudo /var/ossec/bin/agent_control -l | grep WS01
# Make sure no other vendor is running, then launch ONE vendor only
Get-Process -Name AnyDesk -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Process "C:\Program Files\TeamViewer\TeamViewer.exe"; Start-Sleep 10; Stop-Process -Name TeamViewer -Force -ErrorAction SilentlyContinue
# TeamViewer launches three processes and each fires 100210
sudo grep '"id":"100210"' /var/ossec/logs/alerts/alerts.json | grep WS01 | tail -3 | jq -rc '{company:.data.win.eventdata.company, image:.data.win.eventdata.image}'

{"company":"TeamViewer Germany GmbH","image":"C:\\Program Files\\TeamViewer\\TeamViewer.exe"}
{"company":"TeamViewer Germany GmbH","image":"C:\\Program Files\\TeamViewer\\tv_w32.exe"}
{"company":"TeamViewer Germany GmbH","image":"C:\\Program Files\\TeamViewer\\tv_x64.exe"}

# TeamViewer's multiple processes do not trigger 100211
sudo grep '"id":"100211"' /var/ossec/logs/alerts/alerts.json | grep WS01 | jq -r --arg t "$(TZ=America/Chicago date -d '1 min ago' +%Y-%m-%dT%H:%M:%S)" 'select(.timestamp > $t) | .timestamp' | wc -l

0
One vendor, three processes, no multiplicity alert. The distinctness check is never satisfied.
Fig 5. One vendor, three processes, no multiplicity alert. The distinctness check is never satisfied.

Rule 100212 - one remote-access tool spawning a different one

# Staging directory with no spaces, and NOT C:\Windows\Temp - see the note below
New-Item -ItemType Directory -Path 'C:\Lab' -Force | Out-Null

# The fake parent: a copy of cmd.exe wearing an RMM vendor's filename
Copy-Item -Path 'C:\Windows\System32\cmd.exe' -Destination 'C:\Lab\AnyDesk.exe' -Force

# The real child: a genuine TeamViewer binary, so it carries real vendor metadata
Copy-Item -Path 'C:\Program Files\TeamViewer\TeamViewer.exe' -Destination 'C:\Lab\TeamViewer.exe' -Force

# Fake AnyDesk spawns TeamViewer - the parent-child pair the rule looks for
Start-Process -FilePath 'C:\Lab\AnyDesk.exe' -ArgumentList '/c','C:\Lab\TeamViewer.exe'

# Confirm the process-create landed locally, matching Image exactly so a crash-reporter event cannot be picked up instead
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1} -MaxEvents 60 | ForEach-Object { $x=[xml]$_.ToXml(); $img=($x.Event.EventData.Data | Where-Object {$_.Name -eq 'Image'}).'#text'; if ($img -eq 'C:\Lab\TeamViewer.exe') { $_ } } | Select-Object -First 1 | Format-List Message

Message : Process Create:
          UtcTime: 2026-08-16 07:35:41.328
          ProcessGuid: {74a4dd39-684d-6a81-df07-000000002300}
          ProcessId: 5756
          Image: C:\Lab\TeamViewer.exe
          Description: TeamViewer
          Product: TeamViewer Full
          Company: TeamViewer Germany GmbH
          OriginalFileName: TeamViewer.exe
          CommandLine: C:\Lab\TeamViewer.exe
          User: CORP\Administrator
          TerminalSessionId: 0
          IntegrityLevel: High
          ParentProcessGuid: {74a4dd39-684c-6a81-dd07-000000002300}
          ParentProcessId: 3172
          ParentImage: C:\Lab\AnyDesk.exe
          ParentCommandLine: "C:\Lab\AnyDesk.exe" /c C:\Lab\TeamViewer.exe
          ParentUser: CORP\Administrator
# Confirm the causal alert fired at level 13 with the parent named
sudo grep '"id":"100212"' /var/ossec/logs/alerts/alerts.json | grep WS01 | tail -1 | jq -rc '{level:.rule.level, company:.data.win.eventdata.company, parent:.data.win.eventdata.parentImage}'

{"level":13,"company":"TeamViewer Germany GmbH","parent":"C:\\Lab\\AnyDesk.exe"}

Match Image exactly, not the message text. Filtering with -like 'C:\Lab\TeamViewer.exe' returns WerFault.exe, the Windows crash reporter, because the path appears in its ParentImage when TeamViewer crashes. A loose substring grabs the wrong event and it looks like the right one.

Why C:\Lab and not C:\Windows\Temp. Built-in Wazuh rule 92066 fires at level 4 on any executable under Windows\Temp or Windows\SysWOW64 launched by powershell.exe. Rule 100210 is level 3. Wazuh reports one rule per event and keeps the higher level, so staging in Windows\Temp and launching from PowerShell hands the event to the built-in rule and the custom rule never appears.

This fires, but it is a false positive. Rule 100212 matches the parent by path, not identity, so a renamed cmd.exe satisfies it - which is exactly what the certificate check further down exposes. A true positive needs a real remote session launching the second tool, which needs an attacker host and a second workstation. That is the next article.

100213 is the mirror of this rule, catching AnyDesk spawned by a different vendor rather than TeamViewer. Swap the two binaries in the trigger above to fire it. There is one rule per direction because Sysmon has no parent-vendor field to compare against.

Rule 100212 at level 13. TeamViewer launched by a process named AnyDesk, with the parent named in the alert.
Fig 6. Rule 100212 at level 13. TeamViewer launched by a process named AnyDesk, with the parent named in the alert.

Rule 100214 - the spawned tool running as SYSTEM

# Run the same parent-child spawn as SYSTEM via a scheduled task - no external tooling needed
schtasks /create /tn "LabSystemSpawn" /tr "C:\Lab\AnyDesk.exe /c C:\Lab\TeamViewer.exe" /sc once /st 00:00 /ru SYSTEM /rl HIGHEST /f
schtasks /run /tn "LabSystemSpawn"

# Confirm the child ran at System integrity rather than High
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1} -MaxEvents 60 | ForEach-Object { $x=[xml]$_.ToXml(); $img=($x.Event.EventData.Data | Where-Object {$_.Name -eq 'Image'}).'#text'; if ($img -eq 'C:\Lab\TeamViewer.exe') { $_ } } | Select-Object -First 1 | Format-List Message

Message : Process Create:
          UtcTime: 2026-08-16 07:50:30.812
          ProcessGuid: {74a4dd39-6bc6-6a81-3c08-000000002300}
          ProcessId: 6516
          Image: C:\Lab\TeamViewer.exe
          Company: TeamViewer Germany GmbH
          OriginalFileName: TeamViewer.exe
          CommandLine: C:\Lab\TeamViewer.exe
          User: NT AUTHORITY\SYSTEM
          IntegrityLevel: System
          ParentProcessId: 5072
          ParentImage: C:\Lab\AnyDesk.exe
          ParentCommandLine: "C:\Lab\AnyDesk.exe" /c C:\Lab\TeamViewer.exe
          ParentUser: NT AUTHORITY\SYSTEM

# Remove the task
schtasks /delete /tn "LabSystemSpawn" /f
# Confirm the escalation fired at level 14
sudo grep '"id":"100214"' /var/ossec/logs/alerts/alerts.json | grep WS01 | tail -1 | jq -rc '{level:.rule.level, integrity:.data.win.eventdata.integrityLevel, company:.data.win.eventdata.company, parent:.data.win.eventdata.parentImage}'

{"level":14,"integrity":"System","company":"TeamViewer Germany GmbH","parent":"C:\\Lab\\AnyDesk.exe"}

The full alert document. Three independent conditions stack here: a known vendor in company, a different vendor in parentImage, and integrityLevel: System.

{
  "rule": {
    "id": "100214",
    "level": 14,
    "description": "HIGH FIDELITY secondary RMM deploy under SYSTEM: TeamViewer Germany GmbH spawned by C:\\Lab\\AnyDesk.exe",
    "groups": ["sysmon", "rmm", "rmm_detected", "rmm_secondary_deploy"],
    "mitre": { "id": "T1219", "technique": "Remote Access Software", "tactic": "Command and Control" },
    "firedtimes": 1
  },
  "agent":    { "name": "WS01" },
  "manager":  { "name": "siem01" },
  "location": "EventChannel",
  "data": { "win": {
    "system": {
      "eventID":       "1",
      "computer":      "WS01.corp.local",
      "systemTime":    "2026-08-16T07:50:30.8194471Z",
      "eventRecordID": "126612"
    },
    "eventdata": {
      "image":             "C:\\Lab\\TeamViewer.exe",
      "company":           "TeamViewer Germany GmbH",
      "originalFileName":  "TeamViewer.exe",
      "description":       "TeamViewer",
      "product":           "TeamViewer Full",
      "fileVersion":       "15.80.4.0",
      "commandLine":       "C:\\Lab\\TeamViewer.exe",
      "currentDirectory":  "C:\\Windows\\System32\\",
      "user":              "NT AUTHORITY\\SYSTEM",
      "integrityLevel":    "System",
      "terminalSessionId": "0",
      "processGuid":       "{74a4dd39-6bc6-6a81-3c08-000000002300}",
      "processId":         "6516",
      "parentImage":       "C:\\Lab\\AnyDesk.exe",
      "parentCommandLine": "\"C:\\Lab\\AnyDesk.exe\" /c C:\\Lab\\TeamViewer.exe",
      "parentProcessGuid": "{74a4dd39-6bc6-6a81-3a08-000000002300}",
      "parentProcessId":   "5072",
      "parentUser":        "NT AUTHORITY\\SYSTEM",
      "hashes":            "MD5=8D007DA9D21C4DD12792430570F52BE0,SHA256=F27A87FC7986AC2843AB1C46955A5367DEB08AFCAF5AF1F1DB2C65D22F6A716D,IMPHASH=67C3F44AA796B6DCBC2178735CE06403"
    }
  }}
}

A scheduled task rather than PsExec. Both run a process as SYSTEM, but schtasks is built into Windows and needs nothing downloaded, and a SYSTEM scheduled task is itself a documented persistence technique - the Gentlemen crew used exactly this.

Interactive launches produce High integrity, not System. A process started from your own shell inherits your integrity level, so 100214 stays silent no matter how many times you run the causal test that way. Only the SYSTEM run satisfies it. Same parent, same child, same command line - the integrity level is the only thing that changed, and it is the difference between level 13 and level 14.

Rule 100214 at level 14. Same parent and child as above, running as SYSTEM instead of as a user.
Fig 7. Rule 100214 at level 14. Same parent and child as above, running as SYSTEM instead of as a user.

4Velociraptor - certificate-verified lineage

Confirm the process tracker is recording before firing anything

-- Client -> WS01 -> Shell tab -> switch the dropdown from Powershell to VQL
-- Rows back means the tracker is running. Empty means it is not, and every lineage query below returns nothing with no error.
SELECT * FROM process_tracker_pslist()

Resolve the tracker identifier for the spawned child

-- The tracker identifier is the composite PID-starttime string in the Pid field, NOT the Sysmon ProcessGuid
SELECT Pid, Name, Exe FROM process_tracker_pslist() WHERE Exe =~ "Lab.*TeamViewer"
[
  { "Pid": "5756-1786865741", "Name": "TeamViewer.exe", "Exe": "C:\\Lab\\TeamViewer.exe" },
  { "Pid": "6992-1786865905", "Name": "TeamViewer.exe", "Exe": "C:\\Lab\\TeamViewer.exe" },
  { "Pid": "6516-1786866630", "Name": "TeamViewer.exe", "Exe": "C:\\Lab\\TeamViewer.exe" }
]

Three rows because the tracker retains exited processes, which is what makes it a tracker rather than a process list. Take the newest, here 6516-1786866630, matching processId: 6516 in the 100214 alert above.

Walk the ancestry and verify every hop against its signing certificate

-- callchain is a FUNCTION, so it has to be wrapped in foreach rather than used bare in FROM
-- fields nest one level deeper under _value.Data
-- authenticode returns .SubjectName and .Trusted
SELECT _value.Data.Name AS Name, _value.Data.Pid AS Pid, _value.Data.Exe AS Exe, _value.Data.Company AS Company, authenticode(filename=_value.Data.Exe).SubjectName AS Signer, authenticode(filename=_value.Data.Exe).Trusted AS Trusted FROM foreach(row=process_tracker_callchain(id="6516-1786866630"))
[
  { "Name": "wininit.exe",    "Pid": "672-1786789991",  "Exe": "C:\\Windows\\System32\\wininit.exe",
    "Company": null,                      "Signer": "O=Microsoft Corporation, CN=Microsoft Windows Publisher", "Trusted": "trusted" },
  { "Name": "services.exe",   "Pid": "820-1786789991",  "Exe": "C:\\Windows\\System32\\services.exe",
    "Company": null,                      "Signer": "O=Microsoft Corporation, CN=Microsoft Windows Publisher", "Trusted": "trusted" },
  { "Name": "svchost.exe",    "Pid": "2248-1786789995", "Exe": "C:\\Windows\\System32\\svchost.exe",
    "Company": null,                      "Signer": "O=Microsoft Corporation, CN=Microsoft Windows Publisher", "Trusted": "trusted" },
  { "Name": "AnyDesk.exe",    "Pid": "5072-1786866630", "Exe": "C:\\Lab\\AnyDesk.exe",
    "Company": "Microsoft Corporation",   "Signer": "O=Microsoft Corporation, CN=Microsoft Windows",           "Trusted": "trusted" },
  { "Name": "TeamViewer.exe", "Pid": "6516-1786866630", "Exe": "C:\\Lab\\TeamViewer.exe",
    "Company": "TeamViewer Germany GmbH", "Signer": "C=DE, O=TeamViewer Germany GmbH, CN=TeamViewer Germany GmbH", "Trusted": "trusted" }
]

The file is named AnyDesk.exe. Its certificate says Microsoft Windows. The name lies, the certificate does not. And the row below it, a genuine TeamViewer, verifies against TeamViewer's own certificate - so the same check exposes the impostor and confirms the real one.

The two columns fail in opposite directions. Company is null on the three Windows ancestors because the tracker only captured metadata for processes it watched start, and those began at boot. Signer is populated for them because authenticode() reads the file from disk at query time. The corollary matters: run this before cleanup. Delete C:\Lab first and the signer column comes back empty for both staged binaries, because there is no file left to check.

The same process in both tools. Wazuh raises the alert, Velociraptor proves the file named AnyDesk is signed by Microsoft.
Fig 8. The same process in both tools. Wazuh raises the alert, Velociraptor proves the file named AnyDesk is signed by Microsoft.

5Cleanup

# Remove the staged decoys and the whole staging directory
Remove-Item 'C:\Lab' -Recurse -Force -ErrorAction SilentlyContinue

# Make sure the scheduled task is gone
schtasks /delete /tn "LabSystemSpawn" /f 2>$null

# Confirm nothing is left behind
Test-Path 'C:\Lab'; schtasks /query /tn "LabSystemSpawn" 2>$null

6Answers

1. Renamed AnyDesk copy - fires. The rule matches company, which is compiled into the file itself, not the filename. Copy-Item changes the label the filesystem holds and nothing inside the file. This is the whole reason identification is anchored there.

2. TeamViewer alone, three processes - does not fire. All three carry the same company, so different_field is never satisfied. Verified: three 100210 alerts, zero 100211. This one is worth running deliberately, because three alerts all reading latest: TeamViewer Germany GmbH from a different test make it look like one vendor is triggering the rule.

3. AnyDesk, then TeamViewer eleven minutes later - does not fire. timeframe is 600 seconds. Both alert histories are in the logs: eleven minutes apart produced nothing, seven seconds apart fired three times. The rule reasons about time, not just co-occurrence.

4. Genuine AnyDesk renamed to svc-helper.exe - does not fire. The parent is matched by a path pattern listing vendor names, and svc-helper.exe contains none of them. This is the exact inverse of the cmd.exe test: there a fake parent with the right name fired wrongly, here a real parent with the wrong name stays silent. Both failures come from the same cause, matching a name instead of an identity.

5. Interactive instead of SYSTEM - does not fire. A process launched from your own shell inherits your integrity level, which is High. The rule requires System. Verified both ways: the interactive run left 100214 at zero, the scheduled task fired it at level 14.

6. Decoy in C:\Windows\Temp - rule 92066 appears, and 100210 does not. Both rules match the event. Wazuh reports one rule per event and keeps the higher level, so level 4 beats level 3 and the custom rule is discarded silently. It gets worse downstream: 100211 chains off 100210, so if 100210 never surfaces the multiplicity rule cannot fire either. A correct rule, matching correctly, invisible.

7. Operation BlueDash - 100211 fires, the causal rules do not. Two vendors on one host inside the window, so multiplicity catches it. The causal rules miss because the parent of both is PowerShell, not a remote-access tool, and 100214 inherits that miss. The blunt rule catches the live campaign; the specific one built on top of it does not.

That is only true because of a fix made while writing this: Level was not in the vendor list, so only ScreenConnect was ever identified and there was nothing to correlate. The correlation logic was never the weak part. The list it reads from is, and a list is only as current as the last person to update it.

7Next steps

Everything above happened on one machine, and the tool doing the launching was a fake cmd.exe that I renamed. That was enough to show the rules work and to show one of them can be tricked, but it is not a real attack. In a real one, someone is already connected to a machine and uses that connection to reach another one. That is the next writeup: come in from the attacker box, land on the first workstation, then use the remote tool sitting there to reach a second workstation and install something else on it.

#           /
#          / /
#         / / /
#        / / / /
#       / / / / /
#      / / / / / /
#     / / / / / / /
#    / / / / / / / /
#   / / / / / / / / / https://purplepyram1d.com

Rules and reproduction steps: github.com/purplepyram1d/rmm-multiplicity