visitor@0xll.sh:~$ cat blog/multi-tenant-phishing-server.md

One VPS, Many Operators: Building a Multi-Tenant Phishing Platform for Red Team Engagements

What is LPhish?

LPhish ("Let's Phish") is an open-source phishing operations platform that fuses two of the most widely used open-source phishing tools — GoPhish (campaign orchestration, email sending, result tracking) and evilginx3 (adversary-in-the-middle credential capture) — into one unified web control plane.

Instead of juggling two separate admin UIs, two databases, and a pile of shell scripts to glue them together, an operator gets a single console that manages engagements, phishlets, lures, campaigns, DNS, TLS certificates, and a unified live event feed.


The problem it solves

GoPhish and evilginx3 are powerful individually, but running them together for a real engagement is painful:

LPhish's core bet: make evilginx3 write directly into GoPhish's database, and put a single authenticated control plane in front of both backends' APIs.


Architecture at a glance

                          ┌──────────────────────────────────────────────┐
   Operator (browser)     │            LPhish control plane            │
   ── :8080 ─────────────►│  HTTP + JSON API + Web UI  (bin/lphish)    │
                          │                                              │
                          │  /api/engagements/{id}/gophish/*   ────────┐ │
                          │  /api/engagements/{id}/evilginx/*  ──────┐ │ │
                          └──────────────────────────────────────────┼─┼─┘
                                                                     │ │
                        ┌─────────────────────────────┐   ┌──────────▼─▼──────────┐
   Target (email/browser)                             │   │  GoPhish admin API    │
   ── :443 (lure URL) ──►  evilginx3 (AitM proxy)     │   │  :3333 (bridge)        │
   ── :25 / SMTP ───────►  GoPhish mailer (SMTP sink) │   │  SQLite (SHARED DB)    │
                        │  -api :8081                 │   │  gophish.db            │
                        └─────────────┬───────────────┘   └───────────▲───────────┘
                                      └──── evilginx3 writes ─────────┘
                                      (opened / clicked / submitted data)
Component Role Ports
bin/lphish Control plane: auth, engagements, operators, API bridges, unified feed, web UI 127.0.0.1:8080
bin/gophish (fork) GoPhish admin API + mailer; template execution; result/status store 127.0.0.1:3333 admin API, :8083 TLS phish server
bin/evilginx3 (fork) AitM proxy; phishlet engine; REST API bridge; writes events to the shared GoPhish DB :8443 HTTPS, 127.0.0.1:8081 -api
Apache2 front-end (optional) TLS termination on public :443; proxies / → evilginx3, /phish → control plane 0.0.0.0:443

The shared database — the key architectural decision

Both forks were modified so that the evilginx3 event path calls GoPhish's database layer (database.HandleEmailOpened, database.HandleClickedLink, database.HandleSubmittedData) against the same SQLite file GoPhish uses (gophish.db, selected with evilginx3 -g).

GoPhish status values therefore advance exactly like a native GoPhish campaign:

Email/SMS Sent → Email Opened → Clicked Link → Submitted Data

A status is only ever promoted — a late email-open after a click is recorded as an event but does not downgrade Clicked Link back to Email Opened. This single decision is what makes the unified live feed and the campaign result funnel possible.

The client_id blob — how the two tools share identity

The campaign URL stored on the GoPhish campaign is the plain lure URL (e.g. https://login.login.evil.test/zGkzbfAO). GoPhish's mailer automatically appends tracking parameters through evilginx.CreatePhishUrl, which wraps rid, fname, lname, and email into one encrypted client_id query parameter:

?client_id=<8-char RC4 key><base64url(checksum-byte + RC4(params))>

evilginx3's extractParams decrypts that blob on the lure path and treats the decoded rid as the click/recipient id. A plaintext ?rid=... query string is not accepted (the log shows lure parameter checksum doesn't match), which keeps the tracking mechanism tamper-resistant. Email-open tracking uses the same blob format with parameters o=track&rid=<rid>; evilginx3 answers with a tracking PNG and logs email opened.


What the console can do

The control plane is a single-page app (vanilla JS + CSS, no framework) served by the Go binary. Its tabs cover the full engagement lifecycle:

Engagements & operators

Phishlets (evilginx3 management)

Lures

Campaigns (GoPhish resources in the console)

Browser-in-the-Browser (BitB) pages

Static harvest pages

Device-code phishing (RFC 8628)

DNS, certs, and domain intelligence

Unified live feed


Deployment

Three deployment modes are supported:

  1. One command (sandbox/dev) — ./runtime/start_all.sh builds and starts the whole stack    (GoPhish, evilginx3, control plane, optional Apache2 front-end, and an opt-in dev SMTP sink    on :2525 for end-to-end testing without a real relay).
  2. Persistent host (systemd) — ./runtime/install_systemd.sh installs units that survive    logout, auto-restart on crash, and start on boot. The default install runs isolated    per-engagement cells (each engagement gets its own gophish + evilginx); --shared is the    legacy single-backend layout.
  3. Manual — per-component start commands, with prebuilt linux/amd64 binaries in bin/.

An optional Apache2 front-end terminates TLS on public :443, proxies / to evilginx3 (127.0.0.1:8443), /phish to the control plane's harvest pages, and serves the BitB assets — with X-Forwarded-For set to the real client IP so the blacklist and session logging work.


Security posture

The control plane is hardened in ways worth calling out:

The README is also candid about the trade-offs: OAuth secrets/refresh tokens are stored in plaintext in the shared gophish.db (like upstream GoPhish's password field), and the GoPhish bridge uses InsecureSkipVerify because the admin API serves a self-signed cert on loopback.


Responsible use

LPhish is a research / authorized-testing platform. The NOTICE.md is explicit: the forks retain their original licenses (GoPhish MIT, evilginx3 BSD-style), and the modifications are provided for authorized security assessments and awareness testing only. The README's BitB section even includes a detection note — modern browsers expose OS-level window chrome that fake chrome can't reproduce, so the technique is best used where the assessment calls for it.


Bottom line

LPhish is a serious attempt to solve a real operational problem in phishing simulation: the GoPhish/evilginx3 split. By making evilginx3 write into GoPhish's database and wrapping both APIs in one hardened control plane, it collapses the two-tool workflow into a single console with a unified funnel from email sent to credentials submitted. For red teams and authorized phishing assessments that already use both tools, it's worth a look.

This post was generated from a review of the LPhish repository at /home/ubuntu/LPhish (HEAD 27ea906, September 2026).