Research/webmin-batch-home-prefix
0-dayNo CVEMediumPublic

Webmin 2.670, Users batch home is a raw prefix

Webmin 2.670 useradmin/batch_exec.cgi check_user compares home with a raw substr. save_user.cgi uses is_under_directory and appends a slash. ACL /home/a therefore matches /home/ab on batch create and is refused on the form. Labbed. No CVE yet.

Name
Webmin 2.670, Users batch home is a raw prefix
Type
0-day analysis
CVE
n/a
CVE Risk
medium
Disclosure Status
public
Vendor
Webmin
Affected
Webmin 2.670 (3a2b2b86). Signed-in Users and Groups user with batch create, home ACL without a trailing slash, autohome off.
Published
11 Nov 2026
Updated
11 Nov 2026
Tags
0-day, webmin, path, prefix, cwe-22, authenticated

/home/a is six characters. The form already knew that.

A Webmin help-desk account that is only allowed to put new home directories under /home/a can still create /home/ab through Users and Groups batch. The ordinary Create User form refuses that path. The ACL looks like a directory fence. Batch treats it as the first few characters of a string.

The proof of concept is on GitHub: abraxas/webmin-batch-home-prefix (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, entrypoint.sh, run.sh, poc.py. Authorized lab only. It talks to loopback.

This is Webmin 2.670 (3a2b2b8671c0669c129f5275b3dd1853106ae02b). No CVE yet. CWE-22. 6.5 Medium (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N). Remote path prefix. Authenticated Users and Groups batch create. Home ACL without a trailing slash.

This is not anonymous. This is not a shell module. Root already handed the account Users and Groups. The leftover is that batch create does not use the same fence as the form.

Why Webmin

Webmin is still the box people put on a Linux host so someone who is not root can make users, edit Apache, and restart services. Fine-grained ACLs are the product. webmin.com/security says so: do not file "I was given the shell module." File the case where the ACL denied the action and the CGI still did it.

I wanted that case. Unauth remote, or a signed-in user who was told no and still reached the sink. Default install of the tag people download. Master was 298 commits ahead the day I cloned. I did not hunt master.

Pinning 2.670

Latest published tag on 2026-11-04 was 2.670, 21 Sep 2026. Commit 3a2b2b8671c0669c129f5275b3dd1853106ae02b. Tree version says 2.670. I cloned --branch 2.670 --depth 1 and left it there.

The lab image is a source setup.sh of that tree on Debian bookworm, ssl off, login root / labroot, then an entrypoint that adds a limited Users and Groups user with home=/home/a. Not a distro package. Not Virtualmin.

The skip list is half the hunt

Already public on or before this pin, so not this post:

  • custom-command parameter injection (fixed in 2.670)
  • File Manager search / chooser directory listing (fixed in 2.670)
  • DHCPd / MySQL / PostgreSQL / Nginx / Samba / Squid ACL bypass (2.653)
  • URL-download SSRF (2.652)
  • reflected XSS CVE-2026-49243 (2.650)
  • status-module stored XSS CVE-2026-22678 (2.641)
  • help-page root command (2.640)
  • SVG mail XSS CVE-2026-49102
  • attachment detach overwrite CVE-2026-49103
  • Basic-auth 2FA bypass CVE-2026-42210 and CVE-2026-56022
  • Squid cache-manager injection CVE-2025-67738
  • password-reset host header CVE-2025-61541
  • proxy client-cert spoof CVE-2026-56020
  • shell autocomplete CVE-2024-12828
  • File Manager CVE-2022-0824 and CVE-2022-0829
  • arbitrary file write CVE-2022-30708
  • backdoor CVE-2019-15231

I did not refile those. I did not treat default home=/ as a leftover. That ACL is the whole disk. The product said so.

Surfaces that returned nothing

I opened useradmin/save_user.cgi first because 2.670 had just taught File Manager about prefixes. Line 237 already calls is_under_directory. That helper lives in web-lib-funcs.pl. It refuses .., resolves links, then appends a slash and compares. /home/a becomes /home/a/. /home/ab is not under that.

Export CGIs use the same helper. gexport_exec.cgi, export_exec.cgi. Batch file path uses it against batchdir. The create form was not the hole.

I looked at chooser.cgi next because the File Manager listing fix smelled like a sibling. That is a different leftover: a chroot string that starts with the restricted root, then .. inside the listing. Names, sizes, mtimes. Not a user. Not this post.

LDAP Users save_user.cgi backticks mv. That is a command injection leftover, not a prefix. I left it.

The leftover

useradmin/batch_exec.cgi has its own check_user. Create calls it at line 204. Modify calls it at 499. Then create_home_directory mkdir's, or mv moves the old home.

Plain text
$al = length($access{'home'});
if (length($_[0]->{'home'}) < $al ||
    substr($_[0]->{'home'}, 0, $al) ne $access{'home'}) {
        return &text('usave_ehomepath', $_[0]->{'home'});
        }

No slash. No canonicalize. ACL home=/home/a is six characters. /home/ab matches. /home/a/../ab matches too, because it still starts with those six.

save_user.cgi:237 is the sibling that checks:

Plain text
if (!&is_under_directory($access{'home'}, $user{'home'})) {
        &error(&text('usave_ehomepath', $user{'home'}));
        }

is_under_directory does $dir =~ s/\/*$/\// before the prefix compare. That is the slash check_user never added.

The ACL editor is a text box plus a file chooser. Operators type /home/a. They do not type /home/a/. Defaultacl is home=/, which is a different grant. autohome=1 skips the check on both CGIs. The leftover is the non-slash directory root on the batch path only.

LDAP Users batch copies the same substr. ldap-useradmin/batch_exec.cgi:566. I did not lab that copy.

How the lab was built

Shared image webmin-lab:2.670 from the pin. Entrypoint adds Webmin user limited / lablimited, module useradmin, ACL home=/home/a, ucreate=1, batch=1, makehome=1, autohome off. /home/a exists. /home/ab does not.

Login. GET /useradmin/batch_form.cgi. Multipart POST /useradmin/batch_exec.cgi with source=2 and a create line whose home is /home/ab. Body says Created user h31batch. The directory is there. passwd has the name.

Then POST /useradmin/save_user.cgi with the same home for h31save. The form says you are not allowed to create that home directory. passwd has no h31save. /home/ab is still the batch home.

Plain text
IOC limited-acl home=/home/a ucreate=1 batch=1 autohome=0
IOC batch_exec status=200 created=1 denied=0
IOC batch-home /home/ab exists=1
IOC save_user status=200 denied=1 passwd=0
SUCCESS WEBMIN-H31-BATCH-PREFIX batch-prefix-home save_user-denied WEBMIN-H31-BATCH-PREFIX

Loopback 127.0.0.1:18460. Compose webmin-batch-home-prefix. run.sh clones the tag.

Wrong turns, in the order they happened

Default home=/. That is not a leftover. The KEEP bar is "denied by ACL and still reaches the sink." / denies nothing.

File Manager listing. Closed in 2.670. I wasted a pass confirming the search/chooser prefix was gone, then had to remember batch is a different CGI.

chooser.cgi chroot. Prefix plus .. lists names outside the restricted root. Listing, not user create. Different leftover.

I posted /home/ab to save_user.cgi expecting the same match, because I wanted one oracle. The form denied it. That is the proof the two CGIs disagree. I almost marked the lab FAIL.

autohome=1 skips both checks. I had to strip that key in the entrypoint or the negative would have been noise.

A reverse shell. Theatre.

What an attacker can do

Needs an existing Webmin login that already has Users and Groups, batch create or modify, and a home ACL that does not end in /. They cannot do this with no session. They cannot do this with only the Apache module.

In ordinary words: a coworker who is allowed to make accounts under one directory can make an account whose home sits next to that directory, because the batch importer never put the slash on. The Create User button still stops them.

Privilege Method / sink Recon / integrity / availability Lab-proven or inferred
Create an OS user whose home is a prefix neighbor of the ACL Authenticated POST /useradmin/batch_exec.cgi source=2 batch line create:h31batch:...:/home/ab:... Integrity (passwd + home dir) Lab-proven. Body Created user h31batch. /home/ab exists. /etc/passwd has h31batch:
mkdir that home as the miniserv user check_user then create_home_directory Integrity (directory create) Lab-proven. Home did not exist before the POST
Same prefix on modify + movehome check_user then mv of the old home Integrity (rename home) Inferred from source. Lab oracle is create
/home/a/../ab as a batch home same substr of length(access home) Integrity Inferred from source. is_under_directory rejects ..; check_user does not
Same helper in LDAP Users batch ldap-useradmin/batch_exec.cgi check_user Integrity Inferred. Same raw prefix. Not this lab
Negative control: Create User form POST /useradmin/save_user.cgi with home /home/ab n/a Lab-proven. Denied. No h31save in passwd. /home/ab still the batch home

Stays closed on this leftover

Axis Method / sink Why it is closed
Integrity Single-user create / save save_user.cgi calls is_under_directory
Integrity Trailing-slash ACL home=/home/a/ Raw prefix then fails for /home/ab
Integrity autohome=1 Both CGIs skip the home-root check. Documented grant
Recon Anonymous batch Needs a sid and the useradmin module
Integrity File Manager search / chooser listing Fixed in 2.670
Integrity chooser.cgi chroot prefix Different leftover
Integrity Custom-command injection Fixed in 2.670

The realistic ACL is /home/alice, not /home/a. Batch then matches /home/alice2 and /home/alice-old. The lab used /home/a because six characters make the compare obvious. Same class.

This is not host RCE. This is not a root password. It is an OS user and a directory the ACL said no to, created as the miniserv user, which is usually root.

The fix

Call is_under_directory($access{'home'}, $user->{'home'}) from check_user the way save_user.cgi already does. Do not compare a raw substr of length($access{'home'}). Append the slash, or refuse .., or both. Same change in ldap-useradmin/batch_exec.cgi.

Until then, put a trailing slash on every Users and Groups home ACL that is meant to be a directory root.

References

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