Research/webmin-fastrpc-unauth
0-dayNo CVEHighPublic

Webmin 2.670, fastrpc unauth accept

Webmin 2.670 fastrpc tcpwrite/tcpread accept the first peer on every interface with no session id. Unauthenticated file read and replace as the miniserv user, only while an RPC copy is in flight (up to 30s). Labbed. No CVE yet.

Name
Webmin 2.670, fastrpc unauth accept
Type
0-day analysis
CVE
n/a
CVE Risk
high
Disclosure Status
public
Vendor
Webmin
Affected
Webmin 2.670 (3a2b2b8671c0669c129f5275b3dd1853106ae02b). Needs an RPC-capable session already copying a named file. Data sockets bind INADDR_ANY. Control connection still checks sid.
Published
11 Nov 2026
Updated
11 Nov 2026
Tags
0-day, webmin, fastrpc, missing-auth, cwe-306, unauthenticated, remote

Whoever knocked first got the file.

Webmin has two sockets for a cluster copy. The first one is polite. It checks the session id, and it will not even start unless the account is allowed to make RPC calls. The second one is a firehose. It binds every interface, waits thirty seconds, and hands the named file to the first TCP peer. I did not send a cookie to that port. I did not need to.

If you run Webmin on more than one box and you copy a file between them, someone on the network of the destination can steal that file or overwrite it as the Webmin process - usually root - during the copy. They cannot log into the panel. They cannot start the transfer. They have to arrive before the honest copier, and they have at most thirty seconds.

Lab and proof of concept: abraxas/webmin-fastrpc-unauth (loopback client). Stack in lab/: Dockerfile, docker-compose.yml, run.sh, poc.py. Authorized lab only. Loopback.

This is Webmin 2.670 (3a2b2b8671c0669c129f5275b3dd1853106ae02b). No CVE yet. CWE-306. 8.1 High (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N). Missing auth on the data socket. Remote, unauthenticated, only while an RPC copy is in flight.

This is not a standing open admin. This is not CVE-2026-42210 or CVE-2026-56022.

Why Webmin

I wanted a panel that still ships a first-party RPC, not a plugin. Cluster Copy, Servers Index, remote_write, fastrpc.cgi. That is the product talking to itself over the network as root. The policy at webmin.com/security and the in-tree SECURITY.md say to mail security@webmin.com. No cash table. Fine-grained ACLs are the intended control for "root gave this user a module that can run commands." This leftover is not that. An anonymous peer never received a module.

Pinning 2.670

Latest published tag when I cloned: 2.670, commit 3a2b2b8671c0669c129f5275b3dd1853106ae02b, 2026-09-21. master was hundreds of commits ahead. I hunted the tag people install, not the moving head.

The skip list is half the hunt

Already public, not refiled:

  • 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 / CVE-2022-0829, arbitrary file write CVE-2022-30708, backdoor CVE-2019-15231

Session login, PAM login, xmlrpc.cgi, and the unauthenticated/ static tree did not show a second missing check of this shape. rpc.cgi and xmlrpc.cgi both call webmin_user_can_rpc() before they do anything useful.

Two sockets, one of them forgot the sid

fastrpc.cgi is the fast path for remote_*. First HTTP hit, after the RPC grant, allocates a control port, prints 1 $port $sid $version, forks, and listens. Every later request on that socket is length plus sid plus a serialised hash.

Plain text
my ($len, $auth) = split(/\s+/, $line);
die "Invalid session ID" if ($auth ne $sid);

That check is real. I did not find a way around it. webmin_user_can_rpc() at the top of the CGI is also real. Default ACL rpc=2 is not "anyone." It falls through to the username: root, admin, or sysadm. rpc=0 is a hard no. A user denied RPC cannot open this listener.

Then the file actions. tcpwrite and tcpread fork a child, call allocate_socket on $port + 1, and wait up to 30 seconds:

Plain text
my $sel = select($rmask, undef, undef, 30);
exit if ($sel <= 0);
if (vec($rmask, fileno($tsock), 1)) {
    accept(TRANS, $tsock) || exit;
}

No sid. No source address. No comparison against the control peer. allocate_socket binds INADDR_ANY and in6addr_any, even when miniserv itself is bound to loopback or a single IP. First handshake wins. The child either streams the named file out, or writes whatever arrived into the named path, then prints OK. It runs as the miniserv user. On a default install that is root.

The honest client is remote_write / remote_read. After the RPC call returns [ $file, $tport ], those functions open_socket to that port and send or receive raw file bytes. Cluster Copy calls remote_write for each file. The copier never authenticates the data socket because the server never asked.

In English: the panel still has a lock on the front door. The loading dock is propped open for half a minute whenever someone inside starts a copy.

Wrong turns, in order

