
Kali365 - Réplique EvilTokens
Un serveur de phishing par code d'appareil pour l'émulation d'adversaires. Capture les jetons OAuth Microsoft 365 via le flux Device Authorization Grant. Inspiré par GraphSpy.
Avertissement — Utilisation autorisée uniquement. Cet outil est destiné aux testeurs d'intrusion et aux red teamers opérant sous autorisation écrite explicite. Une mauvaise utilisation peut violer les lois sur la fraude informatique.
microsoft.com/devicelogin.Backend Python (un port par campagne) + front-end nginx + persistance SQLite.
main_server.py ← unified entry point (run this)
db.py ← SQLite persistence layer
html_enroll.py ← victim enrollment page builder
html_search.py ← victim post-auth search page builder
css_shared.py ← shared CSS for operator UI
nginx_helper.py ← auto-generates and reloads nginx reverse-proxy config
nginx.conf ← base nginx config (includes /etc/nginx/conf.d/*.conf)
Lorsqu'une campagne est créée avec un domaine défini, nginx_helper.py écrit automatiquement un bloc serveur par domaine dans NGINX_CONF (/etc/nginx/conf.d/easytokens.conf par défaut) et recharge nginx. Le trafic des victimes arrivant sur le port 80 pour ce domaine est proxyfié vers l'instance Python correcte.
nginx.conf de base rejette toute requête dont l'en-tête Host ne correspond pas à une campagne connue (return 444), empêchant l'exposition accidentelle du backend Python.NGINX_CONF='' ou NGINX_RELOAD_CMD='' pour désactiver la gestion automatique de nginx.docker compose up --build
Ports exposés par Docker :
| Port | Objectif |
|---|---|
80 | nginx — trafic victime basé sur le domaine |
3000 | Python — interface opérateur + accès direct victime |
http://localhost:3000/http://localhost:3000/apphttp://<campaign-domain>/ (routées via nginx sur le port 80)Les jetons capturés et les journaux sont persistés dans des volumes Docker :
| Volume | Chemin dans le conteneur | Objectif |
|---|---|---|
easytokens-data | /data/easytokens.db | Base de données SQLite |
./logs | /logs/ | Fichiers de log par requête |
Prérequis : Python 3.12+, aucun paquet tiers.
python main_server.py
Le répertoire php-relay/ contient un conteneur Apache/PHP autonome qui agit comme un nœud distribué côté victime. Il sert la même page d'inscription par code d'appareil que le serveur principal, interroge Microsoft directement et transmet les jetons capturés au panneau principal via POST /webhook.
checkin.php envoie une requête POST à POST /relay/checkin sur le panneau principal, en s'authentifiant avec RELAY_SECRET. Le panneau enregistre le nœud et retourne un node_id.index.php), qui initie un nouveau flux de code d'appareil.poll.php. Lorsqu'un jeton est capturé, poll.php le transmet à POST /webhook sur le panneau principal, étiqueté avec le node_id.Configurer dans php-relay/.env :
Les deux conteneurs simultanément (recommandé) :
# from EasyTokens/
docker compose -f docker-compose.yml -f php-relay/docker-compose.yml up --build
L'exécution des deux avec une seule fusion -f les place sur le même réseau Docker, donc MAIN_SERVER_URL=http://easytokens:3000 se résout correctement.
Accès :
| URL | Objectif |
|---|---|
http://localhost:8082/ | Page d'inscription victime du relais |
http://localhost:3000/app/nodes | Vue opérateur des nœuds relais enregistrés |
Relais uniquement (panneau distant) :
cd php-relay
# Set MAIN_SERVER_URL to the public panel URL in .env
docker compose up --build
openssl rand -hex 32RELAY_SECRET=<secret> dans EasyTokens/.env (pris en compte par le panneau principal via docker-compose.yml)php-relay/.envDeux tables dans la base de données SQLite :
tokens — chaque jeton d'accès/rafraîchissement capturé (UPN, jeton d'accès, jeton de rafraîchissement, jeton d'ID, portée, source, horodatage de capture)device_codes — campagnes initiées par l'opérateur (code utilisateur, code d'appareil, URL de vérification, statut, ID de jeton lié, port, domaine)MIT — Copyright (c) 2026 Casey Smith
| Méthode | Chemin | Audience | Description |
|---|
GET | / | Victime | Page d'inscription par code d'appareil (thème Dunder Mifflin) |
POST | /poll | Victime (JS) | Interroge le point de terminaison du jeton MS ; stocke le jeton en cas de succès |
GET | /search?s=<id> | Victime | Page de recherche OneDrive/e-mail post-authentification |
GET | /app | Opérateur | Tableau de bord — statistiques + captures récentes |
GET | /app/device-codes | Opérateur | Toutes les campagnes avec statut de sondage en direct |
POST | /app/device-codes/new | Opérateur | Créer une nouvelle campagne de code d'appareil |
GET | /app/tokens | Opérateur | Tous les jetons capturés |
GET | /app/search?id=<id> | Opérateur | Recherche OneDrive + e-mail pour un jeton spécifique |
POST | /webhook | Nœud relais | Ingérer un jeton depuis un nœud relais authentifié |
POST | /relay/checkin | Nœud relais | Enregistrer un nœud relais ; retourne node_id |
| Variable | Par défaut | Description |
|---|
PORT | 3000 | Port d'écoute du serveur Python |
PORT_MAX | 3010 | Limite supérieure de la plage de ports exposée par Docker |
DB_PATH | /data/easytokens.db | Chemin de la base de données SQLite |
LOG_DIR | /logs | Répertoire des logs par requête |
NGINX_CONF | /etc/nginx/conf.d/easytokens.conf | Chemin où nginx_helper écrit la configuration générée ; définir sur '' pour désactiver |
NGINX_RELOAD_CMD | nginx -s reload | Commande shell pour recharger nginx après les modifications de configuration ; définir sur '' pour désactiver |
RELAY_SECRET | (non défini) | Secret partagé pour l'authentification du nœud relais. Générer avec openssl rand -hex 32. Doit correspondre à RELAY_SECRET dans php-relay/.env. Laisser non défini pour désactiver le support du relais. |
| Variable | Par défaut | Description |
|---|
MAIN_SERVER_URL | http://easytokens:3000 | URL du panneau principal accessible depuis l'intérieur du conteneur relais |
RELAY_SECRET | (requis) | Doit correspondre à RELAY_SECRET sur le panneau principal |
RELAY_PORT | 8082 | Port hôte pour exposer le relais |
RELAY_LABEL | nom d'hôte | Nom lisible affiché dans le panneau pour ce nœud |
CHECKIN_RETRIES | 10 | Tentatives de réinscription avant d'abandonner |
CHECKIN_DELAY | 5 | Secondes entre les tentatives de réinscription |
NODE_ID_FILE | /tmp/relay_node_id | Chemin où le node_id est mis en cache après l'inscription |
| Champ | Valeur |
|---|
| ID client | d3590ed6-52b3-4102-aeff-aad2292ab01c (Microsoft Office) |
| Ressource | https://graph.microsoft.com |
| Portées | openid profile email offline_access Mail.Read Files.Read |
| URL du code d'appareil | https://login.microsoftonline.com/common/oauth2/devicecode |
| URL du jeton | https://login.microsoftonline.com/Common/oauth2/token |