Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions plugins/alerts/vmware_esxi_alert_volume_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

// ESXi records after header parsing (process and message, as the ESXi filter
// stores them) evaluated with this module's go-sdk CEL. They pin the
// de-duplication keys that keep three ESXi rules from alerting on every record
// of routine activity, and check that the keys resolve. Header parsing of raw
// lines runs in the EventProcessor playground instead.
import (
"encoding/json"
"reflect"
"testing"

"github.com/threatwinds/go-sdk/plugins"
"github.com/threatwinds/go-sdk/utils"
"github.com/tidwall/gjson"
"google.golang.org/protobuf/encoding/protojson"
)

func esxiVolumeRule(t *testing.T, name string) *plugins.Rule {
t.Helper()
b, err := utils.ReadPbYaml("../../rules/vmware/vmware-esxi/" + name + ".yml")
if err != nil {
t.Fatal(err)
}
r := new(plugins.Rule)
if err = protojson.Unmarshal(b, r); err != nil {
t.Fatal(err)
}
r.Normalize()
return r
}

func esxiVolumeEvent(t *testing.T, process, message string) string {
t.Helper()
b, err := json.Marshal(map[string]any{
"dataType": "vmware-esxi", "dataSource": "192.0.2.21",
"log": map[string]any{"process": process, "message": message},
})
if err != nil {
t.Fatal(err)
}
return string(b)
}

func TestESXiAlertVolume(t *testing.T) {
cache := plugins.NewCELCache("esxi-alert-volume")
for _, tc := range []struct {
file string
dedup []string
positive, negative [][2]string
}{
{"vm_escape_detection", []string{"dataSource", "lastEvent.log.process"},
[][2]string{{"Vpxa", "[VpxLRO] -- ERROR lro-1001 -- 5f1c2d3e-0000-4000-8000-000000000001 -- guestOperationsFileManager -- vim.vm.guest.FileManager.listFiles: :vim.fault.FileNotFound"}},
[][2]string{{"Vpxa", "[VpxLRO] -- BEGIN lro-1002 -- guestOperationsFileManager -- vim.vm.guest.FileManager.deleteFile -- 5f1c2d3e-0000-4000-8000-000000000002"}}},
{"esxi_syslog_disruption", []string{"dataSource"},
[][2]string{{"vobd", `[UserLevelCorrelator] 12480247076375us: [vob.user.vmsyslogd.remote.failure] The host "192.0.2.50:7002" has become unreachable. Remote logging to this host has stopped.`}},
[][2]string{{"Hostd", "[Originator@6876 sub=Statssvc.StatsCollector] Calculated read I/O size 648481 for scsi0:8 is out of range"}}},
{"esxi_firewall_modification", []string{"dataSource"},
[][2]string{{"shell", "[shell[1234]]: [root]: esxcli network firewall ruleset set -r sshServer -e true"}},
[][2]string{{"Hostd", "[Originator@6876 sub=Statssvc.StatsCollector] Calculated read I/O size 648481 for scsi0:8 is out of range"}}},
} {
t.Run(tc.file, func(t *testing.T) {
r := esxiVolumeRule(t, tc.file)
if len(r.GroupBy) != 0 || !reflect.DeepEqual(r.DeduplicateBy, tc.dedup) {
t.Fatalf("grouping got groupBy %v deduplicateBy %v, want deduplicateBy %v", r.GroupBy, r.DeduplicateBy, tc.dedup)
}
for i, rec := range append(tc.positive, tc.negative...) {
want := i < len(tc.positive)
event := esxiVolumeEvent(t, rec[0], rec[1])
if got, err := cache.Eval(r.Where, event); err != nil || got != want {
t.Fatalf("record %d: where got %v (%v), want %v for %s", i, got, err, want, event)
}
if !want {
continue
}
for _, key := range tc.dedup {
path := map[string]string{"dataSource": "dataSource", "lastEvent.log.process": "log.process"}[key]
if v := gjson.Get(event, path); v.Type != gjson.String || v.String() == "" {
t.Fatalf("de-duplication key %s (%s) does not resolve in %s", key, path, event)
}
}
}
})
}
}
8 changes: 4 additions & 4 deletions rules/vmware/vmware-esxi/esxi_firewall_modification.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rule version v1.0.0
# Rule version v1.1.0