I read fastrpc.cgi as an unauthenticated CGI first. It is not. Without webmin_user_can_rpc() you get 0 Invalid user for RPC and no listener.

I looked at xmlrpc.cgi and rpc.cgi for the same missing accept. Both gate on the RPC grant. The leftover is only the data child.

I treated CVE-2026-42210 / CVE-2026-56022 as cousins. Those are Basic-auth 2FA. Different caller, different fix.

I almost wrote this up as a standing unauth file read. The ports are not open when idle. The lab's first oracle is the negative: 10001 through 10004 closed inside the container before any RPC copy.

The first lab client used a User-Agent that started with Webmin. MiniServ answers that with Basic 401 and never sets sid. HTTP/1.1 on session_login.cgi was the other foot-gun. The working client is HTTP/1.0, User-Agent: H02FastrpcLab/1.0, cookie testing=1.

Compose that only publishes 10000 is a miss. allocate_socket walks 10001, 10002, 10003. The data child is not on the HTTP port. The pack publishes 18481-18484 on loopback.

The lab

run.sh clones the pin into lab/src, builds webmin-lab:2.670 from that tree, and brings up compose. Image install is noninteractive setup.sh: ssl=0, login=root, password=labroot, port=10000. The PoC plants /tmp/h02src, confirms the data ports are closed, logs in, hits /fastrpc.cgi, and speaks the control protocol.

YAML
ports:
  - "127.0.0.1:18480:10000"
  - "127.0.0.1:18481:10001"
  - "127.0.0.1:18482:10002"
  - "127.0.0.1:18483:10003"
  - "127.0.0.1:18484:10004"

From lab/docker-compose.yml. lab/Dockerfile copies the pin, runs setup.sh, then:

Dockerfile
EXPOSE 10000
CMD ["/usr/bin/perl", "/usr/share/webmin/miniserv.pl", "--nofork", "/etc/webmin/miniserv.conf"]

Control action tcpread on /tmp/h02src returns a data port. Connecting with no sid dumps the witness. Control action tcpwrite on /tmp/h02dst returns another port. Sending the witness with no sid, then cat of the dest, is the replace.

Plain text
IOC negative-closed 10001,10002,10003,10004
IOC fastrpc status=200 ctype=text/plain body=1 10001 ... 2.670
IOC tcpread-unauth bytes=19 body=WEBMIN-H02-FASTRPC
IOC tcpwrite-unauth ack=OK dest=WEBMIN-H02-FASTRPC
SUCCESS WEBMIN-H02-FASTRPC tcpread-unauth-read tcpwrite-unauth-replace WEBMIN-H02-FASTRPC

What an attacker can do

Needs an RPC-capable session already copying a named file. Default ACL still lets root / admin / sysadm start fastrpc.cgi. A user with rpc=0 cannot open the listener. The window is 30 seconds. First accept() wins. Impact is the file the copier named, as the miniserv user.

Privilege Method / sink Recon / integrity / availability Lab-proven or inferred
Read the named file as the miniserv user connect to the tcpread data port; no sid Recon (file read) Lab-proven. IOC tcpread-unauth bytes=19 body=WEBMIN-H02-FASTRPC
Replace the named destination connect to the tcpwrite data port; send bytes; no sid Integrity (file write) Lab-proven. dest became WEBMIN-H02-FASTRPC
Reach the socket on every interface allocate_socket INADDR_ANY / in6addr_any Recon / integrity Source. Lab published 18483 / 18484 to loopback
Steal or overwrite a Cluster Copy path cluster-copy-lib.pl remote_write Recon / integrity Inferred. Lab used /tmp/h02src and /tmp/h02dst
Negative: idle data ports TCP 10001-10004 before fastrpc.cgi n/a Lab-proven. IOC negative-closed 10001,10002,10003,10004

Stays closed on this leftover

Axis Method / sink Why it is closed
Integrity Control connection without sid fastrpc.cgi:136
Integrity User denied RPC opens the listener webmin_user_can_rpc(); rpc=0 returns 0
Recon Data ports with no copy in flight nothing listens until tcpwrite / tcpread
Integrity Unauthenticated rpc.cgi / xmlrpc.cgi / panel login those paths still check session or RPC grant
Integrity Standing unauth admin, CVE-2026-42210 / CVE-2026-56022 different bug
Integrity Host RCE from this socket the data child reads or writes the named file; it does not eval

The fix

Ask for the RPC sid, or a fresh per-transfer token, on the data socket before touching the file. Bind that socket the way miniserv binds, not INADDR_ANY. Connecting back to the RPC peer is cleaner than accept() of whoever arrives first.

I hunted this as a Medium because the window is short. The score I am using is 8.1 High with AC:H, because when the window is open the peer is unauthenticated and the file is root's.

References

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