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.
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:
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.
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:
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.
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-FASTRPCWhat 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
- github.com/webmin/webmin tag 2.670 (
3a2b2b8671c0669c129f5275b3dd1853106ae02b) fastrpc.cgiweb-lib-funcs.plremote_write,remote_read,webmin_user_can_rpccluster-copy/cluster-copy-lib.pl- webmin.com/security
- CWE-306
- Pack: github.com/abraxas/webmin-fastrpc-unauth
@abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.