
Reproducible Docker lab + raw-socket exploit for CVE-2015-3306 (ProFTPD mod_copy pre-auth arbitrary file copy) — a patch-diffing learning exercise
A fully reproducible lab and a hand-written exploit for CVE-2015-3306 — built as a learning exercise in vulnerability research: patch diffing, manual triggering, and raw-socket exploit development.
ProFTPD's mod_copy module implements the SITE CPFR / SITE CPTO
command pair, which copies files on the server side without
transferring data to the client.
In version 1.3.5, neither handler checked whether the session was authenticated. The module simply assumed "nobody will send SITE commands before logging in" — a broken assumption that became a CVE. Any unauthenticated client could copy any file readable by the server process to any location writable by it.
The fix (commit 212d54271f, released in 1.3.5a) does two things:
copy_cpfr and :
copy_cptoauthenticated = get_param_ptr(cmd->server->conf, "authenticated", FALSE);
if (authenticated == NULL || *authenticated == FALSE) {
pr_response_add_err(R_530, _("Please login with USER and PASS"));
errno = EPERM;
return PR_ERROR(cmd);
}
CopyEngine on|off directive — previously the module could not
even be disabled in builds that shipped it.The full unified diff is in patch.diff. Reading patches is
the skill: the fix tells you where the wound was.
The Dockerfile compiles ProFTPD 1.3.5 (the last vulnerable release) from
the official source tarball with mod_copy enabled:
docker build -t proftpd-135 .
docker run -d --name lab135 -p 127.0.0.1:2121:21 -p 127.0.0.1:30000-30010:30000-30010 proftpd-135
docker exec lab135 chmod 777 /home/ftp
PassivePorts in proftpd.conf pins the data channel — FTP's two-channel
architecture (control + dynamic data) is the reason naive container
mappings fail: the control port works, the data port doesn't.
exploit.py uses raw sockets — no ftplib, because libraries hide the
protocol, and hiding the protocol is exactly what we are fighting:
SITE CPFR /etc/segredo.txt -> 350 (no USER/PASS sent: this IS the bug)
SITE CPTO /home/ftp/... -> 250 (arbitrary copy performed)
USER ftp / PASS ... -> 230 (login is only the exfil path)
PASV / RETR -> 150 -> 226 (flag captured)
DELE -> 250 (cleanup: no IOC left behind)
AllowOverwrite, port mappings) decides the final impact.time.time_ns(), not time.time()), and no reliance
on leftover state.RETR answers twice: 150,
then 226. Drain both before the next command.-d10) tells you exactly
which check fired.For education and authorized lab use only. Running this against systems you do not own or have written permission to test is illegal.