Sliver#

InfraGuard parses Sliver’s HTTPS C2 profile JSON to generate all valid URI combinations that an implant might use. Sliver constructs request URIs procedurally from path/file/extension segments, so the parser expands these into a full URI set for the profile filter.

Sliver supports proxy-aware C2 over both HTTP and HTTPS. Since Sliver does not rely on the SSL/TLS layer for security, these protocols are considered synonymous – an implant generated with --http may attempt to connect over both HTTP and HTTPS.

Config#

domains:
  assets.example.com:
    upstream: "${SLIVER_UPSTREAM}"
    profile_path: "examples/sliver.json"
    profile_type: "sliver"

    drop_action:
      type: "tarpit"
      target: ""

See config/examples/c2-sliver.yaml.

Profile JSON Format#

The C2 profile lives at ~/.sliver/configs/http-c2.json on the Sliver teamserver. It has two top-level objects: implant_config (controls how the implant generates requests) and server_config (controls how the server responds).

Changes to the C2 profile may break compatibility with existing implants. Make profile changes before generating any implants.

implant_config#

The implant procedurally generates each HTTP request. URIs are constructed as /{path}/{file}{ext}, where each segment is randomly selected from the configured lists.

URI Generation Fields#

The profile uses three flat lists shared across all communication stages:

FieldDescription
pathsArray of path segments (e.g. js, assets, scripts)
filesArray of file name stems (e.g. jquery.min, bootstrap, app)
extensionsArray of file extensions (e.g. ["js", "", "php"]). Empty string means no extension.

Each implant is embedded with a randomly generated subset of the server’s profile (controlled by min_files/max_files and min_paths/max_paths), so two implants generated from the same server may produce different URL patterns.

Legacy per-stage keys#

Older Sliver versions used stage-specific path/file/extension keys. The parser still supports these and prefers them when present, falling back to the generic paths/files/extensions arrays when a stage-specific key is missing.

StagePaths keyFiles keyExtension keyDefault ExtHTTP Method
stagerstager_pathsstager_filesstager_file_ext.woffGET
pollpoll_pathspoll_filespoll_file_ext.jsGET
start_sessionsession_pathssession_filesstart_session_file_ext.htmlPOST
sessionsession_pathssession_filessession_file_ext.phpPOST
closeclose_pathsclose_filesclose_file_ext.pngGET

If your profile uses the flat format (the common case now), the parser uses paths for all stage path lookups, files for all stage file lookups, and extensions as the extension list for every stage.

Other Fields#

FieldDescription
user_agentThe implant’s User-Agent. Empty string (default) randomly generates a platform-appropriate UA.
chrome_base_versionChrome version number for the generated UA, plus a random value up to 3 (default: 100)
macos_versionmacOS version string used in the generated UA (e.g. 10_15_7)
min_files / max_filesMin/max number of file names randomly selected per implant build (default: 2/4)
min_paths / max_pathsMin/max number of path segments randomly selected per implant build (default: 2/4)
min_path_length / max_path_lengthMin/max character length of individual path segments (default: 2/4)
url_parametersArray of {name, value, probability} objects added as URL query parameters. Probability 1-100.
headersArray of {name, value, probability} objects added as request headers

Nonce#

Every request includes a nonce query parameter that looks like a standard cache-buster but actually encodes which data encoder was used. The nonce behavior is configured by:

FieldDescription
nonce_query_argsAlphabet used to generate the nonce parameter name (default: lowercase a-z)
nonce_query_lengthLength of the nonce parameter name (default: 1, so the param name is a single letter like ?a=, ?b=, etc.)
nonce_modeHow the nonce is transmitted. UrlParam puts it in the query string.

The server computes nonce % EncoderModulus to determine the encoder ID. Requests without a valid nonce are ignored from a C2 standpoint (though the server may still respond with static website content).

Supported encoders:

EncoderDescription
Base64Custom alphabet (not interoperable with standard Base64)
HexStandard hex encoding
GzipStandard gzip
EnglishEncodes data as English ASCII text
PNGEncodes data into valid PNG image files
Gzip+EnglishCombined
Base64+GzipCombined

The server always responds using the same encoder as the request.

server_config#

FieldDescription
random_version_headersIf true, the server returns random Apache/Nginx and PHP version numbers (default: false)
headersArray of {name, value, probability, method} response headers. probability 1-100 (any other value treated as 100). method optionally restricts the header to GET or POST responses only.
cookiesArray of cookie name strings (e.g. JSESSIONID, PHPSESSID, SID)

Example Profile#

