diff --git a/Payload_Type/cazalla/.docker/Dockerfile b/Payload_Type/cazalla/.docker/Dockerfile new file mode 100644 index 0000000..d57c6f5 --- /dev/null +++ b/Payload_Type/cazalla/.docker/Dockerfile @@ -0,0 +1,53 @@ +# Alternative Dockerfile for local development and customization +# This Dockerfile ensures that local changes are always picked up +# Use this if the standard Dockerfile doesn't pick up your changes + +FROM itsafeaturemythic/mythic_python_base:latest + +# --- ARGs que Mythic usa al construir payloads (mantenerlos) --- +ARG CA_CERTIFICATE +ARG NPM_REGISTRY +ARG PYPI_INDEX +ARG PYPI_INDEX_URL +ARG DOCKER_REGISTRY_MIRROR +ARG HTTP_PROXY +ARG HTTPS_PROXY + +# --- Evita prompts (tzdata) y builds no reproducibles --- +ENV DEBIAN_FRONTEND=noninteractive \ + PIP_NO_CACHE_DIR=1 + +# --- Instala toolchain y utilidades necesarias para compilar agentes en C --- +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + apt-utils ca-certificates git zip make build-essential \ + libssl-dev zlib1g-dev libbz2-dev xz-utils tk-dev libffi-dev \ + liblzma-dev libsqlite3-dev protobuf-compiler mingw-w64 nasm && \ + rm -rf /var/lib/apt/lists/* + +# --- Reconfirma pip y setuptools actualizados (ya viene, pero por si acaso) --- +RUN python -m pip install --upgrade pip setuptools wheel + +# --- Instala tus dependencias (sin reinstalar mythic-container) --- +COPY requirements.txt / +RUN pip install --no-cache-dir --no-deps -r /requirements.txt + +# --- Variables de entorno necesarias para comunicación con Mythic Core --- +ENV MYTHIC_SERVER_HOST="mythic_server" +ENV MYTHIC_SERVER_PORT="17443" +ENV MYTHIC_SERVER_USERNAME="mythic_admin" +ENV MYTHIC_SERVER_PASSWORD="mythic_password" + +# --- Copia tu código de translator de forma explícita --- +# Esto asegura que todos los cambios locales se copien correctamente +WORKDIR /Mythic/ + +# Copiar estructura completa del proyecto +COPY ./ /Mythic/cazalla/ + +# Asegurar que los permisos sean correctos para Python +RUN chmod -R a+rX /Mythic/cazalla/ + +# --- Comando por defecto --- +CMD ["python3", "main.py"] + diff --git a/Payload_Type/cazalla/.dockerignore b/Payload_Type/cazalla/.dockerignore new file mode 100644 index 0000000..d6a077d --- /dev/null +++ b/Payload_Type/cazalla/.dockerignore @@ -0,0 +1,69 @@ +# Git files +.git +.gitignore +.gitattributes + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ +*.egg + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Build artifacts +*.exe +*.dll +*.o +*.obj +*.a +*.lib +build/ +*.pdb +*.ilk + +# Logs +*.log + +# Documentation (optional - remove if you want to include docs in container) +*.md +LICENSE* + +# Test files (optional - remove if you want tests in container) +test/ +tests/ +*_test.py +test_*.py + +# Temporary files +*.tmp +*.bak +*.cache + +# Mythic specific (don't copy these as they're managed by Mythic) +.env +docker-compose.yml +mythic-cli + +# Agent code build directories (will be built inside container) +cazalla/agent_code/cazalla/build/ + diff --git a/Payload_Type/cazalla/Dockerfile b/Payload_Type/cazalla/Dockerfile index ddf59fe..2c94daa 100644 --- a/Payload_Type/cazalla/Dockerfile +++ b/Payload_Type/cazalla/Dockerfile @@ -35,6 +35,7 @@ ENV MYTHIC_SERVER_USERNAME="mythic_admin" ENV MYTHIC_SERVER_PASSWORD="mythic_password" # --- Copia tu código de translator --- +# This COPY ensures local changes are included when USE_BUILD_CONTEXT=true WORKDIR /Mythic/ COPY ./ /Mythic/cazalla/ diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/procesos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/procesos.c index 0faa2b5..3b84c48 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/procesos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/procesos.c @@ -9,6 +9,9 @@ BOOL SelfIsWindowsVistaOrLater(); BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...); #include +#include + +// Nota: Removidas estructuras de PEB ya que no obtenemos command_line (como Xenon) BOOL obtenerNombreDelToken(HANDLE hProcess, char* nombreCuenta, int tamano) { HANDLE hToken; @@ -28,6 +31,8 @@ BOOL obtenerNombreDelToken(HANDLE hProcess, char* nombreCuenta, int tamano) { return result; } +// Nota: Como Xenon, no obtenemos command_line para mantener simplicidad y compatibilidad + VOID listarProcesos(PAnalizador argumentos) { SIZE_T tamanoUuid = 36; @@ -49,38 +54,44 @@ VOID listarProcesos(PAnalizador argumentos) { PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) }; if (Process32First(toolhelp, &pe)) { do { + // Como Xenon: intentar abrir proceso para obtener info adicional HANDLE hProcess = OpenProcess(SelfIsWindowsVistaOrLater() ? PROCESS_QUERY_LIMITED_INFORMATION : PROCESS_QUERY_INFORMATION, FALSE, pe.th32ProcessID); DWORD sid = -1; + + // Inicializar valores por defecto + nombreCuenta[0] = '\0'; + BOOL isWow64 = FALSE; + if (hProcess) { + // Obtener información del usuario if (!obtenerNombreDelToken(hProcess, nombreCuenta, sizeof(nombreCuenta))) { nombreCuenta[0] = '\0'; } + // Obtener Session ID if (!ProcessIdToSessionId(pe.th32ProcessID, &sid)) { sid = -1; } - BOOL isWow64 = IsWow64ProcessEx(hProcess); - - PackageAddFormatPrintf(salida, - FALSE, - "%s\t%d\t%d\t%s\t%s\t%d\n", - pe.szExeFile, - pe.th32ParentProcessID, - pe.th32ProcessID, - isWow64 ? "x86" : arch, - nombreCuenta, - sid); + // Obtener arquitectura + isWow64 = IsWow64ProcessEx(hProcess); + + CloseHandle(hProcess); + } else { + // Si no podemos abrir el proceso, intentar obtener al menos el Session ID + ProcessIdToSessionId(pe.th32ProcessID, &sid); } - else { - PackageAddFormatPrintf(salida, - FALSE, - "%s\t%d\t%d\n", - pe.szExeFile, - pe.th32ParentProcessID, - pe.th32ProcessID); - } - CloseHandle(hProcess); + + // Formato exacto como Xenon: nombre\tPPID\tPID\tarch\tuser\tsession\n + PackageAddFormatPrintf(salida, + FALSE, + "%s\t%d\t%d\t%s\t%s\t%d\n", + pe.szExeFile, + pe.th32ParentProcessID, + pe.th32ProcessID, + hProcess ? (isWow64 ? "x86" : arch) : arch, // Usar arch por defecto si no podemos abrir + nombreCuenta[0] != '\0' ? nombreCuenta : "", + sid != -1 ? sid : 0); } while (Process32Next(toolhelp, &pe)); } diff --git a/Payload_Type/cazalla/cazalla/agent_functions/ps.py b/Payload_Type/cazalla/cazalla/agent_functions/ps.py index f5a69eb..e48b8ec 100644 --- a/Payload_Type/cazalla/cazalla/agent_functions/ps.py +++ b/Payload_Type/cazalla/cazalla/agent_functions/ps.py @@ -40,5 +40,11 @@ class PsCommand(CommandBase): return response async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + """ + Process response from agent. The translator automatically converts + tab-separated process list to Process Browser JSON format. + """ resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + # Note: The translator (commands_from_implant.py) handles conversion + # from tab-separated format to Process Browser JSON format automatically return resp \ No newline at end of file diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index 63de62b..2a836df 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -86,6 +86,65 @@ def getTasking(data): return dataJson +def parse_process_list(output_text): + """ + Parsea el texto tab-separated de procesos al formato JSON del Process Browser. + Formato esperado del agente (como Xenon): nombre\\tPPID\\tPID\\tarch\\tuser\\tsession\\n + Formato requerido por Mythic Process Browser según docs: + https://docs.mythic-c2.net/customizing/hooking-features/process_list + """ + processes = [] + lines = output_text.strip().split('\n') + + for line in lines: + line = line.strip() + if not line: + continue + + parts = line.split('\t') + if len(parts) < 3: + continue # Necesitamos al menos nombre, PPID, PID + + try: + # Campos requeridos según documentación de Mythic + process_id = int(parts[2]) if len(parts) > 2 and parts[2].strip().isdigit() else None + if process_id is None: + continue # Saltar si no hay PID válido + + process = { + "process_id": process_id, + "name": parts[0] if len(parts) > 0 and parts[0].strip() else "", + } + + # Si el nombre está vacío o es inválido, saltar este proceso + if not process["name"]: + continue + + # Campos opcionales según documentación + if len(parts) > 1 and parts[1].strip().isdigit(): + process["parent_process_id"] = int(parts[1]) + + if len(parts) > 3 and parts[3].strip(): + process["architecture"] = parts[3].strip() + + if len(parts) > 4 and parts[4].strip(): + process["user"] = parts[4].strip() + + # Session ID se guarda como metadata + if len(parts) > 5 and parts[5].strip().isdigit(): + process["session_id"] = int(parts[5]) + + # Nota: No incluimos command_line como Xenon para mantener simplicidad + # Si se necesita en el futuro, se puede agregar lectura de PEB + + processes.append(process) + except Exception as e: + print(f"[ERROR] Error parseando línea de proceso: {line}, error: {e}") + continue + + return processes + + def postResponse(data): print(f"Tamaño del Base64 recibido: {len(data)} bytes") @@ -103,11 +162,51 @@ def postResponse(data): print(f"Fallo al decodificar la salida: {e}") decoded_output = "[ERROR DECODIFICANDO]" + task_id_str = uuidTask.decode('cp850') + + # Intentar detectar si es una lista de procesos (formato tab-separated) + # Formato esperado del comando ps: nombre\tPPID\tPID\tarch\tuser\tsession\n + is_process_list = False + processes = [] + + # Detectar si es una lista de procesos: formato tab-separated con al menos 3 columnas + # Formato: nombre\tPPID\tPID\t[arch]\t[user]\t[session] + if decoded_output and '\t' in decoded_output: + lines = [l.strip() for l in decoded_output.strip().split('\n') if l.strip()] + if len(lines) > 0: + # Verificar que la mayoría de líneas tengan formato de proceso + # (al menos 3 columnas: nombre, PPID, PID) + valid_lines = 0 + for line in lines[:10]: # Revisar primeras 10 líneas + parts = line.split('\t') + if len(parts) >= 3: + # Verificar que PPID (índice 1) y PID (índice 2) sean números + # y que el nombre (índice 0) no esté vacío + if (parts[1].strip().isdigit() and + parts[2].strip().isdigit() and + parts[0].strip()): + valid_lines += 1 + + # Si al menos 80% de las líneas revisadas tienen formato válido + sample_size = len(lines[:10]) + if sample_size > 0 and valid_lines >= max(1, sample_size * 0.8): + is_process_list = True + print(f"[PROCESS_BROWSER] Detectado formato de lista de procesos ({valid_lines}/{sample_size} líneas válidas), parseando...") + processes = parse_process_list(decoded_output) + print(f"[PROCESS_BROWSER] Parseados {len(processes)} procesos") + jsonTask = { - "task_id": uuidTask.decode('cp850'), - "user_output": decoded_output, + "task_id": task_id_str, "completed": True, } + + # Siempre incluir user_output para que se vea en la consola + jsonTask["user_output"] = decoded_output + + # Si detectamos una lista de procesos, también incluir el formato para Process Browser + if is_process_list and len(processes) > 0: + jsonTask["processes"] = processes + print(f"[PROCESS_BROWSER] Respuesta convertida al formato Process Browser con {len(processes)} procesos") resTaks.append(jsonTask) diff --git a/README.md b/README.md index c271ffc..164cc9b 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,87 @@ The agent will be cross-compiled for Windows using MinGW in a Docker container. --- +## 🔨 Customizing Cazalla + +If you've forked Cazalla or made local changes and want to use your customized version, you need to configure Mythic to use local builds instead of remote images. + +### Making Changes Work + +By default, Mythic may use pre-built remote images for faster deployment. However, if you've made local changes to the agent code, translator, or any Python files, you need to configure Mythic to use your local build context. + +#### Configure Build Variables + +After installing Cazalla, Mythic will add these variables to your `~/Mythic/.env` file: + +```bash +# Use local build context instead of remote image +CAZALLA_USE_BUILD_CONTEXT="true" + +# Use direct mount instead of Docker volume (recommended for development) +CAZALLA_USE_VOLUME="false" + +# Optional: Remote image URL (only used if USE_BUILD_CONTEXT is false) +CAZALLA_REMOTE_IMAGE="ghcr.io/your-org/cazalla:v1.0.0" +``` + +#### Variables Explanation + +- **`CAZALLA_USE_BUILD_CONTEXT`**: + - Set to `"true"` to build from your local `Dockerfile` and include your local changes + - Set to `"false"` (default) to use the pre-built remote image + - **Important**: You must set this to `"true"` if you've modified any files! + +- **`CAZALLA_USE_VOLUME`**: + - Set to `"false"` (default) to mount the local folder directly into the container + - Set to `"true"` to use a Docker volume instead + - Direct mount is better for development as changes are immediately visible + +- **`CAZALLA_REMOTE_IMAGE`** (optional): + - Specifies a pre-built remote image URL + - Only used when `CAZALLA_USE_BUILD_CONTEXT="false"` + +#### Apply Changes + +After modifying these variables, rebuild the container: + +```bash +cd ~/Mythic +sudo ./mythic-cli build cazalla +``` + +#### Verifying Your Changes + +1. Make your code changes +2. Set `CAZALLA_USE_BUILD_CONTEXT="true"` in `~/Mythic/.env` +3. Run `sudo ./mythic-cli build cazalla` +4. Check logs: `sudo docker logs cazalla_translator` + +If changes still don't appear: +- Verify the `Dockerfile` copies your modified files (see line 39: `COPY ./ /Mythic/cazalla/`) +- Check that file paths in the Dockerfile match your directory structure +- Ensure you're editing files in the correct location (inside `Payload_Type/cazalla/`) + +#### Example: Customizing Agent Code + +```bash +# 1. Edit agent C code +vim Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c + +# 2. Ensure build context is enabled +echo 'CAZALLA_USE_BUILD_CONTEXT="true"' >> ~/Mythic/.env + +# 3. Rebuild the container +cd ~/Mythic +sudo ./mythic-cli build cazalla + +# 4. Rebuild a payload from the UI +# Your changes will now be included in the compiled agent +``` + +For more information, see the [Mythic documentation on customizing public agents](https://docs.mythic-c2.net/customizing/customizing-public-agent). + +--- + ## 🛠️ Supported Commands ### File System Operations