
ProFTPD 1.3.9d - High - Remote - Privilege leftover / File write - SITE COPY dest follows symlink
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · proftpd-site-copy-dest-follow
Class: Privilege leftover / File write Reach: Remote
ProFTPD 1.3.9d (f59f248) - proftpd.org
Contrib mod_copy SITE COPY Limit-checks dest with dir_canonical_vpath (unresolved). pr_fs_copy_file2 then opens the dest without O_NOFOLLOW, so a dest symlink is followed. FTP STOR Limit-checks a resolved path (dir_realpath / dir_best_path). SITE COPY dest does not.
An authenticated FTP user overwrites a WRITE-denied file by pointing a dest symlink at it and issuing SITE COPY. Direct STOR and SITE COPY to the victim stay 550. STOR / SITE RMDIR / RMD of the link stay 550. DELE removes the link node only. No shell. Unix DAC still applies.
This is the inverse of public Issue 2095 (SITE COPY source READ skip). It is distinct from public Issue 2256 (RNTO EXDEV symlink follow). This leftover is SITE COPY dest on every copy, not only EXDEV.
| ID | no CVE yet |
| Class | Privilege leftover / File write |
| Reach | Remote (authenticated FTP) |
| CWE | CWE-59, CWE-863 |
| CVSS | High: 7.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N |
| Product | ProFTPD |
| Affected | 1.3.9d tag v1.3.9d (f59f248e26bdfa514443e42bf72a145641df635d) contrib mod_copy |
| Auth | authenticated FTP (mod_copy loaded, CopyEngine default on). SITE SYMLINK plant also needs contrib mod_site_misc unless the link already exists |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Who: an authenticated FTP user. mod_copy loaded. A dest symlink in a WRITE-allowed directory (SITE SYMLINK, or a pre-existing link). SITE SYMLINK src uses dir_check_canon G_READ, so planting a link to the victim file needs READ on that file. The split is READ allowed, WRITE denied. The session UID must still be able to Unix-write the target.
| Method | Sink | Privilege |
|---|---|---|
| SITE SYMLINK (or existing link) in a WRITE-allowed dir whose target is a WRITE-denied file the UID can Unix-write; SITE COPY readable-src that-link | pr_fsio_open dest follows | Integrity (lab-proven): truncate/overwrite the Limit-denied file as the session UID. Lab: /secret/victim went from ORIGINAL to the witness after SITE COPY /readable/src.txt /upload/link. Direct SITE COPY to /secret/victim stayed 550. |
| SITE RMDIR of that same link | dir_check_canon -> dir_best_path | Closed (lab-proven): 550 RMDIR /upload/link: Operation not permitted. Victim intact. |
| Core RMD of that same link | dir_best_path + WRITE | Closed (lab-proven): 550 Permission denied. Victim intact. |
| STOR of the dest link | dir_best_path + WRITE | Closed (lab-proven): STOR /upload/link -> 550. Victim intact. |
| DELE of the dest link | unlink the link node | Closed for the victim (lab-proven): DELE removed the link only; victim still held the witness. |
DefaultRoot jail to host /etc | chroot | Closed. Unix DAC still applies. No unauth. No shell. |
Lab-proven vs inferred: overwrite via unresolved dest symlink, denied STOR/RMDIR/RMD, and DELE-of-link-only are lab-proven. Unauth and Unix DAC remain inferred from the auth/chroot model.
I pinned proftpd/proftpd v1.3.9d (f59f248). Default core FTP with sample basic.conf had no unpublished Crit/High. Contrib mod_copy was next.
copy_copy (contrib/mod_copy.c) builds dest with dir_canonical_vpath then dir_check(..., G_WRITE, to) on that unresolved string. pr_fs_copy_file2 (src/fsio.c) comments that stat/open follow the dest symlink, then pr_fsio_open(dst, O_WRONLY|O_CREAT|O_NONBLOCK) with no O_NOFOLLOW.
Public Issue 2095 is source READ skip on SITE COPY. Skip that exact bug. Public Issue 2256 is RNTO EXDEV follow. This leftover is SITE COPY dest on every copy.
SITE RMDIR recursive follow was a parent drop: SITE RMDIR uses dir_check_canon / dir_best_path, which resolves the symlink before Limit. Lab confirmed SITE RMDIR and core RMD of the dest link stay denied.
Wrong turns already recorded: treating dest Limit as resolved because STOR is; treating Issue 2256 as this leftover; claiming SITE RMDIR walks the victim. Theatre: a reverse shell. The oracle is /secret/victim holding the witness after SITE COPY through /upload/link, with STOR of the link still 550.
python3 proftpd-site-copy-dest-follow-Abraxas-Labs.py
or:
cd lab
./run.sh
Loopback FTP 127.0.0.1:18720, passive 18721-18730. Compose project proftpd-site-copy-dest-follow. Witness PROFTPD-SITE-COPY-DEST-FOLLOW-WITNESS. Last line:
SUCCESS PROFTPD-SITE-COPY-DEST-FOLLOW victim-has-witness=yes stor-secret=denied stor-link=denied site-rmdir-walk=no rmd-walk=no dele-victim=intact PROFTPD-SITE-COPY-DEST-FOLLOW-WITNESS
Limit-check the resolved dest (dir_realpath / dir_best_path), or open dest with O_NOFOLLOW. STOR already Limit-checks a resolved path.
Vendor intake is [email protected]. Do not open a public GitHub issue on proftpd/proftpd.
f59f248e26bdfa514443e42bf72a145641df635d)contrib/mod_copy.c copy_copysrc/fsio.c pr_fs_copy_file2<Limit READ>GNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.