vsftpd 3.0.5 - High - Remote - Privilege leftover - brace deny_file misses .private/secret
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · vsftpd-deny-file-brace-child
Class: Privilege leftover Reach: Remote
vsftpd 3.0.5 - Chris Evans
The man page (vsftpd.conf.5) example is deny_file={*.mp3,*.mov,.private}. Matching names "should not be accessible in any way," including "download, change into directory, affect something within directory." vsf_access_check_file (access.c:16-43) calls vsf_filename_passes_filter on the client path as given, then str_locate_str of the entire brace tunable. Filter matches last-component / prefix; leftover /secret fails; substring of {*.mp3,*.mov,.private} misses.
CWD .private is denied (basename hits). RETR .private/secret and RETR dir/.private succeed. That "within directory" is what this leftover breaks.
CVE-2015-1419 is a public distro realpath/basename patch. Official 3.0.5 never took it. Debian's brace branch still matches the full path, so this child walk remains on patched distro packages too. Do not re-file 2015-1419.
This leftover is a config-gated deny-list miss. It is not unauth RCE, not a chroot escape, and not a host /etc read.
| ID | no CVE yet |
| Class | Privilege leftover |
| Reach | Remote (authenticated FTP) |
| CWE | CWE-863, CWE-22 |
| CVSS | High: 7.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Product | vsftpd (Chris Evans) |
| Affected | vsftpd 3.0.5 official tarball SHA-256 26b602ae454b0ba6d99ef44a09b6b9e0dfa7f67228106736df1f278c70bc91d3 |
| Auth | authenticated FTP USER/PASS (anonymous in the lab). deny_file set to a brace list (default unset) |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only. Anonymous read-only. Witness file. |
Who: an authenticated FTP user (anonymous ftp in the lab). Session UID after the usual vsftpd drop. deny_file is the extra policy that was supposed to take capabilities away from that UID. Virtual-user recipes the man page names share guest_username, so Unix DAC does not separate them. Stock 3.0.5 with tunable_deny_file NULL is not this leftover.
| Method + sink | Privilege |
|---|---|
RETR .private/secret with documented brace deny_file={*.mp3,*.mov,.private} | handle_retr str_open of a file whose directory name is on the deny list, as the session uid. Lab: 150/226, 37 bytes, body VSFTPD-DENY-FILE-BRACE-CHILD-WITNESS |
RETR dir/.private | same sink. A file named .private under another directory (must_match_at_current_pos rejects a match at index > 0). Lab: 150/226, body DIRDOT-VSFTPD-DENY-FILE-BRACE-CHILD-WITNESS |
vsf_access_check_file gate; lab was read-only)| Method + sink | Privilege |
|---|---|
STOR / APPE the same path when write_enable=YES | handle_upload_common create / overwrite as the session uid |
DELE / RNTO the same path | str_unlink / rename of a name the denylist was meant to stop |
| Method + sink | Privilege |
|---|---|
LIST .private/x (or LIST inside the denied tree) | handle_dir_common access-checks the full arg (miss), then str_opendir of the .private prefix after the slash split. Names only. Unix DAC still applies |
| Method + sink | Result |
|---|---|
CWD .private | 550 Permission denied. Basename .private hits the brace |
RETR song.mp3 | 550. *.mp3 is a last-component pattern and does match |
CWD .private itself (basename hits)hide_file LIST hide of the dirent (basename from readdir; RETR of a hidden name is documented intended)/etc from a chroothttp_enable default off; FTP-mode GET kills the session)deny_file=.private is saved by the substring fallback (str_locate_str of .private in .private/secret)I pinned official vsftpd 3.0.5 (SHA-256 26b602ae...c91d3) rather than a GitHub mirror or a Debian/Fedora tree. Nineteen source hunts. Default compiled daemon has no unpublished Critical/High leftover. This one needs the operator to copy the man-page brace example.
vsf_filename_passes_filter (ls.c) matches * / ? / unnested {,} and only does useful work on a last-component pattern. For name .private/secret against {*.mp3,*.mov,.private}:
*.mp3, *.mov, .private are matched at the start of .private/secret. Leftover /secret fails unless the token was *.{*.mp3,*.mov,.private} and misses.Wrong turn already public: CVE-2015-1419 is distro-only realpath. Official Changelog has no such fix. Even Debian's brace branch copies the full normalized path, so children still walk.
cd lab
./run.sh
Loopback control 127.0.0.1:18710, PASV 18711-18718. Compose project vsftpd-deny-file-brace-child. Official 3.0.5 tarball, anonymous read-only, deny_file={*.mp3,*.mov,.private}. Witness VSFTPD-DENY-FILE-BRACE-CHILD-WITNESS. Last line:
SUCCESS vsftpd-deny-file-brace-child cwd-dotprivate=550 retr-child=yes retr-dir-dotprivate=yes mp3=550 VSFTPD-DENY-FILE-BRACE-CHILD-WITNESS
Match brace alts against every path component, or realpath / basename before the filter (with the usual symlink caveats). The documented example should actually deny children.
Vendor intake is [email protected] (vsftpd). Do not open a public GitHub issue on any vsftpd mirror.
Abraxas Labs - abraxaslabs.tech - github.com/abraxas - @abraxas_null - [email protected]