Detect Virtual Private Server in Splunk
Adversaries may compromise third-party Virtual Private Servers (VPSs) to use as operational infrastructure. By taking over VPS instances purchased by legitimate third parties at providers such as DigitalOcean, Linode, Vultr, Hetzner, or OVH, adversaries gain infrastructure that carries the reputation of trusted cloud providers while obscuring the true origin of their operations. Compromised VPS infrastructure is typically leveraged for Command and Control, staging payloads, proxying traffic, or exfiltrating data. Notable examples include Volt Typhoon compromising VPS nodes to proxy C2 traffic through legitimate-appearing cloud infrastructure, and Turla reusing compromised Iranian threat actor VPS infrastructure. Detection must pivot from the adversary's external preparatory action (compromising the VPS itself, which is unobservable from the victim network) to the observable USAGE patterns: beaconing connections to VPS provider IP space, C2-characteristic network behavior such as regular connection intervals with low data volume, and threat intelligence matches against known-compromised VPS nodes.
MITRE ATT&CK
- Tactic
- Resource Development
- Technique
- T1584 Compromise Infrastructure
- Sub-technique
- T1584.003 Virtual Private Server
- Canonical reference
- https://attack.mitre.org/techniques/T1584/003/
SPL Detection Query
| tstats summarydata=t count as ConnectionCount, sum(All_Traffic.bytes_out) as TotalBytesSent, sum(All_Traffic.bytes_in) as TotalBytesReceived, min(_time) as FirstSeen, max(_time) as LastSeen
FROM datamodel=Network_Traffic.All_Traffic
WHERE All_Traffic.action="allowed"
AND (All_Traffic.dest_port=80 OR All_Traffic.dest_port=443 OR All_Traffic.dest_port=8080 OR All_Traffic.dest_port=8443 OR All_Traffic.dest_port=4443 OR All_Traffic.dest_port=4444 OR All_Traffic.dest_port=1080 OR All_Traffic.dest_port=9001)
AND (
cidrmatch("64.225.0.0/16", All_Traffic.dest) OR cidrmatch("143.198.0.0/16", All_Traffic.dest)
OR cidrmatch("157.245.0.0/16", All_Traffic.dest) OR cidrmatch("167.71.0.0/16", All_Traffic.dest)
OR cidrmatch("45.33.0.0/16", All_Traffic.dest) OR cidrmatch("45.56.0.0/16", All_Traffic.dest)
OR cidrmatch("45.79.0.0/16", All_Traffic.dest)
OR cidrmatch("45.63.0.0/16", All_Traffic.dest) OR cidrmatch("45.76.0.0/16", All_Traffic.dest)
OR cidrmatch("95.216.0.0/16", All_Traffic.dest) OR cidrmatch("135.181.0.0/16", All_Traffic.dest)
OR cidrmatch("138.201.0.0/16", All_Traffic.dest) OR cidrmatch("157.90.0.0/16", All_Traffic.dest)
OR cidrmatch("176.9.0.0/16", All_Traffic.dest) OR cidrmatch("5.9.0.0/16", All_Traffic.dest)
)
BY All_Traffic.src, All_Traffic.dest, All_Traffic.dest_port
| rename All_Traffic.src as src_ip, All_Traffic.dest as dest_ip, All_Traffic.dest_port as dest_port
| eval SessionDurationMinutes = round((LastSeen - FirstSeen) / 60, 2)
| eval AvgIntervalMinutes = if(ConnectionCount > 1, round(SessionDurationMinutes / (ConnectionCount - 1), 2), null())
| eval AvgBytesPerConn = round((TotalBytesSent + TotalBytesReceived) / ConnectionCount, 0)
| eval LowVolumeBeacon = if(AvgBytesPerConn < 5000 AND ConnectionCount > 20, "true", "false")
| eval RegularInterval = if(AvgIntervalMinutes > 0 AND AvgIntervalMinutes < 60, "true", "false")
| where ConnectionCount >= 10 AND (LowVolumeBeacon="true" OR RegularInterval="true" OR ConnectionCount > 50)
| eval FirstSeenHuman = strftime(FirstSeen, "%Y-%m-%d %H:%M:%S")
| eval LastSeenHuman = strftime(LastSeen, "%Y-%m-%d %H:%M:%S")
| table src_ip, dest_ip, dest_port, ConnectionCount, AvgIntervalMinutes, AvgBytesPerConn, LowVolumeBeacon, RegularInterval, TotalBytesSent, TotalBytesReceived, FirstSeenHuman, LastSeenHuman
| sort - ConnectionCount Uses the Splunk Network_Traffic datamodel (tstats for performance) to identify beaconing connections from internal hosts to known VPS provider IP ranges (DigitalOcean, Linode, Vultr, Hetzner) on C2-relevant ports. Calculates average inter-connection interval and bytes-per-connection to flag automated beacon patterns. Low volume with high frequency is the signature of C2 check-in traffic. Alerts where connection count ≥10 and either the low-volume beacon or regular interval flag is set.
Data Sources
Required Sourcetypes
False Positives & Tuning
- SaaS application clients (Slack, Teams, Zoom) making persistent connections to cloud-hosted endpoints on these provider ranges
- Corporate VPN concentrators hosted at the listed providers that all clients connect through
- Monitoring agents (Datadog, New Relic, Elastic) that regularly beacon to collector endpoints on cloud VPS infrastructure
- Developer machines running CI/CD jobs that make API calls to cloud-hosted build infrastructure
- Browser telemetry and crash-reporting services that perform regular check-ins to cloud endpoints
Other platforms for T1584.003
Testing Methodology
Validate this detection against 4 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.
- Test 1Simulate C2 Beacon to VPS Provider IP Range
Expected signal: Sysmon Event ID 3: 20 Network Connection events with Image=curl.exe (or the parent PowerShell/cmd.exe), DestinationIp=143.198.0.1, DestinationPort=80. Events will be spaced approximately 60 seconds apart. Firewall/proxy logs will show 20 outbound connections to 143.198.0.1:80 from the test host.
- Test 2Resolve and Connect to Known VPS-Hosted Domain (DNS + Network Correlation)
Expected signal: Sysmon Event ID 22: DNS query for ipinfo.io resolving to an IP in cloud provider space. Sysmon Event ID 3: Network connection to the resolved IP on port 80 within seconds of the DNS query. Both events will share the same PID (powershell.exe) allowing correlation. The DNS event will contain the resolved IP in QueryResults.
- Test 3Linux Beacon Simulation to VPS IP Range
Expected signal: auditd SYSCALL records for connect() syscalls to 95.216.0.1:80, or syslog entries if auditd is configured to capture network calls. On hosts with Sysmon for Linux: Event ID 3 (Network Connection) with DestinationIp=95.216.0.1. Firewall/proxy session logs show repeated connections from the source host at ~30s intervals.
- Test 4Cobalt Strike Default JA3 Fingerprint Simulation via Custom TLS Client Hello
Expected signal: Sysmon Event ID 3: Network connection to 143.198.0.1:443 initiated by python.exe. If TLS fingerprinting is deployed at the network layer (Zeek bro:ssl:json or similar), the JA3 field will contain the fingerprint value matching or approximating Cobalt Strike's default. MDE DeviceNetworkEvents will record the connection with TlsFingerprint field populated.
References (9)
- https://attack.mitre.org/techniques/T1584/003/
- https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-038a
- https://media.defense.gov/2019/Oct/18/2002197242/-1/-1/0/NSA_CSA_Turla_20191021%20ver%204%20-%20nsa.gov.pdf
- https://michaelkoczwara.medium.com/cobalt-strike-c2-hunting-with-shodan-c448d501a6e2
- https://cloud.google.com/blog/topics/threat-intelligence/scandalous-external-detection-using-network-scan-data-and-automation/
- https://threatconnect.com/blog/infrastructure-research-hunting/
- https://github.com/activecm/rita
- https://docs.zeek.org/en/master/logs/ssl.html
- https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/
Unlock Pro Content
Get the full detection package for T1584.003 including response playbook, investigation guide, and atomic red team tests.