dataTypes:
- vmware-esxi
Expand All @@ -14,7 +14,7 @@ references:
- https://kb.vmware.com/s/article/2008226
- https://attack.mitre.org/techniques/T1562/004/
description: |
Detects modifications to ESXi firewall ruleset configurations using esxcli network firewall commands. Attackers modify firewall rules to enable outbound connections for C2 communication or to expose management interfaces.
Detects modifications to ESXi firewall ruleset configurations using esxcli network firewall commands. Attackers modify firewall rules to enable outbound connections for C2 communication or to expose management interfaces. One alert is raised per ESXi host; repeats are suppressed for seven days.

Next Steps:
1. Review the specific firewall rule changes
Expand All @@ -36,5 +36,5 @@ where: |
)) ||
(contains("log.message", "iptables") && exists("origin.hostname"))
)
groupBy:
- adversary.hostname
deduplicateBy:
- dataSource
8 changes: 4 additions & 4 deletions rules/vmware/vmware-esxi/esxi_syslog_disruption.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rule version v1.0.0
# Rule version v1.1.0

dataTypes:
- vmware-esxi
Expand All @@ -14,7 +14,7 @@ references:
- https://kb.vmware.com/s/article/2003322
- https://attack.mitre.org/techniques/T1562/002/
description: |
Detects attempts to disable or modify ESXi syslog forwarding to prevent security monitoring. Attackers disable syslog to operate without detection by the SIEM.
Detects attempts to disable or modify ESXi syslog forwarding to prevent security monitoring. Attackers disable syslog to operate without detection by the SIEM. ESXi also reports here when the remote log host becomes unreachable. One alert is raised per ESXi host; repeats are suppressed for seven days.

Next Steps:
1. Verify the syslog configuration change was authorized
Expand All @@ -36,5 +36,5 @@ where: |
(contains("log.message", "syslog") && contains("log.message", "disabled")) ||
(contains("log.message", "Syslog.global.logHost") && contains("log.message", "changed"))
)
groupBy:
- adversary.hostname
deduplicateBy:
- dataSource
8 changes: 4 additions & 4 deletions rules/vmware/vmware-esxi/vm_escape_detection.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Rule version v1.0.0
# Rule version v1.1.0

dataTypes:
- vmware-esxi
Expand All @@ -15,7 +15,7 @@ references:
- https://www.csoonline.com/article/3837874/vmware-esxi-gets-critical-patches-for-in-the-wild-virtual-machine-escape-attack.html
- https://blogs.vmware.com/security/2022/09/esxi-targeting-ransomware-the-threats-that-are-after-your-virtual-machines-part-1.html
description: |
Detects VM escape attempts through abnormal guest operations, suspicious VMware Tools activity, or attempts to access host resources from guest VMs. VM escape is a critical security event where an attacker breaks out of virtual machine containment to access the hypervisor or host system.
Detects VM escape attempts through abnormal guest operations, suspicious VMware Tools activity, or attempts to access host resources from guest VMs. VM escape is a critical security event where an attacker breaks out of virtual machine containment to access the hypervisor or host system. One alert is raised per ESXi host and process; repeats are suppressed for seven days.

Next Steps:
1. Immediately isolate the affected virtual machine from the network
Expand All @@ -32,6 +32,6 @@ where: |
(regexMatch("log.message", "(?i)(vmdk|vswp)") && regexMatch("log.message", "(?i)(modify|rename|delete)") && !regexMatch("log.process", "(?i)(vmware|esxcli)")) ||
(regexMatch("log.message", "(?i)vib.*install") && regexMatch("log.message", "(?i)(malicious|unsigned|failed)")) ||
(regexMatch("log.eventInfo", "(?i)(vm.*escape|breakout|containment.*breach)"))
groupBy:
deduplicateBy:
- dataSource
- lastEvent.log.process
- adversary.hostname
Loading