
Kali365 - EvilTokens Replica
A device-code phishing server for adversary emulation. Captures Microsoft 365 OAuth tokens via the Device Authorization Grant flow. Inspired by GraphSpy.
Warning — Authorized Use Only. This tool is intended for penetration testers and red teamers operating under explicit written authorization. Misuse may violate computer fraud laws.
microsoft.com/devicelogin.Python backend (one port per campaign) + nginx front-end + SQLite persistence.
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)
When a campaign is created with a domain set, nginx_helper.py automatically writes a per-domain server block to NGINX_CONF (/etc/nginx/conf.d/easytokens.conf by default) and reloads nginx. Victim traffic arriving on port 80 for that domain is proxied to the correct Python server instance.
nginx.conf drops any request whose Host header does not match a known campaign (return 444), preventing accidental exposure of the Python backend.NGINX_CONF='' or NGINX_RELOAD_CMD='' to disable automatic nginx management.docker compose up --build
Ports exposed by Docker:
| Port | Purpose |
|---|---|
80 | nginx — domain-based victim traffic |
3000 | Python — operator UI + direct victim access |
http://localhost:3000/http://localhost:3000/apphttp://<campaign-domain>/ (routed through nginx on port 80)Captured tokens and logs are persisted to Docker volumes:
| Volume | Path in container | Purpose |
|---|---|---|
easytokens-data | /data/easytokens.db | SQLite database |
./logs | /logs/ | Per-request log files |
Requirements: Python 3.12+, no third-party packages.
python main_server.py
The php-relay/ directory contains a standalone Apache/PHP container that acts as a distributed victim-facing node. It serves the same device-code enrollment page as the main server, polls Microsoft directly, and forwards captured tokens to the main panel via POST /webhook.
checkin.php POSTs to POST /relay/checkin on the main panel, authenticating with RELAY_SECRET. The panel registers the node and returns a node_id.index.php), which initiates a fresh device-code flow.poll.php. When a token is captured, poll.php forwards it to POST /webhook on the main panel, tagged with the node_id.Configure in php-relay/.env:
Both containers simultaneously (recommended):
# from EasyTokens/
docker compose -f docker-compose.yml -f php-relay/docker-compose.yml up --build
Running both with a single -f merge puts them on the same Docker network, so MAIN_SERVER_URL=http://easytokens:3000 resolves correctly.
Access:
| URL | Purpose |
|---|---|
http://localhost:8082/ | Relay victim enrollment page |
http://localhost:3000/app/nodes | Operator view of registered relay nodes |
Relay only (remote panel):
cd php-relay
# Set MAIN_SERVER_URL to the public panel URL in .env
docker compose up --build
openssl rand -hex 32RELAY_SECRET=<secret> in EasyTokens/.env (picked up by the main panel via docker-compose.yml)php-relay/.envTwo tables in the SQLite database:
tokens — every captured access/refresh token (UPN, access token, refresh token, id token, scope, source, capture timestamp)device_codes — operator-initiated campaigns (user code, device code, verification URL, status, linked token ID, port, domain)MIT — Copyright (c) 2026 Casey Smith
| Method | Path | Audience | Description |
|---|
GET | / | Victim | Device-code enrollment page (Dunder Mifflin theme) |
POST | /poll | Victim (JS) | Polls MS token endpoint; stores token on success |
GET | /search?s=<id> | Victim | Post-auth OneDrive/email search page |
GET | /app | Operator | Dashboard — stats + recent captures |
GET | /app/device-codes | Operator | All campaigns with live poll status |
POST | /app/device-codes/new | Operator | Create a new device code campaign |
GET | /app/tokens | Operator | All captured tokens |
GET | /app/search?id=<id> | Operator | OneDrive + email search for a specific token |
POST | /webhook | Relay node | Ingest a token from an authenticated relay node |
POST | /relay/checkin | Relay node | Register a relay node; returns node_id |
| Variable | Default | Description |
|---|
PORT | 3000 | Listening port for the Python server |
PORT_MAX | 3010 | Upper bound of port range exposed by Docker |
DB_PATH | /data/easytokens.db | SQLite database path |
LOG_DIR | /logs | Per-request log directory |
NGINX_CONF | /etc/nginx/conf.d/easytokens.conf | Path nginx_helper writes generated config to; set to '' to disable |
NGINX_RELOAD_CMD | nginx -s reload | Shell command to reload nginx after config changes; set to '' to disable |
RELAY_SECRET | (unset) | Shared secret for relay node authentication. Generate with openssl rand -hex 32. Must match RELAY_SECRET in php-relay/.env. Leave unset to disable relay support. |
| Variable | Default | Description |
|---|
MAIN_SERVER_URL | http://easytokens:3000 | URL of the main panel reachable from inside the relay container |
RELAY_SECRET | (required) | Must match RELAY_SECRET on the main panel |
RELAY_PORT | 8082 | Host port to expose the relay on |
RELAY_LABEL | hostname | Human-readable name shown in the panel for this node |
CHECKIN_RETRIES | 10 | Check-in retry attempts before giving up |
CHECKIN_DELAY | 5 | Seconds between check-in retries |
NODE_ID_FILE | /tmp/relay_node_id | Path where the node_id is cached after check-in |
| Field | Value |
|---|
| Client ID | d3590ed6-52b3-4102-aeff-aad2292ab01c (Microsoft Office) |
| Resource | https://graph.microsoft.com |
| Scopes | openid profile email offline_access Mail.Read Files.Read |
| Device code URL | https://login.microsoftonline.com/common/oauth2/devicecode |
| Token URL | https://login.microsoftonline.com/Common/oauth2/token |