Research/webmin-filemin-cpio
0-dayNo CVEHighPublic

Webmin 2.670, File Manager cpio absolute extract

Webmin 2.670 filemin/extract.cgi pipes rpm2cpio into cpio -idmv with no --no-absolute-filenames. A File Manager user fenced to a real allowed_paths directory writes absolute cpio members outside that jail as root. Not default $HOME=/. Not CVE-2026-49103. Labbed. No CVE yet.

Name
Webmin 2.670, File Manager cpio absolute extract
Type
0-day analysis
CVE
n/a
CVE Risk
high
Disclosure Status
public
Vendor
Webmin
Affected
Webmin 2.670 (3a2b2b8671c0669c129f5275b3dd1853106ae02b). File Manager with work_as_root and a real allowed_paths jail. Needs rpm2cpio on the host.
Published
11 Nov 2026
Updated
11 Nov 2026
Tags
0-day, webmin, filemin, cpio, path, cwe-22, remote, authenticated

GNU cpio already had the flag. File Manager never passed it.

I wanted a File Manager user who was actually fenced. Not the default ACL, where $HOME becomes / and the whole disk is in bounds. A real directory. work_as_root still on, because that is what defaultacl ships. Then an RPM whose inner files were named with a leading slash.

Someone with a Webmin login and File Manager is supposed to unpack archives only inside the folder they were given. If that folder is a real jail, they can upload an RPM whose payload files have names like /tmp/whatever. Extracting that RPM writes those files as root, outside the folder they were allowed to touch. They do not need a shell module. They do not need the default "home is the whole disk" ACL. They need File Manager, a real allowed_paths directory, work_as_root, and rpm2cpio on the host.

This is not anonymous. This is not CVE-2026-49103. The lab writes a witness file. It does not spawn a shell.

Proof lives at abraxas/webmin-filemin-cpio (loopback client). Stack: lab/ Dockerfile, docker-compose.yml, entrypoint.sh, run.sh, poc.py. Loopback only.

Webmin 2.670 (3a2b2b8671c0669c129f5275b3dd1853106ae02b). No CVE yet. CWE-22. 8.8 High (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). Remote path write.

Why this product

Webmin is the box people put on a server so they can stop SSHing for every edit. File Manager is the module that looks like a desktop folder tree. The interesting case on this tree is not "I was given the shell module." The policy at webmin.com/security says that is why fine-grained ACLs exist. allowed_paths is the fence. If extract walks through the fence, the fence was a lie.

Unpaid VDP. I hunted the tag people install anyway.

Pinning 2.670

Tag 2.670, commit 3a2b2b8671c0669c129f5275b3dd1853106ae02b, published 2026-09-21. Master at scan time was 792e4d3006a3522fe80d3cf2fffe149bf2227dfd, 298 commits ahead. I did not hunt master. I did not request webmin.com.

The skip list is half the hunt

Already public on or before this pin. I did not refile them.

A sibling in a module those notes do not name can still be a find. This one is File Manager extract, not mail detach.

Surfaces that returned nothing

JWT, OAuth, password-reset leftovers of the host-header bug, SSRF after the download fix, SQL, XXE, GraphQL, request smuggling, cache, template injection, dangling download hosts, package defaults, deserialization, mass assignment, CORS, hash merge, CI, session-id construction. Empty on this pin for Crit/High from an untrusted actor.

The archive name was in the jail. The members were not.

filemin/extract.cgi takes file, runs it through validate_filename_path, then MIME-switches. Zip, tar, 7z, rar, deb, rpm. The RPM arm is:

