Protocole WebSocket¶
Stentor utilise des connexions WebSocket pour la communication bidirectionnelle en temps réel entre les composants du système. Deux hubs WebSocket indépendants gèrent des préoccupations différentes :
- CockpitHub diffuse les événements vers les clients de l'interface utilisateur de l'opérateur (mises à jour des beacons, sortie des tâches, journaux de la console)
- RelayHub gère les connexions des agents relais (commandes, pulsations, événements C2)
Les deux hubs utilisent le cadrage de messages JSON sur des connexions WebSocket standard (RFC 6455). Cette page documente les procédures de connexion, les formats de message, les types d'événements et les mécanismes de récupération pour chaque hub.
CockpitHub (Opérateur WebSocket)¶
CockpitHub est le flux d'événements en temps réel entre le serveur Stentor et les clients du navigateur de l'opérateur. Chaque session de l'interface utilisateur de l'opérateur ouvre une connexion WebSocket pour recevoir les mises à jour des beacons, la progression des tâches, les sorties de la console et les événements système au fur et à mesure qu'ils se produisent.
Connexion¶
Point de terminaison : GET /api/v1/cockpit/ws
CockpitHub prend en charge deux méthodes d'authentification. L'authentification basée sur les tickets est l'approche recommandée : elle évite d'exposer des JWT de longue durée dans les URL WebSocket (qui peuvent apparaître dans les journaux du serveur et les caches proxy).
Tout d'abord, obtenez un ticket de courte durée en appelant l'API REST avec votre jeton d'accès :
# Step 1: Get a WebSocket ticket (valid for 30 seconds, single-use)
TICKET=$(curl -s -X POST https://stentor.app/api/v1/auth/ws-ticket \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.ticket')
# Step 2: Connect with the ticket
wscat -c "wss://stentor.app/api/v1/cockpit/ws?ticket=$TICKET"
// JavaScript: Ticket-based WebSocket connection
async function connectCockpit(apiUrl, accessToken) {
// 1. Fetch a single-use ticket
const res = await fetch(`${apiUrl}/v1/auth/ws-ticket`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const { ticket } = await res.json();
// 2. Connect using the ticket (not the JWT)
const wsUrl = apiUrl.replace(/^http/, 'ws');
const ws = new WebSocket(`${wsUrl}/v1/cockpit/ws?ticket=${ticket}`);
ws.onopen = () => console.log('Connected to CockpitHub');
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
console.log(`[seq=${event.seq}] ${event.type}:`, event.payload);
};
return ws;
}
Propriétés du billet
- A usage unique : Chaque ticket ne peut être utilisé qu'une seule fois (consommé à la validation)
- ** TTL de 30 secondes : ** les tickets expirent 30 secondes après la génération
- Opaque : Les tickets sont des chaînes hexadécimales de 64 caractères (32 octets aléatoires)
- Le nettoyage en arrière-plan s'exécute toutes les 60 secondes pour purger les tickets expirés
Transmettez le jeton d'accès directement en tant que paramètre de requête :
Avis de dépréciation
Le paramètre ?token= est obsolète. Les JWT dans les URL peuvent fuir via les journaux d'accès au serveur, l'historique du navigateur et les en-têtes de référent HTTP. Migrez vers l’authentification basée sur les tickets.
Réponses aux erreurs¶
Si l'authentification échoue, le serveur répond avec une erreur JSON avant la mise à niveau de WebSocket :
| Coder | Erreur | Description |
|---|---|---|
TICKET_EXPIRED | ticket expired | TTL du ticket dépassé (30 s) |
TICKET_NOT_FOUND | ticket not found | Billet déjà utilisé ou n'a jamais existé |
TICKET_INVALID | invalid ticket | Erreur de validation du billet |
TOKEN_EXPIRED | token expired | Le jeton d'accès JWT a expiré |
TOKEN_INVALID_SIGNATURE | invalid token signature | La vérification de la signature JWT a échoué |
TOKEN_WRONG_TYPE | invalid token type | Le jeton n'est pas un jeton d'accès |
TOKEN_INVALID | invalid token | Échec général de la validation JWT |
AUTH_MISSING | ticket or token required | Aucune authentification fournie |
Format des messages¶
Tous les messages de CockpitHub sont des SequencedEvents : chaque événement est entouré d'un numéro de séquence croissant de manière monotone :
{
"seq": 42,
"type": "beacon_update",
"payload": {
"beacon_id": "a1b2c3d4-...",
"hostname": "WORKSTATION-01",
"username": "CORP\\jsmith",
"status": "active"
}
}
| Champ | Type | Description |
|---|---|---|
seq | uint64 | Numéro de séquence croissant de manière monotone (commence à 1) |
type | string | Identifiant du type d'événement (voir Types d'événements ci-dessous) |
payload | object | Payload spécifique au type (varie selon le type d'événement) |
Le champ seq permet aux clients de détecter les messages supprimés. Si votre client reçoit seq=40 suivi de seq=43, les séquences 41 et 42 ont été abandonnées (voir Drop Recovery).
Types d'événements¶
CockpitHub diffuse les types d'événements suivants à tous les clients opérateurs connectés.
beacon_update¶
L'état d'un beacon a changé (nouvelle beacon enregistrée, enregistrement ou changement de statut).
{
"seq": 1,
"type": "beacon_update",
"payload": {
"beacon_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"machine_ip": "10.0.0.20",
"hostname": "WORKSTATION-01",
"username": "CORP\\jsmith",
"pid": 4832,
"last_seen": "2026-02-19T10:00:00Z",
"status": "active"
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
beacon_id | string | UUID du beacon |
machine_ip | string | Adresse IP de la machine cible |
hostname | string | Nom d'hôte cible |
username | string | Contexte utilisateur (DOMAIN\format utilisateur) |
pid | int | ID de processus du beacon |
last_seen | string | Horodatage ISO 8601 du dernier enregistrement |
status | string | "active" ou "lost" |
console_log¶
Une entrée de journal pour la console opérateur (entrée/sortie de commande, messages d'information, erreurs).
{
"seq": 2,
"type": "console_log",
"payload": {
"tab_id": "beacon-a1b2c3d4",
"timestamp": "2026-02-19T10:00:05Z",
"type": "output",
"text": "Directory listing of C:\\Users\\jsmith\\Desktop..."
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
tab_id | string | Onglet de la console cible : "event-log" pour le journal global, "beacon-{id}" pour les beacons spécifiques |
timestamp | string | Horodatage ISO 8601 |
type | string | Type d'entrée : "input", "output", "info", "success", "error" |
text | string | Contenu du message du journal |
task_started¶
Une tâche a été envoyée à un beacon pour exécution.
{
"seq": 3,
"type": "task_started",
"payload": {
"task_id": "f1e2d3c4-...",
"beacon_id": "a1b2c3d4-...",
"technique": "shell",
"status": "started"
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
task_id | string | UUID de la tâche |
beacon_id | string | UUID du beacon cible |
technique | string | Nom de la commande/technique (par exemple, "shell", "ls", "hashdump") |
status | string | Toujours "started" pour cet événement |
task_completed¶
Une tâche a terminé son exécution sur un beacon.
{
"seq": 4,
"type": "task_completed",
"payload": {
"task_id": "f1e2d3c4-...",
"beacon_id": "a1b2c3d4-...",
"technique": "shell",
"status": "completed",
"output": "Volume in drive C has no label..."
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
task_id | string | UUID de la tâche |
beacon_id | string | UUID du beacon cible |
technique | string | Nom de la commande/technique |
status | string | "completed" ou "failed" |
output | string | Sortie de commande (vide si aucune sortie) |
pivot_link¶
Un lien de mouvement latéral a été établi entre deux machines (représenté sous forme d'arêtes dans le graphe pivot).
{
"seq": 5,
"type": "pivot_link",
"payload": {
"from": "machine-001",
"to": "machine-002",
"type": "pivot",
"method": "smb"
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
from | string | ID du nœud source |
to | string | ID du nœud cible |
type | string | Type de lien : "initial" (lien C2) ou "pivot" (mouvement latéral) |
method | string | Méthode de déplacement : "smb", "wmi", "winrm", "dcom" (vide pour les liens initiaux) |
lateral_movement_completed¶
Une opération de mouvement latéral a commencé, s'est terminée ou a échoué (déclenche des animations de bord dans la vue topologique).
{
"seq": 6,
"type": "lateral_movement_completed",
"payload": {
"task_id": "f1e2d3c4-...",
"source_beacon_id": "a1b2c3d4-...",
"source_machine_id": "machine-001",
"target_ip": "10.10.10.50",
"method": "psexec",
"status": "completed"
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
task_id | string | UUID de la tâche |
source_beacon_id | string | Beacon initiant le mouvement |
source_machine_id | string | Identifiant de la machine source |
target_ip | string | IP de la machine cible |
method | string | Méthode de mouvement latéral (par exemple, "psexec", "wmi", "winrm", "dcom") |
status | string | "started", "completed" ou "failed" |
shell_output¶
Sortie en temps réel d'une session shell interactive (routes vers l'onglet de terminal xterm.js correct).
{
"seq": 7,
"type": "shell_output",
"payload": {
"beacon_id": "a1b2c3d4-...",
"output": "C:\\Users\\jsmith> ",
"is_error": false,
"timestamp": "2026-02-19T10:01:00Z"
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
beacon_id | string | Beacon UUID (achemine la sortie vers l'onglet correct du terminal) |
output | string | Texte de sortie du shell |
is_error | bool | S'il s'agit d'une sortie stderr |
timestamp | string | Horodatage ISO 8601 |
file_transfer_progress¶
Mise à jour de la progression pour une opération de téléchargement ou de téléchargement de fichier en cours.
{
"seq": 8,
"type": "file_transfer_progress",
"payload": {
"transfer_id": "t1r2a3n4-...",
"beacon_id": "a1b2c3d4-...",
"direction": "download",
"filename": "secrets.docx",
"progress": 0.65,
"bytes_done": 6553600,
"bytes_total": 10085376,
"status": "in_progress",
"sha256": ""
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
transfer_id | string | Identifiant de transfert unique (corrèle les événements de progression) |
beacon_id | string | UUID du beacon |
direction | string | "download" (beacon vers serveur) ou "upload" (serveur vers beacon) |
filename | string | Nom de fichier d'origine |
progress | float | Taux d'achèvement (0,0 à 1,0) |
bytes_done | int64 | Octets transférés jusqu'à présent |
bytes_total | int64 | Taille totale du fichier en octets |
status | string | "in_progress", "completed" ou "failed" |
sha256 | string | Hachage du fichier (rempli à la fin) |
beacon_highlight_update¶
Un opérateur a modifié la couleur de surbrillance d'un beacon (repère visuel de collaboration).
{
"seq": 9,
"type": "beacon_highlight_update",
"payload": {
"beacon_id": "a1b2c3d4-...",
"color": "#ef4444"
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
beacon_id | string | UUID du beacon |
color | string | Valeur de couleur CSS (hexadécimale) ou chaîne vide lorsque la surbrillance est supprimée |
dialog_request¶
Un script CNA demande l'entrée de l'opérateur via une invite de dialogue (prompt_text, prompt_confirm, prompt_file, dialog_show).
{
"seq": 10,
"type": "dialog_request",
"payload": {
"dialog_type": "prompt_text",
"title": "Enter target username",
"message": "Provide the username for lateral movement:",
"default_value": "administrator"
}
}
navigate¶
Un script CNA demande la navigation vers une page spécifique (openBeaconConsole, openPayloadDialog, etc.).
{
"seq": 11,
"type": "navigate",
"payload": {
"target": "beacon_console",
"beacon_id": "a1b2c3d4-..."
}
}
cna_tab¶
Un script CNA crée un onglet de visualisation personnalisé (addTab, showVisualization).
{
"seq": 12,
"type": "cna_tab",
"payload": {
"tab_id": "custom-viz-1",
"title": "Attack Graph",
"content_type": "html",
"content": "<div>...</div>"
}
}
siem_alert¶
Une alerte SIEM transmise aux clients opérateurs pour une sensibilisation en temps réel.
{
"seq": 13,
"type": "siem_alert",
"payload": {
"rule": "Suspicious PowerShell execution",
"severity": "high",
"host": "WORKSTATION-01",
"timestamp": "2026-02-19T10:02:00Z"
}
}
dropped_messages¶
Envoyé lorsque le serveur détecte que des messages ont été supprimés pour ce client (voir Drop Recovery).
{
"seq": 50,
"type": "dropped_messages",
"payload": {
"last_sent_seq": 42,
"current_seq": 50,
"dropped_count": 8
}
}
Schéma de payload
| Champ | Type | Description |
|---|---|---|
last_sent_seq | uint64 | Dernier numéro de séquence envoyé avec succès à ce client |
current_seq | uint64 | Numéro de séquence du serveur actuel |
dropped_count | uint64 | Nombre de messages supprimés |
Récupération de chute¶
CockpitHub utilise un tampon d'envoi limité (1024 messages par client). Lorsque le tampon d'un client est plein (par exemple, une connexion lente ou un volume d'événements important), le serveur ne peut pas fournir de nouveaux événements. Au lieu de supprimer silencieusement les messages, le serveur :
- Envoie un événement
dropped_messagesavec la plage d'intervalle (last_sent_seq à current_seq) - Si même la notification de suppression ne peut pas être envoyée, le client est déconnecté
Lorsque votre client reçoit un événement dropped_messages, la stratégie de récupération recommandée est la suivante :
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
if (event.type === 'dropped_messages') {
console.warn(
`Dropped ${event.payload.dropped_count} messages ` +
`(seq ${event.payload.last_sent_seq + 1} - ${event.payload.current_seq})`
);
// Full data refresh -- re-fetch all mutable state from REST API
refreshBeacons();
refreshTasks();
refreshCredentials();
return;
}
// Normal event processing
handleEvent(event);
};
Suivi du numéro de séquence
Suivez la dernière valeur seq reçue. A la reconnexion, comparez-la avec la séquence actuelle du serveur (disponible via l'API REST) pour déterminer si une actualisation est nécessaire.
Messages client à serveur¶
CockpitHub accepte également les messages des clients connectés. Le ReadPump traite les messages JSON entrants et les achemine par type.
tunnel_local_response¶
Trames de tunnel renvoyées depuis les navigateurs de l'opérateur pour rportfwd_local (transfert de port inversé via le réseau de l'opérateur).
{
"type": "tunnel_local_response",
"beacon_id": "a1b2c3d4-...",
"bind_port": 8080,
"frames": [
{
"conn_id": 1,
"type": 2,
"data": "<base64>"
}
]
}
| Champ | Type | Description |
|---|---|---|
type | string | Toujours "tunnel_local_response" |
beacon_id | string | UUID de beacon qui possède le port inverse avant |
bind_port | uint16 | Port de liaison local sur la machine de l'opérateur |
frames | array | Trames de tunnel (données de connexion revenant au beacon) |
RelayHub (Relais WebSocket)¶
RelayHub gère les connexions WebSocket persistantes entre le serveur Stentor et les agents de relais. Les relais sont des processus Linux (généralement exécutés sur Kali) qui hébergent les listeners C2, traitent les enregistrements de beacons et exécutent la génération de payload.
Connexion¶
Point de terminaison : GET /ws/relay
Les relais s'authentifient à l'aide d'un secret partagé et de leur UUID enregistré. Les deux peuvent être fournis via des en-têtes (de préférence) ou des paramètres de requête.
Conditions préalables¶
- Le relais doit exister dans la base de données -- Le serveur valide l'UUID du relais par rapport à la table
relays. Si le relais n'est pas enregistré, la connexion renvoie HTTP 404. - Le secret partagé doit correspondre -- L'en-tête
X-Relay-Secret(ou le paramètre de requêtesecret) doit correspondre à la variable d'environnementRELAY_SECRETconfigurée sur le serveur.
Réponses aux erreurs¶
| Statut | Erreur | Description |
|---|---|---|
| 400 | relay_id required | Aucun identifiant de relais fourni |
| 400 | invalid relay_id format | L'ID de relais n'est pas un UUID valide |
| 401 | invalid relay secret | Le secret ne correspond pas à la configuration du serveur |
| 404 | relay not found | UUID du relais non enregistré dans la base de données |
| 500 | internal error | Échec de la requête de base de données |
Inscription à la base de données requise
Après une reconstruction de la base de données, les enregistrements de relais sont perdus. Vous devez réinsérer le relais dans la table relays avant que le relais puisse se reconnecter :
Format des messages¶
Tous les messages RelayHub utilisent l'enveloppe RelayMessage :
{
"type": "command",
"id": "550e8400-e29b-41d4-a716-446655440000",
"correlation_id": null,
"timestamp": "2026-02-19T10:00:00Z",
"payload": { ... }
}
| Champ | Type | Description |
|---|---|---|
type | string | Type de message : "command", "response", "heartbeat", "event", "ack" |
id | uuid | Identifiant unique du message |
correlation_id | uuid? | Pour les réponses : le id de la commande d'origine (permet la correspondance requête/réponse) |
timestamp | string | Horodatage de création ISO 8601 |
payload | object | Payload spécifique au type (voir les sections ci-dessous) |
Types de messages¶
command (backend vers relais)¶
Les commandes sont envoyées du serveur à un relais pour lancer des actions (démarrer les listeners, mettre les tâches en file d'attente, générer des payloads).
{
"type": "command",
"id": "cmd-uuid",
"timestamp": "2026-02-19T10:00:00Z",
"payload": {
"command": "start_listener",
"args": {
"listener_id": "6ea88162-...",
"name": "HTTPS Relay",
"type": "https",
"bind_address": "0.0.0.0",
"port": 8443
}
}
}
Schéma de payload (RelayCommandPayload) :
| Champ | Type | Description |
|---|---|---|
command | string | Nom de la commande (voir Référence de la commande) |
args | object | Arguments spécifiques à la commande (objet JSON) |
response (relais vers backend)¶
Les réponses portent le résultat d'une commande reçue précédemment. Le correlation_id relie la réponse au id de la commande d'origine.
{
"type": "response",
"id": "resp-uuid",
"correlation_id": "cmd-uuid",
"timestamp": "2026-02-19T10:00:01Z",
"payload": {
"success": true,
"error": "",
"data": { "listener_id": "6ea88162-...", "status": "running" }
}
}
Schéma de payload (RelayResponsePayload) :
| Champ | Type | Description |
|---|---|---|
success | bool | Si la commande a réussi |
error | string | Message d'erreur (vide en cas de succès) |
data | object? | Données de réponse (spécifiques à la commande, présentes en cas de succès) |
heartbeat (bidirectionnel)¶
Messages de surveillance de l'état envoyés dans les deux sens pour maintenir la connexion et signaler l'état du relais.
{
"type": "heartbeat",
"id": "hb-uuid",
"timestamp": "2026-02-19T10:00:30Z",
"payload": {
"relay_id": "aaaaaaaa-...",
"status": "idle"
}
}
Schéma de payload (RelayHeartbeatPayload) :
| Champ | Type | Description |
|---|---|---|
relay_id | string | UUID de l'instance de relais |
status | string | État actuel du relais : "idle", "busy" ou "error" |
event (relais vers backend)¶
Événements asynchrones du relais vers le serveur (enregistrements de beacons, réalisations de tâches, activité de phishing).
{
"type": "event",
"id": "evt-uuid",
"timestamp": "2026-02-19T10:01:00Z",
"payload": {
"event": "beacon_new",
"data": {
"beacon_id": "a1b2c3d4-...",
"hostname": "WORKSTATION-01",
"username": "jsmith",
"ip": "10.0.0.20",
"os": "Windows 10",
"arch": "x64",
"pid": 4832
}
}
}
Schéma de payload (RelayEventPayload) :
| Champ | Type | Description |
|---|---|---|
event | string | Nom de l'événement (voir Référence de l'événement) |
data | object | Données spécifiques à l'événement |
ack (relais vers backend)¶
Accusé de réception envoyé par le relais immédiatement à la réception d'une commande, avant le début de son exécution. Cela confirme la réception tandis que le résultat réel arrive plus tard sous la forme d'un response.
{
"type": "ack",
"id": "ack-uuid",
"timestamp": "2026-02-19T10:00:00Z",
"payload": {
"command_id": "cmd-uuid",
"received": "2026-02-19T10:00:00Z"
}
}
Schéma de payload (RelayAckPayload) :
| Champ | Type | Description |
|---|---|---|
command_id | uuid | ID de la commande en cours d'accusé de réception |
received | string | Horodatage ISO 8601 lors de la réception de la commande |
Référence des commandes¶
Commandes envoyées depuis le serveur Stentor aux agents relais. Chaque commande est enveloppée dans un RelayCommandPayload avec le nom de la commande et les arguments.
Gestion des listeners¶
| Commande | Description | Arguments clés |
|---|---|---|
start_listener | Démarrer un listener C2 sur le relais | listener_id, name, type, bind_address, port, profile_name, tls_cert, tls_key, dns_domain, smb_pipe_name |
stop_listener | Arrêter un listener en cours d'exécution | listener_id |
host_file | Héberger un fichier sur un URI personnalisé sur un listener | listener_id, uri, data, content_type, filename |
unhost_file | Supprimer un fichier hébergé d'un listener | listener_id, uri |
Arguments start_listener (StartListenerArgs)
| Champ | Type | Description |
|---|---|---|
listener_id | string | UUID du listener |
name | string | Nom d'affichage |
type | string | Protocole : "http", "https", "dns", "smb_pipe" |
bind_address | string | Adresse d'écoute (par exemple, "0.0.0.0") |
port | int | Port d'écoute |
profile_name | string? | Nom du profil malléable C2 |
profile_config | object? | Configuration du profil JSON |
tls_cert | string? | Certificat TLS PEM (pour HTTPS) |
tls_key | string? | Clé privée TLS PEM (pour HTTPS) |
dns_domain | string? | Domaine DNS (pour les listeners DNS) |
dns_ttl | int? | TTL de l'enregistrement DNS |
dns_resolver | string? | Résolveur DNS personnalisé |
smb_pipe_name | string? | Nom du canal nommé (pour les listeners SMB) |
beacon_port | int? | Remplacement du port pour le rappel du beacon |
guardrails | object? | Règles de garde-fou d’exécution |
wg_private_key | string? | Clé privée WireGuard (pour VPN) |
wg_peer_public_key | string? | Clé publique homologue WireGuard |
wg_tunnel_ip | string? | IP du tunnel WireGuard |
wg_peer_tunnel_ip | string? | IP du tunnel homologue WireGuard |
wg_c2_port | int? | Port WireGuard C2 |
Opérations de tâches¶
| Commande | Description | Arguments clés |
|---|---|---|
queue_task | Mettre en file d'attente une tâche pour la livraison du beacon lors du prochain enregistrement | beacon_id, task_type, task_data |
tunnel | Envoyer des trames de tunnel SOCKS à un beacon | beacon_id, frames[] |
covertvpn | Gérer l'interface VPN TAP secrète | (Arguments spécifiques au VPN) |
Arguments queue_task (QueueTaskPayload)
| Champ | Type | Description |
|---|---|---|
task_id | uuid? | Facultatif : ID de tâche fourni par le backend pour la corrélation |
beacon_id | uuid | UUID du beacon cible |
task_type | string | Type de tâche (par exemple, "shell", "upload", "inject") |
task_data | object | Arguments spécifiques à la tâche |
Génération de payload¶
| Commande | Description | Arguments clés |
|---|---|---|
generate_payload | Générer un binaire de payload (EXE, DLL, shellcode, etc.) | type, c2_url, obfuscate, profile, evasion_options, architecture |
generate_clr_shellcode | Générer le shellcode du chargeur CLR pour PowerShell | assembly, script, arch |
generate_assembly_shellcode | Générer du shellcode pour l'exécution de l'assembly .NET | assembly, args, arch |
generate_browser_pivot_shellcode | Générer le shellcode du proxy WinINet | pipe_name, arch |
sign_payload | Authenticode-signer un binaire de payload | payload, cert_pfx, password |
generate_payload Arguments (GeneratePayloadArgs)
| Champ | Type | Description |
|---|---|---|
type | string | Type de payload : "exe", "dll", "shellcode", "lnk", "docm", "xlsm", "iso", "hta", "html_smuggling", "stager", "service", "msi" |
c2_url | string | URL de rappel C2 |
obfuscate | bool | Utilisez garble pour un hachage binaire unique par build |
profile | string | Nom du profil malléable C2 |
architecture | string? | Arc cible : "x64", "x86" |
sleep | int? | Intervalle de veille du beacon (secondes) |
jitter | int? | Pourcentage de gigue (0-100) |
evasion_options | object? | Contournement AMSI, patch ETW, basculement d'obscurcissement du sommeil |
guardrail_config | object? | Restrictions d'exécution (IP, nom d'hôte, domaine, modèles de nom d'utilisateur) |
kill_date | string? | Date d'expiration de le payload RFC 3339 |
exit_func | string? | Fonction de sortie : "process" ou "thread" |
Configurations spécifiques au type supplémentaires : iso_config, hta_config, html_smuggling_config, stager_config, dll_config, service_config, shellcode_config, msi_config, udrl_config, drip_config, proxy_config, host_rotation_config, sleep_mask_config, udc2_config.
Opérations de phishing¶
| Commande | Description | Arguments clés |
|---|---|---|
send_email | Envoyer un email de phishing via le relais | template_id, from_email, to_email, tracking_id, tracking_base_url |
preview_email | Aperçu d'un modèle d'e-mail rendu | template_id, target_email |
host_payload | Hébergez un payload sur le serveur de suivi | payload_id, data, content_type |
Référence d'événement¶
Événements envoyés par les agents relais au serveur Stentor. Chaque événement est enveloppé dans un RelayEventPayload.
Événements du cycle de vie des beacons¶
| Événement | Description | Champs de données clés |
|---|---|---|
beacon_new | Nouvelle beacon enregistrée depuis une cible | beacon_id, hostname, username, ip, os, arch, pid, listener_id |
beacon_checkin | Beacon existante enregistrée | beacon_id, hostname, username, ip, os, arch, pid |
beacon_dead | Beacon a manqué les enregistrements et est marqué comme mort | beacon_id |
beacon_new / beacon_checkin Payload (BeaconEventPayload)
| Champ | Type | Description |
|---|---|---|
beacon_id | uuid | UUID du beacon |
hostname | string | Nom d'hôte cible |
username | string | Contexte utilisateur |
ip | string | Adresse IP cible |
os | string | Système d'exploitation (par exemple, "Windows 10") |
arch | string | Architecture ("x64", "x86") |
pid | int | ID de processus |
listener_id | string? | UUID du listener d'origine |
Événements de tâches¶
| Événement | Description | Champs de données clés |
|---|---|---|
task_complete | Exécution de la tâche terminée sur un beacon | task_id, beacon_id, success, output, error, artifacts[] |
??? remarque « payload task_complete (TaskCompletePayload) »
| Champ | Type | Description |
|-------|------|-------------|
| `task_id` | `uuid` | UUID de la tâche |
| `beacon_id` | `uuid` | Beacon qui a exécuté la tâche |
| `task_type` | `string?` | Type de tâche (facultatif pour la compatibilité ascendante) |
| `success` | `bool` | Si l'exécution a réussi |
| `output` | `string` | Sortie de commande |
| `error` | `string?` | Message d'erreur (en cas d'échec) |
| `artifacts` | `array?` | Artefacts médico-légaux produits (fichiers, clés de registre) |
Événements du cycle de vie des listeners¶
| Événement | Description | Champs de données clés |
|---|---|---|
listener_started | L'listener a démarré avec succès | listener_id, status, port |
listener_stopped | L'listener s'est arrêté | listener_id, status |
listener_error | L'listener a rencontré une erreur | listener_id, status, error |
??? remarque « Payload du statut d'écoute (ListenerStatusPayload) »
| Champ | Type | Description |
|-------|------|-------------|
| `listener_id` | `string` | UUID du listener |
| `status` | `string` | `"running"`, `"stopped"` ou `"error"` |
| `error` | `string?` | Description de l'erreur (pour `listener_error`) |
| `port` | `int?` | Port lié réel (pour `listener_started`) |
Événements dans les tunnels¶
| Événement | Description | Champs de données clés |
|---|---|---|
tunnel_response | Cadres de tunnel SOCKS à partir d'un beacon | beacon_id, frames[] |
??? remarque « Payload d'événement de tunnel (TunnelEventPayload) »
| Champ | Type | Description |
|-------|------|-------------|
| `beacon_id` | `uuid` | UUID du beacon |
| `frames` | `array` | Tableau d'objets TunnelFrame |
**Cadre tunnel :**
| Champ | Type | Description |
|-------|------|-------------|
| `conn_id` | `uint32` | ID du multiplexeur de connexion |
| `type` | `uint8` | Type de trame : `1`=connexion, `2`=données, `3`=fermer, `4`=erreur |
| `data` | `bytes` | Payload (adresse pour la connexion, octets bruts pour les données) |
Événements de garde-corps¶
| Événement | Description | Champs de données clés |
|---|---|---|
guardrail_violation | Une règle de garde-fou s'est déclenchée sur le relais | Détails des violations spécifiques aux règles |
Événements de phishing¶
| Événement | Description | Champs de données clés |
|---|---|---|
email_sent | E-mail de phishing envoyé avec succès | tracking_id, to_email, timestamp, success |
email_opened | Pixel de suivi chargé par cible | tracking_id, timestamp, user_agent, ip |
link_clicked | Lien de phishing cliqué par cible | tracking_id, timestamp, user_agent, ip |
creds_captured | Identifiants capturés à partir de la page de destination | tracking_id, username, password, timestamp |
payload_downloaded | Fichier de payload téléchargé par cible | payload_id, timestamp, user_agent, ip |
keystroke_captured | Frappes capturées à partir du site cloné | tracking_id, keystrokes, timestamp |
Événements de l’AIIC¶
| Événement | Description | Champs de données clés |
|---|---|---|
profiler_hit | Visite du profileur système collectée | external_ip, internal_ip, user_agent, platform, applications, token |
web_hit | Requête HTTP servie par le serveur Web | method, uri, remote_addr, user_agent, response_code, response_size, handler |
Commandes synchrones¶
Certaines opérations nécessitent d'attendre la réponse d'un relais avant de continuer (par exemple, la génération de payload doit renvoyer les données binaires). Le mécanisme SendCommandSync implémente une requête/réponse synchrone sur le canal WebSocket asynchrone.
sequenceDiagram
participant Server as Stentor Server
participant Relay as Relay Agent
Server->>Server: Create command with unique ID
Server->>Server: Register pending request (ID -> response channel)
Server->>Relay: Send command message
Relay->>Server: Send ack (command_id)
Note over Relay: Execute command...
Relay->>Server: Send response (correlation_id = command ID)
Server->>Server: Match correlation_id to pending request
Server->>Server: Deliver response via channel
Note over Server: Returns data to caller Gestion du délai d'attente : Si le relais ne répond pas dans le délai d'attente spécifié (généralement 30 à 60 secondes pour la génération de payload), la requête renvoie une erreur ErrRelayTimeout. La demande en attente est nettoyée quel que soit le résultat.
Corrélation : Le relais inclut le id de la commande d'origine comme correlation_id dans sa réponse. La fonction handleMessage du serveur vérifie les réponses entrantes par rapport à la carte pendingRequests : si une correspondance est trouvée, la réponse est transmise à la goroutine en attente et l'entrée en attente est supprimée.
Cycle de vie de la connexion¶
Cycle de vie du CockpitHub¶
sequenceDiagram
participant Client as Operator Browser
participant API as REST API
participant Hub as CockpitHub
Client->>API: POST /v1/auth/login
API-->>Client: access_token
Client->>API: POST /v1/auth/ws-ticket
Note over Client: Authorization: Bearer <token>
API-->>Client: { ticket: "a3f2..." }
Client->>Hub: GET /v1/cockpit/ws?ticket=a3f2...
Hub->>Hub: Validate ticket (single-use, 30s TTL)
Hub-->>Client: 101 Switching Protocols
Hub->>Hub: Register client, fire event_join CNA event
loop Real-time Events
Hub->>Client: SequencedEvent { seq, type, payload }
end
Hub->>Client: Ping (every 54s)
Client->>Hub: Pong (must respond within 60s)
alt Client Send Buffer Full
Hub->>Client: dropped_messages event
Note over Client: Re-fetch all data from REST API
end
alt Disconnection
Hub->>Hub: Unregister client, fire event_quit CNA event
Hub->>Hub: Clean up operator tunnel forwards
end Stratégie de reconnexion : Lorsque la connexion WebSocket est interrompue, les clients doivent :
- Attendez brièvement (intervalle exponentiel avec gigue : 1 s initiales, 30 s maximum)
- Demander un nouveau billet via
POST /v1/auth/ws-ticket - Reconnectez-vous avec le nouveau billet
- Comparez le dernier
seqreçu avec la séquence actuelle du serveur - S'il y a un écart, effectuez une actualisation complète des données
Cycle de vie du RelayHub¶
sequenceDiagram
participant DB as PostgreSQL
participant Relay as Relay Agent
participant Hub as RelayHub
Note over DB: Relay must be registered in relays table
Relay->>Hub: GET /ws/relay
Note over Relay: X-Relay-ID + X-Relay-Secret headers
Hub->>Hub: Validate secret
Hub->>DB: GetByID(relay_uuid)
DB-->>Hub: Relay record (or 404)
Hub-->>Relay: 101 Switching Protocols
Hub->>DB: UpdateStatus(relay_id, "connected")
Hub->>Hub: Register client (replaces existing if same ID)
loop Heartbeat Loop
Relay->>Hub: heartbeat { relay_id, status: "idle" }
end
loop Commands & Events
Hub->>Relay: command { command: "start_listener", args: {...} }
Relay->>Hub: ack { command_id: "..." }
Relay->>Hub: response { correlation_id: "...", success: true }
Relay->>Hub: event { event: "beacon_new", data: {...} }
end
alt Disconnection
Hub->>Hub: Unregister client
Hub->>DB: UpdateStatus(relay_id, "disconnected")
end Reconnexion du relais : Si le processus de relais redémarre ou si la connexion WebSocket est interrompue :
- Le relais tente de se reconnecter avec le même UUID et le même secret
- Si un client avec le même identifiant est déjà enregistré, l'ancienne connexion est remplacée
- L'état du relais est mis à jour à "connecté" dans la base de données
- Les listeners actifs doivent être redémarrés après la reconnexion (l'état n'est pas préservé)