From a3986b74972541cfdb7a4daaf2ce9c128105bcb4 Mon Sep 17 00:00:00 2001 From: "marcos.luna" Date: Mon, 17 Nov 2025 16:32:41 +0100 Subject: [PATCH] logger container ready --- Payload_Type/cazalla/Dockerfile | 7 + Payload_Type/cazalla/logger/README.md | 376 +++++++++++++++++++++++ Payload_Type/cazalla/logger/__init__.py | 6 + Payload_Type/cazalla/logger/logger.py | 350 +++++++++++++++++++++ Payload_Type/cazalla/logger/syslog.conf | 14 + Payload_Type/cazalla/main.py | 17 +- README.md | 58 ++++ documentation-payload/Cazalla/_index.md | 1 + documentation-payload/Cazalla/logging.md | 281 +++++++++++++++++ 9 files changed, 1109 insertions(+), 1 deletion(-) create mode 100644 Payload_Type/cazalla/logger/README.md create mode 100644 Payload_Type/cazalla/logger/__init__.py create mode 100644 Payload_Type/cazalla/logger/logger.py create mode 100644 Payload_Type/cazalla/logger/syslog.conf create mode 100644 documentation-payload/Cazalla/logging.md diff --git a/Payload_Type/cazalla/Dockerfile b/Payload_Type/cazalla/Dockerfile index 2c94daa..78363f0 100644 --- a/Payload_Type/cazalla/Dockerfile +++ b/Payload_Type/cazalla/Dockerfile @@ -34,6 +34,13 @@ ENV MYTHIC_SERVER_PORT="17443" ENV MYTHIC_SERVER_USERNAME="mythic_admin" ENV MYTHIC_SERVER_PASSWORD="mythic_password" +# --- Variables de entorno para Syslog Logging Container (opcional) --- +# El logger lee la configuraciΓ³n desde logger/syslog.conf (recomendado) +# TambiΓ©n puede leer desde variables de entorno si estΓ‘n configuradas +ENV SYSLOG_SERVER="" +ENV SYSLOG_PORT="514" +ENV SYSLOG_FACILITY="16" + # --- Copia tu cΓ³digo de translator --- # This COPY ensures local changes are included when USE_BUILD_CONTEXT=true WORKDIR /Mythic/ diff --git a/Payload_Type/cazalla/logger/README.md b/Payload_Type/cazalla/logger/README.md new file mode 100644 index 0000000..cbf1da0 --- /dev/null +++ b/Payload_Type/cazalla/logger/README.md @@ -0,0 +1,376 @@ +# πŸ“Š Cazalla Logging Container + +Complete logging system for the Cazalla agent that records all Mythic events and sends them to a central syslog server. + +## πŸ“‹ Description + +This logging container follows the official Mythic pattern for logging containers. It automatically subscribes to all Mythic events and logs them to: + +1. **Local files**: One file per day at `/tmp/cazalla_mythic_YYYY-MM-DD.log` (inside the container) +2. **Central syslog**: Sends all events to a configured rsyslog server (RFC 3164) + +**Advantages:** +- βœ… Does not modify the agent (avoids detections) +- βœ… Follows the official Mythic pattern +- βœ… Runs on the server, not on the target +- βœ… Logs ALL events automatically +- βœ… Simple configuration via configuration file +- βœ… No log rotation (one file per day, all files are kept) + +## πŸ—οΈ Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Mythic β”‚ +β”‚ Events β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CazallaLogger β”‚ +β”‚ (Logging β”‚ +β”‚ Container) β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”œβ”€β”€β–Ί Local file (/tmp/cazalla_mythic_YYYY-MM-DD.log) + β”‚ + └──► Syslog Server (rsyslog) ──► /var/log/cazalla.log +``` + +## βš™οΈ Configuration + +### Method 1: Configuration File (Recommended) + +The logger reads configuration from a file inside the container. **You don't need to modify docker-compose.yml**. + +1. **Edit the configuration file:** + ```bash + nano Payload_Type/cazalla/logger/syslog.conf + ``` + +2. **Configure your values:** + ```bash + # Syslog server IP or hostname (leave empty to disable syslog) + # Use the host IP (10.8.20.50) or 172.17.0.1 (docker0 bridge) to reach rsyslog on the host + SYSLOG_SERVER=10.8.20.50 + + # Syslog server port (default: 514) + SYSLOG_PORT=514 + + # Syslog facility code (0-23, default: 16 = LOCAL0) + SYSLOG_FACILITY=16 + ``` + +3. **Rebuild and restart the container:** + ```bash + cd /opt/mythic + sudo ./mythic-cli build cazalla + sudo ./mythic-cli restart cazalla + ``` + +### Method 2: Environment Variables (Alternative) + +If you prefer to use environment variables, you can configure them in Mythic's `.env` file and manually add them to `docker-compose.yml`. However, **Method 1 is simpler**. + +### Configuring rsyslog on the Host + +If you want to receive logs on the Debian host where Mythic runs: + +1. **Install rsyslog** (if not installed): + ```bash + sudo apt-get update && sudo apt-get install -y rsyslog + ``` + +2. **Enable UDP reception** in `/etc/rsyslog.conf`: + ```bash + sudo sed -i 's/#module(load="imudp")/module(load="imudp")/' /etc/rsyslog.conf + sudo sed -i 's/#input(type="imudp" port="514")/input(type="imudp" port="514")/' /etc/rsyslog.conf + ``` + +3. **Create rule for Cazalla** in `/etc/rsyslog.d/50-cazalla.conf`: + ```bash + sudo bash -c 'cat > /etc/rsyslog.d/50-cazalla.conf << EOF + # Logs from Cazalla Mythic agent + # Filter by facility LOCAL0 (16) + if \$syslogfacility-text == "local0" then /var/log/cazalla.log + & stop + EOF + ' + ``` + +4. **Restart rsyslog:** + ```bash + sudo systemctl restart rsyslog + ``` + +5. **Verify it's listening:** + ```bash + sudo netstat -ulnp | grep 514 + # You should see: UNCONN ... 0.0.0.0:514 ... rsyslogd + ``` + +## πŸ“Š Events Logged + +The logger subscribes to **ALL** Mythic events: + +| Event | Description | Example | +|--------|-------------|---------| +| `CALLBACK` | Agent connects/checkin | New callback from target | +| `TASK` | Task created/updated | Command executed (`shell`, `ls`, etc.) | +| `RESPONSE` | Agent response | Command output | +| `CREDENTIAL` | Credential captured | Username/password extracted | +| `KEYLOG` | Keystrokes captured | Keylogger data | +| `FILE` | File uploaded/downloaded | File upload/download | +| `ARTIFACT` | Artifact generated | IOC detected (process created, etc.) | +| `PAYLOAD` | Payload created | New payload generated | + +## πŸ“ Log Format + +### Local File + +Logs are saved in structured JSON format: + +```json +{ + "event_type": "TASK", + "timestamp": "2025-11-17T15:18:23.117715674Z", + "operation_id": 1, + "operation_name": "Operation Chimera", + "username": "mythic_admin", + "action": "new_task", + "server_name": "mythic", + "data": { + "id": 18, + "command_name": "shell", + "params": "{\"command\":\"whoami\"}", + "status": "success", + ... + } +} +``` + +### Syslog + +Messages are sent to syslog in RFC 3164 format with complete JSON: + +``` +<134>Nov 17 15:18:23 cazalla cazalla: {"event_type": "TASK", "timestamp": "...", ...} +``` + +- **Priority**: Calculated based on facility (16 = LOCAL0) + severity +- **Severity**: INFO (6) for most, WARNING (4) for artifacts +- **Tag**: `cazalla` +- **Message**: Complete JSON of the event + +## πŸ” Verification + +### Verify Logger is Active + +```bash +# View container logs +sudo docker logs cazalla | grep -i "syslog\|Logger initialized" + +# You should see: +# INFO:cazalla_logger:Syslog client initialized: 10.8.20.50:514 (facility=16) +# INFO:cazalla_logger:Logger initialized: cazalla_logger +``` + +### Verify Local Logs (inside container) + +```bash +# View today's log file +sudo docker exec cazalla cat /tmp/cazalla_mythic_$(date +%Y-%m-%d).log | tail -20 + +# Or view all log files +sudo docker exec cazalla ls -lh /tmp/cazalla_mythic_*.log +``` + +### Verify Logs in Syslog (host) + +```bash +# View logs in real-time +sudo tail -f /var/log/cazalla.log + +# View recent events +sudo tail -50 /var/log/cazalla.log + +# Count events by type +sudo tail -1000 /var/log/cazalla.log | grep -o '"event_type": "[^"]*"' | sort | uniq -c +``` + +### Verify Events are Being Sent + +```bash +# View events in container logs +sudo docker logs cazalla | grep -E "(TASK|RESPONSE|CALLBACK)" | tail -10 + +# View events in syslog +sudo tail -20 /var/log/cazalla.log | grep -E "(TASK|RESPONSE|CALLBACK)" +``` + +## πŸ› οΈ Troubleshooting + +### Logger Not Initializing + +**Symptom:** You don't see "Logger initialized" in container logs. + +**Solution:** +1. Verify the logger is imported in `main.py`: + ```bash + grep -A 5 "from logger" Payload_Type/cazalla/main.py + ``` + +2. Check container logs: + ```bash + sudo docker logs cazalla | grep -i "error\|warning\|logger" + ``` + +### Syslog Not Initializing + +**Symptom:** You see "Syslog not configured" in logs. + +**Solution:** +1. Verify the configuration file exists: + ```bash + sudo docker exec cazalla cat /Mythic/logger/syslog.conf + ``` + +2. Verify `SYSLOG_SERVER` has a value (not empty): + ```bash + sudo docker exec cazalla cat /Mythic/logger/syslog.conf | grep SYSLOG_SERVER + ``` + +3. Rebuild the container if you just edited the file: + ```bash + cd /opt/mythic + sudo ./mythic-cli build cazalla + sudo ./mythic-cli restart cazalla + ``` + +### Messages Not Reaching Syslog + +**Symptom:** Logger initializes but you don't see messages in `/var/log/cazalla.log`. + +**Solution:** +1. Verify rsyslog is listening: + ```bash + sudo netstat -ulnp | grep 514 + ``` + +2. Verify connectivity from container: + ```bash + # Get host IP (docker0 bridge or real IP) + IP_HOST=$(ip addr show docker0 | grep "inet " | awk '{print $2}' | cut -d/ -f1) + echo "Host IP: $IP_HOST" + + # Test from container + sudo docker exec cazalla python3 -c " + import socket + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.sendto(b'test', ('$IP_HOST', 514)) + print('Message sent') + " + + # Verify it arrived + sleep 1 && sudo tail -1 /var/log/cazalla.log + ``` + +3. Verify rsyslog configuration: + ```bash + sudo cat /etc/rsyslog.d/50-cazalla.conf + sudo systemctl status rsyslog + ``` + +4. Verify firewall: + ```bash + sudo iptables -L -n | grep 514 + ``` + +### Logs Are Empty or Incomplete + +**Symptom:** Logs have empty fields (`"operation_id": null`, `"username": ""`). + +**Solution:** +This is normal for some events. The logger extracts all available information from the `LoggingMessage` object. If some fields are empty, it's because Mythic doesn't provide them for that specific event. + +### Configuration File Not Copied to Container + +**Symptom:** The `syslog.conf` file doesn't exist in the container. + +**Solution:** +1. Verify the file exists locally: + ```bash + ls -la Payload_Type/cazalla/logger/syslog.conf + ``` + +2. Verify `CAZALLA_USE_BUILD_CONTEXT="true"` in `/opt/mythic/.env`: + ```bash + grep CAZALLA_USE_BUILD_CONTEXT /opt/mythic/.env + ``` + +3. Rebuild the container: + ```bash + cd /opt/mythic + sudo ./mythic-cli build cazalla + ``` + +## πŸ“š Syslog Facility Codes + +| Code | Facility | Description | +|--------|----------|-------------| +| 0 | KERN | Kernel messages | +| 1 | USER | User-level messages | +| 2 | MAIL | Mail system | +| 3 | DAEMON | System daemons | +| 4 | AUTH | Security/authorization | +| 5 | SYSLOG | Messages generated by syslogd | +| 6 | LPR | Line printer subsystem | +| 7 | NEWS | Network news subsystem | +| 8 | UUCP | UUCP subsystem | +| 9 | CRON | Clock daemon | +| 10 | AUTHPRIV | Security/authorization (private) | +| 11 | FTP | FTP daemon | +| **16-23** | **LOCAL0-7** | **Reserved for local use (recommended)** | + +**Recommendation:** Use `16` (LOCAL0) for custom application logs. + +## πŸ”§ Code Structure + +``` +Payload_Type/cazalla/logger/ +β”œβ”€β”€ __init__.py # Exports cazalla_logger +β”œβ”€β”€ logger.py # CazallaLogger class + SyslogClient +β”œβ”€β”€ syslog.conf # Syslog configuration +└── README.md # This documentation +``` + +### Main Classes + +- **`CazallaLogger`**: Inherits from `mythic_container.LoggingBase.Log` + - Automatically registers with Mythic + - Implements `new_*` methods for each event type + - Formats messages as JSON + - Sends to local file and syslog + +- **`SyslogClient`**: UDP client for syslog (RFC 3164) + - Formats messages according to RFC 3164 + - Maps events to syslog severities + - Handles errors silently + +## πŸ“– References + +- [Mythic Logging Container Documentation](https://docs.mythic-c2.net/customizing/3.-consuming-containers/logging) +- [RFC 3164 - The BSD syslog Protocol](https://tools.ietf.org/html/rfc3164) +- [Mythic Example Containers](https://github.com/MythicMeta/ExampleContainers) + +## πŸ“ Important Notes + +1. **You don't need to modify docker-compose.yml** - Everything is configured from `syslog.conf` +2. **The file is copied to the container** when built, so after editing it you need to rebuild +3. **Logs are saved locally** even if syslog is configured +4. **One file per day** - No rotation, all files are kept +5. **Environment variables take precedence** over the configuration file if both are configured + +--- + +**Developed by:** Kaseya OFSTeam diff --git a/Payload_Type/cazalla/logger/__init__.py b/Payload_Type/cazalla/logger/__init__.py new file mode 100644 index 0000000..ca1c876 --- /dev/null +++ b/Payload_Type/cazalla/logger/__init__.py @@ -0,0 +1,6 @@ +# Logger package for Cazalla Mythic agent +from .logger import cazalla_logger + +# Export logger instance so it can be imported +__all__ = ['cazalla_logger'] + diff --git a/Payload_Type/cazalla/logger/logger.py b/Payload_Type/cazalla/logger/logger.py new file mode 100644 index 0000000..e95197e --- /dev/null +++ b/Payload_Type/cazalla/logger/logger.py @@ -0,0 +1,350 @@ +""" +Logging Container for Cazalla Mythic Agent + +This container subscribes to Mythic events following the official Python logging container pattern. +Based on: https://docs.mythic-c2.net/customizing/3.-consuming-containers/logging +Example: https://github.com/MythicMeta/ExampleContainers/tree/main/Payload_Type/python_services/my_logger + +First implementation: Uses built-in Mythic logger (stdout/file) +Future: Will add syslog forwarding +""" + +from mythic_container.LoggingBase import * +import logging +import json +import sys +import os +import socket +from datetime import datetime + + +class CazallaLogger(Log): + """ + Logging container for Cazalla agent + Follows the official Python logging container pattern + """ + LogToFilePath = "/tmp" # Path for file logging (empty = stdout only) + LogLevel = logging.INFO + LogMaxSizeInMB = 0 # 0 = no rotation, keep everything + LogMaxBackups = 0 # No backups needed (one file per day) + name = "cazalla_logger" + description = "Cazalla logging container - logs ALL Mythic events to daily files and syslog" + + def __init__(self): + """Initialize the logger""" + # Set up Python logging + self.mylogger = logging.getLogger('cazalla_logger') + self.mylogger.setLevel(self.LogLevel) + + # Clear any existing handlers to avoid duplicates + self.mylogger.handlers.clear() + + # Always add stdout handler + stdout_handler = logging.StreamHandler() + stdout_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + stdout_handler.setFormatter(stdout_formatter) + self.mylogger.addHandler(stdout_handler) + + # Configure file logging - one file per day (no rotation) + if self.LogToFilePath: + from pathlib import Path + log_dir = Path(self.LogToFilePath) + log_dir.mkdir(parents=True, exist_ok=True) + + # Create filename with current date: cazalla_mythic_YYYY-MM-DD.log + today = datetime.now().strftime("%Y-%m-%d") + log_file = log_dir / f'cazalla_mythic_{today}.log' + + # Use FileHandler (no rotation) - one file per day + file_handler = logging.FileHandler(log_file, mode='a', encoding='utf-8') + file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(file_formatter) + self.mylogger.addHandler(file_handler) + + self.log_file_path = str(log_file) + else: + self.log_file_path = "stdout only" + + # Initialize syslog client if configured + # Try to read from config file first, then fall back to environment variables + self.syslog_client = None + syslog_server, syslog_port, syslog_facility = self._load_syslog_config() + + if syslog_server: + try: + self.syslog_client = SyslogClient(syslog_server, syslog_port, syslog_facility) + self.mylogger.info(f"Syslog client initialized: {syslog_server}:{syslog_port} (facility={syslog_facility})") + except Exception as e: + self.mylogger.warning(f"Failed to initialize syslog client: {e}") + else: + self.mylogger.info("Syslog not configured (edit logger/syslog.conf or set SYSLOG_SERVER env var to enable)") + + # Log initialization + self.mylogger.info(f"Logger initialized: {self.name}") + self.mylogger.info(f"Log file: {self.log_file_path}") + self.mylogger.info(f"Log level: {logging.getLevelName(self.LogLevel)}") + self.mylogger.info("Subscribed to ALL events: callbacks, tasks, responses, credentials, keylogs, files, artifacts, payloads") + self.mylogger.info("Logging mode: ONE FILE PER DAY (no rotation, no deletion)") + + def _load_syslog_config(self): + """ + Load syslog configuration from config file or environment variables + Priority: 1) Config file, 2) Environment variables, 3) Defaults + Returns: (server, port, facility) + """ + # Try to read from config file first + # Try multiple possible paths + possible_paths = [ + '/Mythic/logger/syslog.conf', # Absolute path in container + os.path.join(os.path.dirname(__file__), 'syslog.conf') if '__file__' in globals() else None, + ] + # Remove None values + possible_paths = [p for p in possible_paths if p] + + config_file = None + for path in possible_paths: + if os.path.exists(path): + config_file = path + break + + # If no config file found, use first path as default + if not config_file and possible_paths: + config_file = possible_paths[0] + + syslog_server = "" + syslog_port = 514 + syslog_facility = 16 + + # Read from config file if it exists + if config_file and os.path.exists(config_file): + try: + with open(config_file, 'r') as f: + for line in f: + line = line.strip() + # Skip comments and empty lines + if not line or line.startswith('#'): + continue + # Parse key=value pairs + if '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + if key == 'SYSLOG_SERVER' and value: # Only set if value is not empty + syslog_server = value + elif key == 'SYSLOG_PORT': + try: + syslog_port = int(value) + except ValueError: + pass + elif key == 'SYSLOG_FACILITY': + try: + syslog_facility = int(value) + except ValueError: + pass + except Exception as e: + # Can't use self.mylogger here as it might not be initialized yet + print(f"Warning: Error reading syslog config file {config_file}: {e}", file=sys.stderr) + + # Override with environment variables if set (env vars take precedence) + # Only override if env var is not empty + env_syslog_server = os.getenv("SYSLOG_SERVER", "") + if env_syslog_server: # Only override if env var is set and not empty + syslog_server = env_syslog_server + if os.getenv("SYSLOG_PORT"): + try: + syslog_port = int(os.getenv("SYSLOG_PORT")) + except ValueError: + pass + if os.getenv("SYSLOG_FACILITY"): + try: + syslog_facility = int(os.getenv("SYSLOG_FACILITY")) + except ValueError: + pass + + return syslog_server, syslog_port, syslog_facility + + def _format_log_message(self, event_type: str, msg: LoggingMessage) -> str: + """Format log message with all available information""" + try: + # Extract information from LoggingMessage (fields are capitalized) + operation_id = getattr(msg, 'OperationID', None) + operation_name = getattr(msg, 'OperationName', None) + username = getattr(msg, 'OperatorUsername', None) + timestamp = getattr(msg, 'Timestamp', None) + action = getattr(msg, 'Action', None) + data = getattr(msg, 'Data', {}) + server_name = getattr(msg, 'ServerName', None) + + # Try to_json() method if available (some versions of LoggingMessage have it) + try: + if hasattr(msg, 'to_json'): + msg_dict = msg.to_json() + # Override with to_json data if available + operation_id = msg_dict.get('operation_id') or operation_id + operation_name = msg_dict.get('operation_name') or operation_name + username = msg_dict.get('username') or username + timestamp = msg_dict.get('timestamp') or timestamp + action = msg_dict.get('action') or action + data = msg_dict.get('data', data) + server_name = msg_dict.get('server_name') or server_name + except: + pass + + # Format timestamp + if timestamp: + if isinstance(timestamp, str): + formatted_timestamp = timestamp + else: + formatted_timestamp = timestamp.isoformat() if hasattr(timestamp, 'isoformat') else str(timestamp) + else: + formatted_timestamp = datetime.now().isoformat() + + # Format as structured JSON for easy parsing + log_data = { + "event_type": event_type, + "timestamp": formatted_timestamp, + "operation_id": operation_id, + "operation_name": operation_name, + "username": username, + "action": action, + "server_name": server_name, + "data": data if data else {} + } + + return json.dumps(log_data, default=str, ensure_ascii=False) + except Exception as e: + # Fallback: try to convert message to string/dict + try: + if hasattr(msg, 'to_json'): + return json.dumps({"event_type": event_type, "raw_message": msg.to_json()}, default=str) + else: + return json.dumps({"event_type": event_type, "raw_message": str(msg)}, default=str) + except: + return f'{{"event_type": "{event_type}", "error": "Failed to format message: {e}", "raw": "{str(msg)}"}}' + + async def new_callback(self, msg: LoggingMessage) -> None: + """Handle new callback events - agent checkin""" + log_msg = self._format_log_message("CALLBACK", msg) + self.mylogger.info(log_msg) + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_callback", log_msg) + + async def new_task(self, msg: LoggingMessage) -> None: + """Handle new task events - command execution""" + log_msg = self._format_log_message("TASK", msg) + self.mylogger.info(log_msg) + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_task", log_msg) + + async def new_response(self, msg: LoggingMessage) -> None: + """Handle new response events - command output""" + log_msg = self._format_log_message("RESPONSE", msg) + self.mylogger.info(log_msg) + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_response", log_msg) + + async def new_credential(self, msg: LoggingMessage) -> None: + """Handle new credential events - captured credentials""" + log_msg = self._format_log_message("CREDENTIAL", msg) + self.mylogger.info(log_msg) + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_credential", log_msg) + + async def new_keylog(self, msg: LoggingMessage) -> None: + """Handle new keylog events - captured keystrokes""" + log_msg = self._format_log_message("KEYLOG", msg) + self.mylogger.info(log_msg) + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_keylog", log_msg) + + async def new_file(self, msg: LoggingMessage) -> None: + """Handle new file events - file uploads/downloads""" + log_msg = self._format_log_message("FILE", msg) + self.mylogger.info(log_msg) + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_file", log_msg) + + async def new_artifact(self, msg: LoggingMessage) -> None: + """Handle new artifact events - IOCs and artifacts""" + log_msg = self._format_log_message("ARTIFACT", msg) + self.mylogger.warning(log_msg) # Warning level for artifacts + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_artifact", log_msg) + + async def new_payload(self, msg: LoggingMessage) -> None: + """Handle new payload events - payload creation""" + log_msg = self._format_log_message("PAYLOAD", msg) + self.mylogger.info(log_msg) + # Send to syslog if configured + if self.syslog_client: + self.syslog_client.send_syslog("new_payload", log_msg) + + +class SyslogClient: + """UDP syslog client following RFC 3164 for rsyslog integration""" + + def __init__(self, server: str, port: int = 514, facility: int = 16): + self.server = server + self.port = port + self.facility = facility + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.hostname = socket.gethostname() + self.tag = "cazalla" + + def _get_timestamp(self) -> str: + """Get current timestamp in RFC 3164 format (Mmm dd HH:MM:SS)""" + now = datetime.now() + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + return f"{months[now.month-1]} {now.day:2d} {now.hour:02d}:{now.minute:02d}:{now.second:02d}" + + def _format_syslog_message(self, severity: int, message: str) -> bytes: + """ + Format syslog message according to RFC 3164 + Format: TIMESTAMP HOSTNAME TAG: MESSAGE + """ + priority = (self.facility * 8) + severity + timestamp = self._get_timestamp() + formatted = f"<{priority}>{timestamp} {self.hostname} {self.tag}: {message}" + return formatted.encode('utf-8') + + def send_syslog(self, event_type: str, json_message: str): + """ + Send message to syslog server + + Args: + event_type: Type of event (CALLBACK, TASK, etc.) + json_message: JSON formatted message to send + """ + try: + # Map event types to syslog severity + severity_map = { + "CALLBACK": 6, # INFO + "TASK": 6, # INFO + "RESPONSE": 6, # INFO + "CREDENTIAL": 6, # INFO + "KEYLOG": 6, # INFO + "FILE": 6, # INFO + "ARTIFACT": 4, # WARNING + "PAYLOAD": 6, # INFO + } + severity = severity_map.get(event_type, 6) # Default to INFO + + # Format and send syslog message + syslog_msg = self._format_syslog_message(severity, json_message) + self.socket.sendto(syslog_msg, (self.server, self.port)) + except Exception as e: + # Don't fail if syslog send fails, just log it + print(f"[SYSLOG] Error sending message: {e}", flush=True) + + +# Register the logger - Mythic will automatically detect and register it +# The class name and inheritance from Log is what makes it work +cazalla_logger = CazallaLogger() diff --git a/Payload_Type/cazalla/logger/syslog.conf b/Payload_Type/cazalla/logger/syslog.conf new file mode 100644 index 0000000..91195ba --- /dev/null +++ b/Payload_Type/cazalla/logger/syslog.conf @@ -0,0 +1,14 @@ +# Syslog Configuration for Cazalla Logging Container +# Edit this file with your syslog server details +# After editing, restart the container: sudo ./mythic-cli restart cazalla + +# Syslog server IP or hostname (leave empty to disable syslog) +# Use the host IP (10.8.20.50) or 172.17.0.1 (docker0 bridge) to reach rsyslog on the host +SYSLOG_SERVER=10.8.20.50 + +# Syslog server port (default: 514) +SYSLOG_PORT=514 + +# Syslog facility code (0-23, default: 16 = LOCAL0) +SYSLOG_FACILITY=16 + diff --git a/Payload_Type/cazalla/main.py b/Payload_Type/cazalla/main.py index 602b4f4..d3e09d1 100644 --- a/Payload_Type/cazalla/main.py +++ b/Payload_Type/cazalla/main.py @@ -4,8 +4,23 @@ import asyncio import cazalla #import websocket.mythic.c2_functions.websocket from translator.translator import * -#from my_logger import logger + +# Import logging container - following official Python pattern +# The logger is automatically registered when the class is instantiated +try: + from logger import cazalla_logger # Import logging container + print("[MAIN] βœ“ Logging container imported and registered") +except Exception as e: + print(f"[MAIN] WARNING: Could not import logging container: {e}") + print("[MAIN] Container will continue without logging functionality") + import traceback + traceback.print_exc() + #from basic_command_augment import * #from basic_eventer.my_eventing import * +# Start Mythic services +# Note: In Go, basic_logger uses MythicServiceLogger service +# In Python, the logging registration happens during import above +# If logging needs a separate service, we may need to register it here mythic_container.mythic_service.start_and_run_forever() \ No newline at end of file diff --git a/README.md b/README.md index 3efaea7..9a6cdeb 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ - [Token Support](#-token-support) - [Context Tracking](#-context-tracking) - [OPSEC Checking](#️-opsec-checking) +- [Logging & Syslog](#-logging--syslog) - [Development](#development) - [Troubleshooting](#troubleshooting) - [Credits](#credits) @@ -1915,6 +1916,63 @@ For more information, see the [Mythic OPSEC Checking documentation](https://docs --- +## πŸ“Š Logging & Syslog + +Cazalla includes a complete logging system that records all Mythic events and sends them to a central syslog server. + +### Features + +- βœ… **Logs ALL events** automatically (callbacks, tasks, responses, credentials, keylogs, files, artifacts, payloads) +- βœ… **Does not modify the agent** (avoids detections) +- βœ… **Runs on the server**, not on the target +- βœ… **Structured JSON format** for easy parsing +- βœ… **One file per day** (no rotation, all files are kept) +- βœ… **Simple configuration** via configuration file + +### Quick Configuration + +1. **Edit the configuration file:** + ```bash + nano Payload_Type/cazalla/logger/syslog.conf + ``` + +2. **Configure the syslog server IP:** + ```bash + SYSLOG_SERVER=10.8.20.50 # Host IP or syslog server IP + SYSLOG_PORT=514 + SYSLOG_FACILITY=16 + ``` + +3. **Rebuild and restart:** + ```bash + cd /opt/mythic + sudo ./mythic-cli build cazalla + sudo ./mythic-cli restart cazalla + ``` + +### Verification + +```bash +# Verify logger is active +sudo docker logs cazalla | grep -i "syslog\|Logger initialized" + +# View logs in real-time +sudo tail -f /var/log/cazalla.log +``` + +### Log Locations + +- **Syslog (host)**: `/var/log/cazalla.log` +- **Local logs (container)**: `/tmp/cazalla_mythic_YYYY-MM-DD.log` + +### Complete Documentation + +For more details on configuration, troubleshooting, and advanced usage, see: +- [Logger Documentation](Payload_Type/cazalla/logger/README.md) - Complete technical documentation +- [Agent Wiki - Logging](documentation-payload/Cazalla/logging.md) - User guide + +--- + ## πŸ”§ Development ### Project Structure diff --git a/documentation-payload/Cazalla/_index.md b/documentation-payload/Cazalla/_index.md index 8c76091..ab496a7 100644 --- a/documentation-payload/Cazalla/_index.md +++ b/documentation-payload/Cazalla/_index.md @@ -82,6 +82,7 @@ See the [Getting Started Guide] for installation and setup instructions. - **Getting Started** - Installation and first steps - **Features Overview** - Detailed feature explanations - **OPSEC Guide** - Operational security considerations +- **Logging & Syslog** - System logging and syslog configuration - **Usage Examples** - Practical examples and use cases - **Troubleshooting** - Common issues and solutions diff --git a/documentation-payload/Cazalla/logging.md b/documentation-payload/Cazalla/logging.md new file mode 100644 index 0000000..4bd3993 --- /dev/null +++ b/documentation-payload/Cazalla/logging.md @@ -0,0 +1,281 @@ ++++ +title = "Logging & Syslog" +chapter = false +weight = 50 +pre = "6. " ++++ + +# πŸ“Š Logging & Syslog + +Cazalla includes a complete logging system that records all Mythic events and sends them to a central syslog server for analysis and auditing. + +## 🎯 Description + +Cazalla's logging system: + +- βœ… **Logs ALL events** from Mythic automatically +- βœ… **Does not modify the agent** (avoids detections) +- βœ… **Runs on the server**, not on the target +- βœ… **Sends logs to syslog** central for analysis +- βœ… **Saves local logs** as backup +- βœ… **Structured JSON format** for easy parsing + +## πŸ“‹ Logged Events + +The logger automatically captures all these events: + +| Event | Description | +|--------|-------------| +| **CALLBACK** | When an agent connects or checks in | +| **TASK** | When a task is created or updated (commands executed) | +| **RESPONSE** | Agent responses (command output) | +| **CREDENTIAL** | Captured credentials | +| **KEYLOG** | Keystrokes captured by the keylogger | +| **FILE** | Files uploaded or downloaded | +| **ARTIFACT** | Generated artifacts (IOCs, processes created, etc.) | +| **PAYLOAD** | Created payloads | + +## βš™οΈ Configuration + +### Step 1: Configure the Logger + +The logger is configured via a configuration file. **You don't need to modify docker-compose.yml**. + +1. **Edit the configuration file:** + ```bash + nano Payload_Type/cazalla/logger/syslog.conf + ``` + +2. **Configure the syslog server IP:** + ```bash + # If rsyslog is on the same host as Mythic, use the host IP + SYSLOG_SERVER=10.8.20.50 + + # Or if using Docker bridge + SYSLOG_SERVER=172.17.0.1 + + # Port (default 514) + SYSLOG_PORT=514 + + # Syslog facility (16 = LOCAL0, recommended) + SYSLOG_FACILITY=16 + ``` + +3. **Rebuild and restart the container:** + ```bash + cd /opt/mythic + sudo ./mythic-cli build cazalla + sudo ./mythic-cli restart cazalla + ``` + +### Step 2: Configure rsyslog on the Host (Optional) + +If you want to receive logs on the same server where Mythic runs: + +1. **Install rsyslog** (if not installed): + ```bash + sudo apt-get update && sudo apt-get install -y rsyslog + ``` + +2. **Enable UDP reception** in `/etc/rsyslog.conf`: + ```bash + sudo sed -i 's/#module(load="imudp")/module(load="imudp")/' /etc/rsyslog.conf + sudo sed -i 's/#input(type="imudp" port="514")/input(type="imudp" port="514")/' /etc/rsyslog.conf + ``` + +3. **Create rule for Cazalla** in `/etc/rsyslog.d/50-cazalla.conf`: + ```bash + sudo bash -c 'cat > /etc/rsyslog.d/50-cazalla.conf << EOF + # Logs from Cazalla Mythic agent + # Filter by facility LOCAL0 (16) + if \$syslogfacility-text == "local0" then /var/log/cazalla.log + & stop + EOF + ' + ``` + +4. **Restart rsyslog:** + ```bash + sudo systemctl restart rsyslog + sudo systemctl enable rsyslog + ``` + +5. **Verify it's listening:** + ```bash + sudo netstat -ulnp | grep 514 + # You should see: UNCONN ... 0.0.0.0:514 ... rsyslogd + ``` + +## πŸ“Š Verification + +### Verify Logger is Active + +```bash +# View container logs +sudo docker logs cazalla | grep -i "syslog\|Logger initialized" + +# You should see: +# INFO:cazalla_logger:Syslog client initialized: 10.8.20.50:514 (facility=16) +# INFO:cazalla_logger:Logger initialized: cazalla_logger +``` + +### View Logs in Real-Time + +```bash +# View logs in syslog (host) +sudo tail -f /var/log/cazalla.log + +# View local logs (inside container) +sudo docker exec cazalla tail -f /tmp/cazalla_mythic_$(date +%Y-%m-%d).log +``` + +### View Specific Events + +```bash +# View only tasks +sudo tail -100 /var/log/cazalla.log | grep '"event_type": "TASK"' + +# View only responses +sudo tail -100 /var/log/cazalla.log | grep '"event_type": "RESPONSE"' + +# Count events by type +sudo tail -1000 /var/log/cazalla.log | grep -o '"event_type": "[^"]*"' | sort | uniq -c +``` + +## πŸ“ Log Format + +Logs are saved in structured JSON format for easy parsing: + +```json +{ + "event_type": "TASK", + "timestamp": "2025-11-17T15:18:23.117715674Z", + "operation_id": 1, + "operation_name": "Operation Chimera", + "username": "mythic_admin", + "action": "new_task", + "server_name": "mythic", + "data": { + "id": 18, + "command_name": "shell", + "params": "{\"command\":\"whoami\"}", + "status": "success", + "callback_id": 2, + "operator_username": "mythic_admin", + ... + } +} +``` + +### Log Locations + +- **Syslog (host)**: `/var/log/cazalla.log` +- **Local logs (container)**: `/tmp/cazalla_mythic_YYYY-MM-DD.log` + - One file per day + - No rotation (all files are kept) + - Format: `cazalla_mythic_2025-11-17.log` + +## πŸ” Usage Examples + +### Search for a Specific Command + +```bash +# Search for all 'whoami' executions +sudo grep -i "whoami" /var/log/cazalla.log | grep '"event_type": "TASK"' +``` + +### View Operator Activity + +```bash +# View all events from a user +sudo grep '"username": "mythic_admin"' /var/log/cazalla.log +``` + +### View Captured Credentials + +```bash +# View credential events +sudo grep '"event_type": "CREDENTIAL"' /var/log/cazalla.log +``` + +### Analyze Generated Artifacts + +```bash +# View all artifacts (detected IOCs) +sudo grep '"event_type": "ARTIFACT"' /var/log/cazalla.log +``` + +## πŸ› οΈ Troubleshooting + +### Logger Doesn't Appear in Mythic GUI + +**Solution:** +1. Verify the logger is imported in `main.py` +2. Rebuild the container: `sudo ./mythic-cli build cazalla` +3. Restart: `sudo ./mythic-cli restart cazalla` + +### Don't See "Syslog client initialized" + +**Solution:** +1. Verify `SYSLOG_SERVER` has a value in `syslog.conf`: + ```bash + sudo docker exec cazalla cat /Mythic/logger/syslog.conf | grep SYSLOG_SERVER + ``` +2. Rebuild the container if you just edited the file + +### Messages Not Reaching Syslog + +**Solution:** +1. Verify rsyslog is listening: `sudo netstat -ulnp | grep 514` +2. Verify connectivity from container to host +3. Verify rsyslog configuration: `sudo cat /etc/rsyslog.d/50-cazalla.conf` +4. Verify firewall: `sudo iptables -L -n | grep 514` + +### Logs Are Empty + +**Solution:** +- Logs only appear when there's activity in Mythic +- Execute a command on an agent to generate events +- Verify the agent is connected and active + +## πŸ“š Advanced Configuration + +### Change Syslog Facility + +The syslog facility determines how messages are categorized. By default, `16` (LOCAL0) is used. + +To change the facility, edit `syslog.conf`: + +```bash +# Use AUTH (4) for security logs +SYSLOG_FACILITY=4 + +# Or use LOCAL1 (17) +SYSLOG_FACILITY=17 +``` + +Then update the rsyslog rule to filter by the new facility. + +### Send to Multiple Syslog Servers + +To send to multiple servers, you would need to modify the logger code or use an intermediate syslog server that forwards messages. + +### SIEM Integration + +The JSON formatted logs can be easily integrated with SIEMs such as: +- Splunk +- ELK Stack (Elasticsearch, Logstash, Kibana) +- Graylog +- QRadar + +Simply configure the SIEM to read from `/var/log/cazalla.log` or from the central syslog server. + +## πŸ”— References + +- [Logger Technical Documentation](../Payload_Type/cazalla/logger/README.md) +- [Mythic Logging Documentation](https://docs.mythic-c2.net/customizing/3.-consuming-containers/logging) +- [RFC 3164 - The BSD syslog Protocol](https://tools.ietf.org/html/rfc3164) + +--- + +**Note:** The logging system is active by default. You only need to configure the syslog server if you want to centralize logs.