{
  "implant_config": {
    "user_agent": "",
    "chrome_base_version": 106,
    "macos_version": "10_15_7",
    "nonce_query_args": "abcdefghijklmnopqrstuvwxyz",
    "nonce_query_length": 1,
    "nonce_mode": "UrlParam",
    "url_parameters": null,
    "headers": null,
    "max_files": 4,
    "min_files": 2,
    "max_paths": 4,
    "min_paths": 2,
    "max_path_length": 4,
    "min_path_length": 2,
    "extensions": ["js", "", "php"],
    "files": [
      "bootstrap", "bootstrap.min", "jquery.min", "jquery",
      "route", "app", "app.min", "array", "backbone", "script",
      "email", "main", "index", "utils"
    ],
    "paths": [
      "js", "umd", "assets", "bundle", "bundles", "scripts",
      "script", "javascripts", "javascript", "jscript"
    ]
  },
  "server_config": {
    "random_version_headers": false,
    "headers": [
      {
        "name": "Cache-Control",
        "value": "no-store, no-cache, must-revalidate",
        "probability": 100,
        "method": "GET"
      }
    ],
    "cookies": [
      "JSESSIONID", "rememberMe", "authToken", "PHPSESSID",
      "SID", "SSID", "APISID"
    ]
  }
}

The example above is trimmed for readability. Real profiles typically have much larger files, paths, and cookies arrays to increase randomization.

How InfraGuard Parses It#

The SliverParser handles both the flat format and the legacy per-stage format. For each communication stage, it checks for the stage-specific key first (e.g. poll_paths), then falls back to the generic paths array. Same logic for files and extensions.

URI generation works by computing the cross product of paths, files, and extensions:

  • GET URIs: poll + close + stager combinations
  • POST URIs: session + start_session combinations

Each set is capped at 100 URIs to prevent combinatorial explosion with large path/file lists.

Extensions without a leading dot get one prepended automatically. An empty string in the extensions array produces URIs with no extension (e.g. /{path}/{file} instead of /{path}/{file}.js).

The parser maps Sliver’s cookie list to the client message config – the first cookie in the server’s cookie list (e.g. JSESSIONID) is used as the data transport cookie name for GET requests. POST requests carry data in the body.

Server response headers with probability >= 50 are included in the profile’s expected response headers.

Generating the Implant#

sliver > generate --http example.com --os mac

Multiple domains can be specified as fallbacks. If the implant can’t connect to the first domain, it tries each subsequent one based on --reconnect (default 60 seconds) until --max-errors (default 1000) is reached:

sliver > generate --http example.com,fallback.com

URL Prefixes#

A path prefix can be prepended to all generated URLs by appending it to the domain:

sliver > generate --http example.com/foo/bar

This prepends /foo/bar to every request URI. Different domains can have different prefixes. Useful when routing through InfraGuard based on path.

Proxy Support#

The implant auto-detects system proxy settings on Windows, macOS, and Linux using a modified go-get-proxied library. Connection attempts are made in this order:

  1. HTTPS over system proxy
  2. HTTP over system proxy
  3. HTTPS direct connect
  4. HTTP direct connect

Proxy settings are read from environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY), and on Windows from Internet Options and WinHTTP settings. NO_PROXY is respected on all platforms.

For environments requiring NTLM/Kerberos proxy authentication (Windows only), the implant can be built with the wininet HTTP library, though this is less stable and more susceptible to EDR hooking.

Listeners#

Start an HTTP listener on the Sliver teamserver:

sliver > http
sliver > http --domain example.com

The --domain flag restricts the listener to a specific domain. Static website content can be served alongside C2 traffic using the --website flag to make the server look legitimate:

sliver > websites --website fake-blog --web-path / --content ./index.html add
sliver > http --website fake-blog --domain example.com

C2 messages are intercepted before website content is checked, so website content can be mapped to any URL including those used for C2.

For HTTPS, the listener generates a random self-signed certificate by default. Let’s Encrypt automatic certificates are supported with --lets-encrypt, or you can provide your own:

sliver > https --domain example.com --cert ./cert.pem --key ./key.pem
pipeline:
  filter_mode: "scoring"
  block_score_threshold: 0.7
  enable_profile_filter: true
  enable_replay_filter: true
  replay_persist: true
  enable_sandbox_filter: true
  enable_ja3_filter: true

Sliver randomizes the User-Agent per implant build (when user_agent is empty), so UA-based filtering won’t be reliable. The bot filter still catches known scanner User-Agents, and the JA3 filter catches non-browser TLS stacks.