Plain text
elsif ($archive_type =~ /(\/x-rpm|\/x-deb)/) {
        my ($dir) = fileparse($full, qr/\.[^.]*/);
        my $path = &validate_filename_path($dir);
        &make_dir($path, 0755);
        if ($archive_type =~ /\/x-rpm/) {
                $cmd = "(rpm2cpio ".
                        quotemeta($full).
                        " | (cd ".quotemeta($path).
                        "; cpio -idmv))";
                }

quotemeta is on the RPM path and the directory File Manager already accepted. The cpio stream is whatever rpm2cpio emits. GNU cpio, by default, extracts a member named /tmp/WEBMIN-H10-CPIO-WRITE to that absolute path. --no-absolute-filenames is the flag that stops it. This argv does not include it.

extract_files in filemin-lib.pl repeats the same pipe for extract-on-upload:

Plain text
$status = system(
        "($rpm2cpio_cmd " .
        quotemeta($archive) .
        " | (cd " .
        quotemeta($extract_to) .
        "; $cpio_cmd -idmv))");

Then chown -R on $extract_to only. A file that landed outside that directory stays whatever uid wrote it. With work_as_root, that is root.

validate_filename_path is not asleep. It refuses NUL, .. path segments, and anything that does not sit under allowed_paths. It never opens the archive. It cannot see a cpio header. The jail check is on the RPM. The write is on the members.

MIME comes from File::MimeInfo. On a host with shared-mime-info, *.rpm is application/x-rpm. That is the branch. Debian's hard Depends for Webmin are tar, unzip, and shared-mime-info. rpm2cpio is not a hard Depend. RPM-based installs have it because it ships with rpm. The lab image installs it on purpose.

Default $HOME is already /. That is not this bug.

get_allowed_paths has a special case:

Plain text
if ($rui[0] eq 'root' && @paths == 1 &&
    ($paths[0] eq '$HOME' || $paths[0] eq '$ROOT')) {
        $base = "/";
        @paths = ( $base );
        }

Shipped defaultacl is allowed_paths=$HOME and work_as_root=1. A root File Manager user with that one token can already see the disk. Writing /tmp/x through cpio would not leave a jail that exists. I did not keep that.

The case that matters is an admin who did the extra work: a real directory in allowed_paths. Help-desk. Contractor. Someone who is supposed to edit files under /lab/jail and nowhere else. Extract still writes /tmp.

Usermin and safeacl switch to the Unix user first. Same member, that uid. work_as_root off is the same drop. I labbed the shipped-on flag with a real jail.

Wrong turns, in the order they happened

First I almost threw the whole class out because $HOME expands to /. That is the default. It is also the thing the KEEP bar is not. A fence that was never installed is not a bypass of the fence.

Then tar. GNU tar on this pin skips member names that contain ... There is a separate story about a first extract planting a symlink and a second extract following it (CVE-2025-45582-shaped). extract.cgi unpacks straight into $cwd, so a symlink already in the extract root is in play for tar/unzip. I did not need that. The RPM path is one extract and an absolute name.

Then unzip. 6.0 strips ../ unless you pass -:. This code never passes -:.

Then CVE-2026-49103. Mailboxes, detachall.cgi, attachment filename, fixed before 2.640. Different CGI. Easy to mash "Webmin writes a file from an archive-shaped blob" into one CVE. It is not one CVE.

Then ordinary upload. upload.cgi calls validate_filename_path on the destination. download.cgi forces application/x-download. You do not smuggle /tmp/x as the uploaded name. You smuggle it as a cpio member inside a name the jail already liked.

Last: treating File Manager as "already root" and walking away. The policy is explicit. Fine-grained ACLs are the control. This extract does not honor them.

What an attacker can do

Needs a Webmin session with File Manager. work_as_root=1. allowed_paths a real directory. rpm2cpio and cpio on the host. Not anonymous. Not the default whole-disk ACL.

Privilege Method / sink Recon / integrity / availability Lab-proven or inferred
Write a file at an absolute cpio member path outside allowed_paths GET /filemin/extract.cgi after POST /filemin/upload.cgi; rpm2cpio | cpio -idmv Integrity (root file write) Lab-proven. /tmp/WEBMIN-H10-CPIO-WRITE contained WEBMIN-H10-CPIO-WRITE
Same write via extract-on-upload upload.cgi extract_uploaded → extract_files Integrity Inferred. Same argv. Lab used the listing extract CGI
Overwrite any path root can create same sink, chosen member name (cron, authorized_keys, miniserv.conf, a CGI under the Webmin root) Integrity / availability Inferred from GNU cpio. Not this lab's oracle
Negative control: member lands under the jail same extract if the member were relative n/a Lab-proven. Fail if /lab/jail/h10/tmp/... exists

Stays closed on this leftover

Axis Method / sink Why it is closed
Integrity Default allowed_paths=$HOME Expands to / for root. No jail to leave.
Integrity Upload of an absolute file name validate_filename_path
Integrity download.cgi application/x-download
Integrity tar member .. GNU tar skips those names
Integrity unzip ../ stripped unless -:
Integrity Usermin / work_as_root off uid drop first
Integrity CVE-2026-49103 mailboxes detach, already fixed
Integrity File Manager listing closed on 2.670
Integrity Unauthenticated extract session required
Integrity Host RCE in this lab witness file only

A root file write is how you plant persistence. I did not prove a shell. I proved the write.

The lab

Limited Webmin user fileuser, password in the lab only. Module grant: File Manager. ACL: allowed_paths=/lab/jail, work_as_root=1. Image webmin-lab:2.670 from the pin. Loopback 127.0.0.1:18430. ssl off. The PoC builds a 383-byte RPM: cpio newc member /tmp/WEBMIN-H10-CPIO-WRITE, gzip payload, lead + signature + header. rpm2cpio | cpio -t lists that absolute name. Upload into the jail. MIME application/x-rpm. GET extract. The file outside the jail holds the witness. If it had been written as /lab/jail/h10/tmp/..., the lab fails.

Plain text
IOC pin-version=2.670 image=webmin-lab:2.670 expect=2.670
IOC rpm2cpio-list rc=0 /tmp/WEBMIN-H10-CPIO-WRITE
IOC mime=application/x-rpm
IOC outside-file='WEBMIN-H10-CPIO-WRITE\n'
SUCCESS WEBMIN-H10-FILEMIN-CPIO extract.cgi absolute-cpio WEBMIN-H10-CPIO-WRITE

run.sh clones the pin. Bind it to loopback.

The fix

Pass --no-absolute-filenames to cpio. Or list members first and refuse any name that is absolute or contains ... Keep the existing check on the archive path. Add the same check to the payload. extract.cgi and extract_files both need it.

I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.