Advanced Transport Features¶
Beyond the core listener types (HTTP/S, DNS, SMB, QUIC, TCP Bind), Stentor's implant includes a suite of advanced transport features for evasion, resilience, and flexibility. These features operate as layers around or alongside the primary transport -- they can be combined to build highly customized C2 communication channels.
Transport Layering
Most of these features are composable. For example, you can use uTLS fingerprinting + domain fronting + rotating transport + adaptive sleep together for a highly resilient and stealthy C2 channel.
DNS over HTTPS (DoH)¶
DNS over HTTPS wraps DNS C2 queries inside standard HTTPS requests to public DoH resolvers (Cloudflare, Google, etc.). This bypasses traditional DNS monitoring that inspects raw UDP/TCP port 53 traffic, since the DNS queries travel inside encrypted HTTPS connections to well-known, trusted endpoints.
Platform Support
DoH transport is currently Windows-only. Non-Windows builds include a stub that returns an error. This is because the primary deployment target is Windows endpoints in corporate environments where DNS monitoring is prevalent.
How It Works¶
sequenceDiagram
participant Implant as Implant (Windows)
participant DoH as DoH Resolver (e.g., Cloudflare)
participant DNS as C2 DNS Server
participant Backend as Backend API
Note over Implant: Encode C2 data as DNS subdomain labels
Implant->>DoH: HTTPS POST /dns-query (RFC 8484)
DoH->>DNS: Standard DNS query to C2 domain
DNS->>Backend: Forward beacon data
Backend-->>DNS: Task data in DNS response
DNS-->>DoH: DNS response (A/AAAA/TXT records)
DoH-->>Implant: HTTPS response with DNS answer
Note over Implant: Decode task from DNS records The implant constructs DNS wire-format queries (RFC 1035), wraps them in HTTPS requests per RFC 8484, and sends them to public DoH resolvers. The C2 server receives the queries through its authoritative DNS server and encodes responses in DNS records.
Data Channels¶
DoH supports three DNS record modes for task retrieval, selectable at runtime:
| Mode | Record Type | Bytes per Response | Best For |
|---|---|---|---|
dns | A (IPv4) | 4 bytes | Minimal data, low profile |
dns6 | AAAA (IPv6) | 16 bytes | Moderate throughput |
dns-txt | TXT | ~255 bytes | Highest throughput (default) |
The mode can be changed at runtime via SetMode(), allowing the operator to switch data channels without redeploying the implant.
Configuration¶
| Field | Type | Default | Description |
|---|---|---|---|
Servers | []string | -- | DoH resolver hostnames (e.g., cloudflare-dns.com, dns.google) |
Verb | string | POST | HTTP method -- GET or POST |
UserAgent | string | -- | Custom User-Agent header |
Proxy | string | -- | HTTPS proxy URL for outbound DoH requests |
Headers | map[string]string | -- | Additional HTTP headers on DoH requests |
Domain | string | -- | C2 domain for DNS queries |
Accept | string | application/dns-message | Accept header value |
Subhost prefixes for different query types are configurable:
| Subhost | Default | Purpose |
|---|---|---|
| A record | cdn | Task retrieval via A records |
| AAAA record | www6 | Task retrieval via AAAA records |
| TXT record | api | Task retrieval via TXT records (default mode) |
DoH Request Methods¶
POST method (default, per Cobalt Strike spec): Sends the DNS wire-format query as the request body with Content-Type: application/dns-message.
GET method: Base64url encodes the DNS query (no padding, per RFC 8484) and places it in the dns query parameter:
Round-Robin Server Selection¶
When multiple DoH servers are configured, the transport uses round-robin selection across requests. This distributes traffic across multiple resolvers to avoid rate limiting and reduce the chance of a single resolver being flagged.
OPSEC Considerations¶
OPSEC
- DoH traffic to Cloudflare/Google is expected on most corporate networks -- it blends with legitimate encrypted DNS traffic
- Configure a custom
UserAgentto match the browser that would normally make DoH requests - Use POST method (default) since it does not expose query data in the URL
- The C2 domain appears only in DNS wire format inside the encrypted HTTPS payload -- not visible to network monitors watching the HTTPS connection
- DoH resolvers log queries; use multiple resolvers with round-robin to distribute the footprint
- Large data exfiltration via DNS (even over HTTPS) is slow; use DoH primarily for command channels and switch to HTTP/S for bulk data transfer
Domain Fronting¶
Domain fronting routes C2 traffic through Content Delivery Networks (CDNs) by exploiting the difference between the TLS SNI hostname and the HTTP Host header. Network monitors see a connection to a legitimate CDN domain, while the CDN routes the request to the actual C2 server based on the Host header.
How It Works¶
sequenceDiagram
participant Implant as Implant
participant CDN as CDN Edge (e.g., CloudFront)
participant C2 as C2 Server
Note over Implant: TLS SNI = d111111abcdef8.cloudfront.net
Note over Implant: HTTP Host = c2.attacker.com
Implant->>CDN: HTTPS connection (SNI: cloudfront.net)
Note over CDN: Routes based on Host header
CDN->>C2: Forward request to c2.attacker.com
C2-->>CDN: C2 response
CDN-->>Implant: HTTPS response
Note over Implant: Network monitor sees: cloudfront.net The FrontedClient builds URLs targeting the front domain (https://d111111abcdef8.cloudfront.net/api/v1/c2/beacon) but sets httpReq.Host = c2.attacker.com to route through the CDN to the real C2 backend.
Configuration¶
| Parameter | Description | Example |
|---|---|---|
frontDomain | CDN domain for TLS SNI | d111111abcdef8.cloudfront.net |
realDomain | Actual C2 domain (HTTP Host header) | c2.attacker.com |
c2Path | API path prefix | /api/v1/c2 |
userAgent | HTTP User-Agent string | Mozilla/5.0 ... |
timeout | HTTP request timeout | 30s |
tlsFingerprint | uTLS browser fingerprint (optional) | chrome_auto |
Proxy Support¶
Domain fronting supports optional proxy configuration via NewFrontedClientWithProxy(). When no explicit proxy is configured, the transport automatically falls back to IE/system proxy detection on Windows (see IE Proxy Detection below).
Integration with uTLS¶
Domain fronting uses newUTLSFrontedTransport() which sets the TLS SNI to the front domain (not the connection target). When a tlsFingerprint is specified, the TLS handshake uses a browser-matching ClientHello while keeping the SNI pointed at the CDN domain.
OPSEC Considerations¶
OPSEC
- CDN provider matters: AWS CloudFront, Azure CDN, and Fastly have been used for fronting. Some providers now actively block domain fronting (Google, AWS in some configurations).
- The front domain should be a high-traffic, high-reputation domain that would not raise suspicion in network logs.
- Use a
tlsFingerprint(e.g.,chrome_auto) to avoid the TLS handshake fingerprinting as Go'scrypto/tlslibrary. - Go's default headers (
User-Agent: Go-http-client/1.1,Accept-Encoding: gzip) are suppressed viasuppressGoDefaults(). - If the CDN performs TLS inspection and validates that SNI matches the Host header, fronting will fail -- test your CDN configuration.
uTLS Fingerprinting¶
Go's standard crypto/tls library produces a distinctive TLS ClientHello fingerprint (JA3/JA4) that network security tools can identify as non-browser traffic. uTLS replaces the TLS handshake with browser-matching fingerprints using the refraction-networking/utls library, making C2 connections appear as normal browser traffic.
How It Works¶
Instead of Go's crypto/tls.Dial, the transport uses utls.UClient with a preset ClientHelloID that reproduces the exact TLS extensions, cipher suites, and handshake parameters of a specific browser version.
Supported Fingerprint Profiles¶
| Profile String | Browser | Notes |
|---|---|---|
chrome_auto | Chrome (latest) | Default fallback for unrecognized values |
chrome_120 | Chrome 120 | Specific version pin |
chrome_131 | Chrome 131 | Specific version pin |
chrome_133 | Chrome 133 | Specific version pin |
firefox_auto | Firefox (latest) | Auto-updated fingerprint |
firefox_120 | Firefox 120 | Specific version pin |
edge_auto | Edge (latest) | Auto-updated fingerprint |
edge_85 | Edge 85 | Legacy version |
edge_106 | Edge 106 | Specific version pin |
safari_auto | Safari (latest) | Auto-updated fingerprint |
safari_16 | Safari 16.0 | macOS/iOS |
ios_14 | iOS 14 Safari | Mobile fingerprint |
random / randomized | Random browser | Randomized per-connection |
Profile Selection
Use chrome_auto or firefox_auto for most engagements -- they auto-update to match the latest browser release. Pin to a specific version (e.g., chrome_131) only when you need reproducible fingerprints for testing or when the target environment has a known browser version.
Transport Variants¶
uTLS is integrated into three transport constructors:
| Constructor | Use Case |
|---|---|
newUTLSTransport(fingerprint, insecureSkipVerify) | Standard HTTPS transport |
newUTLSTransportWithProxy(fingerprint, insecureSkipVerify, proxyURL) | HTTPS through proxy |
newUTLSFrontedTransport(fingerprint, frontDomain, insecureSkipVerify) | Domain fronting (SNI = front domain) |
When fingerprint is empty, all constructors fall back to standard Go crypto/tls (no uTLS). The DisableCompression: true flag is always set to prevent Go from injecting Accept-Encoding: gzip which would fingerprint the client.
OPSEC Considerations¶
OPSEC
- uTLS does not spoof HTTP/2 TLS fingerprints --
http2.Transportuses its own TLS configuration. Use the HTTP/2 Profiled transport for HTTP/2 connections. - The
random/randomizedprofile generates a different fingerprint per connection, which can be a detection vector if an analyst correlates multiple unique fingerprints from the same source. - Match the fingerprint to the
User-Agentheader in your malleable profile -- a Chrome JA3 with a Firefox User-Agent is suspicious. - uTLS supports proxy tunneling: Go's
http.Transporthandles HTTP CONNECT beforeDialTLSContext, so no custom proxy code is needed.
Rotating/Fallback Transport¶
The RotatingTransport wraps multiple transport instances and manages host selection through configurable strategies. It provides multi-host resilience, automatic failover, and an exit strategy when all C2 servers are unreachable.
Rotation Strategies¶
| Strategy | Format | Description |
|---|---|---|
| Round-robin | round-robin | Cycle through hosts sequentially on each request |
| Random | random | Pick a random available host per request |
| Failover (count) | failover-N | Stay on current host until N consecutive failures, then switch |
| Failover (time) | failover-30m | Duration-aware failover variant |
| Rotate (time) | rotate-30m | Switch to next host every 30 minutes regardless of failures |
Duration suffixes: s (seconds), m (minutes), h (hours), d (days).
"round-robin" -- Cycle through all hosts
"random" -- Random selection
"failover-5" -- Switch after 5 failures
"rotate-2h" -- Switch every 2 hours
"rotate-7d" -- Switch every 7 days
Max Retry / Exit Strategy¶
The MaxRetryConfig defines what happens when all hosts are unreachable:
Format: exit-[max]-[increase]-[duration]
exit-96-48-5m
| | |
| | +-- Escalate sleep interval to 5 minutes
| +------ Escalate at 48 consecutive failures
+----------- Exit implant after 96 consecutive failures
| Parameter | Description |
|---|---|
max | Total consecutive failures before implant exits |
increase | Failure count at which sleep is escalated |
duration | Escalated sleep interval (5m, 1h, 7d) |
On success after escalation, the sleep interval is automatically restored to its original value.
Host Mutation (Runtime)¶
Hosts can be added, removed, held, or reset at runtime via the operator's session:
| Method | Description | Limit |
|---|---|---|
AddHost(url) | Add a new C2 host to rotation | Max 32 hosts |
RemoveHost(url) | Remove a host (cannot remove last) | Min 1 host |
SetHeld(url) | Temporarily suspend a host | -- |
ReleaseHeld(url) | Re-enable a suspended host | -- |
ResetHosts() | Restore original host configuration | -- |
Held Host Recovery¶
When all hosts are marked as "held" (temporarily unavailable due to failures), the transport automatically releases all hosts and retries. This prevents a deadlock state where no hosts are available.
Failover Callback¶
Register a callback to be notified when the active host changes:
rt.SetFailoverCallback(func(fromIndex, toIndex int) {
log.Printf("Failover: host %d -> host %d", fromIndex, toIndex)
})
OPSEC Considerations¶
OPSEC
- Use
failover-5for stable C2 with automatic recovery -- the implant stays on the primary host and only fails over when truly unavailable. rotate-2hcycles through hosts on a schedule, distributing traffic across multiple C2 servers to avoid one server accumulating excessive connections.- The exit strategy (
exit-96-48-5m) prevents an implant from beaconing indefinitely when the C2 infrastructure is down, reducing the window for detection. - Sleep escalation at the
increasethreshold slows beacon frequency when connectivity is degraded, reducing network noise. - Use
randomstrategy when multiple C2 servers are fronted by different CDNs to maximize resilience.
Adaptive Sleep¶
The AdaptiveTimer adjusts beacon sleep intervals based on time-of-day and day-of-week. During off-hours (nights and weekends), the sleep interval is multiplied by a configurable factor, reducing beacon frequency when real user traffic is low and anomalous network activity is more likely to be detected.
Configuration¶
| Field | Type | Default | Description |
|---|---|---|---|
WorkStart | int | 8 | Hour (0-23) when business hours begin |
WorkEnd | int | 17 | Hour (0-23) when business hours end |
OffHoursMult | float64 | -- | Sleep multiplier during off-hours (e.g., 3.0) |
Location | string | UTC | IANA timezone (e.g., America/New_York) |
Disabled by Default
NewAdaptiveTimer returns nil when OffHoursMult <= 1.0, effectively disabling the feature. The AdjustedSleep method is nil-safe and returns baseSleep unchanged when the timer is nil.
Sleep Calculation¶
if (hour < WorkStart OR hour >= WorkEnd OR Saturday OR Sunday):
sleep = baseSleep * OffHoursMult
else:
sleep = baseSleep
Example with WorkStart=8, WorkEnd=17, OffHoursMult=3.0, baseSleep=60s:
| Time | Day | Adjusted Sleep |
|---|---|---|
| 10:00 | Tuesday | 60s (business hours) |
| 22:00 | Tuesday | 180s (off-hours, 3x) |
| 06:00 | Wednesday | 180s (before work, 3x) |
| 14:00 | Saturday | 180s (weekend, 3x) |
| 14:00 | Monday | 60s (business hours) |
OPSEC Considerations¶
OPSEC
- Set the timezone to the target's timezone, not the operator's. Use the IANA timezone database format (e.g.,
America/Chicago). - A 3x multiplier is a reasonable starting point: 60s base becomes 180s off-hours.
- On Windows, if the IANA timezone database is not available, the timer falls back to UTC. This may cause incorrect off-hours calculations for non-UTC targets.
- Adaptive sleep makes beacon timing more natural -- defenders monitoring for regular-interval beacons will see different patterns during work hours vs. off-hours.
- Combine with jitter (applied separately at the beacon level) for maximum timing obfuscation.
TCP P2P (Peer-to-Peer Linking)¶
The TCPPeerClient enables parent beacons to connect to child beacons running in TCP bind mode. This creates peer-to-peer relay chains for reaching targets that have no outbound network access, routing C2 traffic through intermediate beacons.
How It Works¶
graph LR
C2[C2 Server] <-->|HTTPS| Parent[Parent Beacon]
Parent <-->|TCP P2P| Child[Child Beacon<br/>Bind Mode]
Child <-->|TCP P2P| Grandchild[Grandchild Beacon<br/>Bind Mode]
style C2 fill:#f96,stroke:#333
style Parent fill:#6f9,stroke:#333
style Child fill:#69f,stroke:#333
style Grandchild fill:#69f,stroke:#333 The child beacon listens on a TCP port (bind mode). The parent beacon connects to it using TCPPeerClient and relays all C2 operations (checkin, task retrieval, result submission) through the P2P message protocol.
P2P Message Protocol¶
All communication uses structured P2P messages with type codes:
| Message Type | Direction | Purpose |
|---|---|---|
MsgTypeCheckin | Parent -> Child | Beacon registration |
MsgTypeCheckinResp | Child -> Parent | Registration response with beacon ID |
MsgTypeGetTask | Parent -> Child | Poll for pending tasks |
MsgTypeTaskResp | Child -> Parent | Task payload or empty response |
MsgTypeSubmitResult | Parent -> Child | Task execution results |
MsgTypeResultAck | Child -> Parent | Acknowledgment (may contain error) |
Connection Lifecycle¶
- Creation:
NewTCPPeerClient(host, port)-- does not connect immediately - First Checkin: Connection is established on the first
Checkin()call - Operations:
GetTask()andSubmitResult()use the established TCP connection - Teardown:
Close()terminates the P2P connection
OPSEC Considerations¶
OPSEC
- TCP P2P traffic is unencrypted at the transport layer by default -- it relies on the P2P protocol's message framing, not TLS. Use this only within internal network segments.
- The bind port on the child beacon is a listening socket that can be discovered by port scans. Use non-standard ports and restrict access with host-based firewalls.
- P2P chains add latency to every C2 interaction -- each hop adds a round-trip.
- Child beacons relay traffic to the C2 server through their own transport, so the entire chain is only as reliable as the weakest link.
HTTP/2 Profiled Transport¶
HTTP2ProfiledClient provides HTTP/2 C2 communication with full malleable profile support. It applies the same URI, header, and body transforms as the standard ProfiledClient but forces HTTP/2 transport using golang.org/x/net/http2.Transport with ALPN h2 negotiation.
Platform Support
HTTP/2 profiled transport is available on Windows and Linux (//go:build windows || linux).
HTTP/2 vs HTTP/1.1 Profiled¶
| Feature | ProfiledClient (HTTP/1.1) | HTTP2ProfiledClient (HTTP/2) |
|---|---|---|
| TLS fingerprint | uTLS browser spoofing | Go's crypto/tls (no uTLS) |
| Protocol | HTTP/1.1 | HTTP/2 with ALPN h2 |
| Multiplexing | No | Yes (concurrent streams) |
| Binary framing | No | Yes (looks like modern browser) |
| Header compression | No | HPACK compression |
Why No uTLS for HTTP/2?
uTLS does not support http2.Transport -- they use incompatible TLS configurations. HTTP/2's binary framing and mandatory TLS 1.3 with ALPN h2 already produce a modern-looking connection fingerprint, making uTLS less critical.
Malleable Profile Integration¶
HTTP/2 profiled transport uses the same profile structure as HTTP/1.1:
- Checkin/GetTask: Uses
http-getprofile block -- metadata transforms, URI selection, termination types (cookie, parameter, header, body) - SubmitResult: Uses
http-postprofile block -- output transforms, ID transforms, configurable HTTP verb
Data placement (termination types):
| Termination | Checkin Data Placement | Task Poll ID Placement |
|---|---|---|
body | Request body | N/A (GET request) |
cookie | Cookie: session=... | Cookie: session=... |
parameter | ?data=... query param | ?beacon_id=... query param |
header | X-Data: ... header | N/A |
OPSEC Considerations¶
OPSEC
- HTTP/2 connections are increasingly common -- most modern websites serve over HTTP/2, making this traffic blend naturally.
- HPACK header compression makes header inspection harder for network monitors.
- The lack of uTLS means the TLS fingerprint will be Go's default
crypto/tls, not a browser fingerprint. This is acceptable because HTTP/2 ALPNh2is already a strong signal of a modern client. - Profile-defined headers suppress Go defaults (
User-Agent: Go-http-client,Accept-Encoding: gzip) viasetOrderedHeaders().
Header Ordering¶
Stentor's header management system removes Go's automatically injected HTTP headers and ensures only profile-defined headers appear in requests. This prevents trivial fingerprinting of the C2 client as a Go application.
Problem: Go's Default Headers¶
Go's net/http automatically injects two headers that identify the client as non-browser:
| Header | Go Default | Detection Risk |
|---|---|---|
User-Agent | Go-http-client/1.1 | Immediate red flag in network logs |
Accept-Encoding | gzip | Reveals Go runtime compression handling |
Solution¶
Two functions handle header management:
setOrderedHeaders() -- For profiled transports (ProfiledClient, HTTP2ProfiledClient):
- Clears all existing headers from the request
- Sets
Accept-Encodingtonil(suppresses Go's auto-injection) - Applies profile-defined headers in their declared order
- Falls back to profile
UserAgentonly if no profile header definesUser-Agent
suppressGoDefaults() -- For non-profiled transports (FrontedClient, EncryptedClient):
- Overrides
User-Agentwith the configured value - Suppresses
Accept-Encoding: gzipauto-injection
HTTP/1.1 Wire Order Limitation
Go's net/http alphabetically sorts header keys when writing HTTP/1.1 on the wire. The primary value of setOrderedHeaders is (a) removing Go-injected defaults and (b) ensuring ONLY profile-defined headers appear. True wire-order control would require a custom RoundTripper.
OPSEC Considerations¶
OPSEC
- Always configure a realistic
User-Agentin your malleable profile that matches the target environment's browser population. - The
Accept-Encoding: niltrick prevents Go from addinggzipbut also means the client will not advertise compression support -- this is normal for some API clients but unusual for browsers. - HTTP/2 uses HPACK header compression which obscures individual header order, making this less critical for HTTP/2 connections.
- Network security tools that flag
Go-http-clientUser-Agent strings will not detect Stentor traffic when headers are properly configured.
IE Proxy Detection (Windows)¶
On Windows targets, the implant automatically detects system proxy settings configured through Internet Options (IE/WinHTTP). This allows the beacon to route through corporate proxy servers without manual configuration.
Platform
This feature is Windows-only. It uses the WinHttpGetIEProxyConfigForCurrentUser API from winhttp.dll.
How It Works¶
At transport initialization, when no explicit proxy is configured, the implant calls the WinHTTP API to read the current user's IE proxy settings. This is the same mechanism that browsers and Windows applications use for proxy discovery.
Proxy Format Support¶
| Format | Example | Handling |
|---|---|---|
| Simple proxy | proxy.corp.com:8080 | Used directly, http:// scheme prepended |
| Per-protocol | http=proxy1:80;https=proxy2:443 | HTTP proxy entry extracted |
| No proxy | -- | Returns empty, no proxy used |
For per-protocol proxy strings, the http= entry is extracted. If no http= entry exists, the first entry is used as a fallback.
Automatic Integration¶
IE proxy detection is called automatically by several transports when no explicit proxy is provided:
NewFrontedClient()-- callsapplyIEProxyFallback(transport)NewFrontedClientWithProxy()-- falls back to IE proxy whenproxyURLis emptynewUTLSTransportWithProxy()-- falls back to IE proxy when bothproxyURLandfingerprintare empty
Limitations¶
| Limitation | Detail |
|---|---|
| No WPAD/PAC support | Only manual proxy settings are detected; auto-configuration (WPAD/PAC) is not supported |
| Per-user settings | Reads the current user's proxy config; SYSTEM-context beacons may have different settings |
| No auto-refresh | Proxy settings are read once at transport initialization; changes require transport restart |
OPSEC Considerations¶
OPSEC
- Using the system proxy is an OPSEC advantage -- it makes C2 traffic indistinguishable from normal application traffic routed through the corporate proxy.
- SYSTEM-context beacons (from services or scheduled tasks) may not have the same proxy settings as the interactive user -- test both contexts.
- The WinHTTP API call does not generate observable events (no network traffic, no event log entries).
- If the proxy requires authentication, Go's
http.Transporthandles NTLM/Negotiate proxy auth automatically when running in the user's context.
Encrypted Transport Wrapper¶
The EncryptedClient adds an AES-256-GCM encryption layer on top of HTTP transport. All C2 payloads (checkin, task retrieval, result submission) are encrypted end-to-end between the implant and the C2 server, independent of TLS.
Why Double Encryption?¶
| Layer | Protection Against |
|---|---|
| TLS (outer) | Network-level interception, MITM |
| AES-256-GCM (inner) | TLS inspection appliances, CDN MITM, compromised proxies |
Even if an SSL inspection appliance or CDN terminates and re-encrypts TLS, the inner AES layer protects the C2 payload from inspection.
Key Exchange Flow¶
sequenceDiagram
participant Implant
participant C2 as C2 Server
Implant->>C2: GET /pubkey (fetch RSA public key)
C2-->>Implant: RSA public key (PEM)
Note over Implant: Generate AES-256 session key
Implant->>C2: POST /keyx (RSA-encrypted session key)
C2-->>Implant: Acknowledgment
Note over Implant,C2: All subsequent traffic uses AES-256-GCM
Implant->>C2: POST /beacon {encrypted checkin}
C2-->>Implant: {encrypted response} - Implant fetches the server's RSA public key
- Implant generates an AES-256 session key and encrypts it with the server's RSA key
- Server decrypts the session key and stores it for the beacon's session
- All subsequent requests use AES-256-GCM encryption
First Checkin Flow¶
The first checkin uses a special flow because the beacon does not yet have an assigned ID:
- Generate a temporary UUID as a session identifier
- Perform key exchange with the temporary UUID
- Send encrypted checkin (beacon ID is nil in the body)
- Server assigns a beacon ID and migrates the session from the temp UUID to the real beacon ID
- Subsequent requests use the server-assigned beacon ID
Request Format¶
Encrypted requests include identifying headers:
| Header | Value | Purpose |
|---|---|---|
X-Encrypted | 1 | Signals the server that the payload is AES-encrypted |
X-Beacon-ID | UUID string | Identifies which session key to use for decryption |
Content-Type | application/json | Standard JSON content type |
The encrypted payload is wrapped in a JSON envelope with base64 encoding:
Session Recovery¶
If the server restarts and loses the session key (returns HTTP 401), the client automatically:
- Invalidates the local session (clears session key and cached public key)
- Re-establishes the session via key exchange
- Retries the failed request (once, to prevent infinite loops)
Custom Endpoint Paths¶
For OPSEC, the key exchange endpoint paths can be customized via SetStagerPaths(pubkeyPath, keyxPath) to match malleable profile stager paths. This avoids using default paths like /pubkey and /keyx that could be signatured.
Integration with uTLS¶
The encrypted client supports uTLS fingerprinting and proxy configuration:
NewEncryptedClient()-- usesnewUTLSTransport(fingerprint, insecureSkipVerify)NewEncryptedClientWithProxy()-- usesnewUTLSTransportWithProxy(fingerprint, insecureSkipVerify, proxyURL)
OPSEC Considerations¶
OPSEC
- The
X-Encrypted: 1andX-Beacon-IDheaders are custom headers that could be signatured. Consider customizing header names via malleable profile if using encrypted transport in sensitive environments. - Key exchange is a one-time operation per session -- it only generates network traffic during initial setup and after server restarts.
- The RSA public key fetch (
/pubkey) and key exchange (/keyx) endpoints use default paths unless customized viaSetStagerPaths()-- change these in production. - AES-256-GCM provides authenticated encryption, preventing payload tampering.
- Session invalidation on 401 ensures the implant recovers gracefully from server restarts without operator intervention.
Transport Comparison¶
| Transport Feature | Stealth | Throughput | Resilience | Platform | Complexity |
|---|---|---|---|---|---|
| DNS over HTTPS | High | Low | Medium | Windows | Medium |
| Domain Fronting | Very High | High | Medium | All | Low |
| uTLS Fingerprinting | High | N/A (layer) | N/A | All | Low |
| Rotating Transport | N/A (layer) | N/A | Very High | All | Medium |
| Adaptive Sleep | High | N/A (timing) | N/A | All | Low |
| TCP P2P | Low | High | Medium | All | Low |
| HTTP/2 Profiled | High | High | Medium | Win/Linux | Medium |
| Header Ordering | High | N/A (layer) | N/A | All | Low |
| IE Proxy Detection | High | N/A (layer) | High | Windows | Low |
| Encrypted Wrapper | Medium | High | Medium | All | Medium |
See Also¶
- HTTP/HTTPS Listeners -- Standard HTTP/HTTPS transport configuration
- DNS Listeners -- Raw DNS C2 transport
- QUIC Listeners -- HTTP/3 over QUIC transport
- SMB Listeners -- Named pipe transport for lateral movement
- TCP Bind Listeners -- TCP bind mode for P2P linking
- Malleable Profiles -- Profile-based traffic shaping