Files
webmin/fastrpc.cgi
Ilia Ross 0678fbfa6b Fix Fast RPC transfer authentication
This PR fixes unauthenticated Fast RPC transfer sockets by requiring the transfer connection to present the active RPC session ID before file data moves.

Patched peers use authentication, while mixed-version transfers retain existing behavior for compatibility.

ANT-2026-1G97HEC6: Reported by Adam email on October 11 2026 at 12:08.
2026-10-15 16:36:32 +02:00

445 lines
12 KiB
Perl
Executable File

#!/usr/local/bin/perl
# Handles remote_* function calls by a faster method. When first called
# as a CGI, forks and starts listening on a port which is returned to the
# client. From then on, direct TCP connections can be made to this port
# to send requests and get replies.
BEGIN { push(@INC, "."); };
use WebminCore;
use POSIX;
use Socket;
$main::allow_rpc_only = 1;
$force_lang = $default_lang;
&init_config();
my $DEFAULT_RPC_TIMEOUT = 60;
my $DEFAULT_RPC_IDLE_FACTOR = 6;
print "Content-type: text/plain\n\n";
# Can this user make remote calls?
if (!&webmin_user_can_rpc()) {
print "0 Invalid user for RPC\n";
exit;
}
# Will IPv6 work?
&get_miniserv_config(\%miniserv);
$use_ipv6 = 0;
if ($miniserv{'ipv6'}) {
eval "use Socket6";
$use_ipv6 = 1 if (!$@);
}
# Find a free port
$port = $miniserv{'port'} || 10000;
$aerr = &allocate_socket(MAIN, $use_ipv6 ? MAIN6 : undef, \$port);
if ($aerr) {
print "0 $aerr\n";
exit;
}
if (open(RANDOM, "/dev/urandom")) {
my $tmpsid;
read(RANDOM, $tmpsid, 16);
$sid = lc(unpack('h*', $tmpsid));
close RANDOM;
}
else {
$sid = time()*$$;
}
$version = &get_webmin_version();
print "1 $port $sid $version\n";
# Timeout
my $rpc_idle_factor = $gconfig{'rpc_idle_factor'} || $DEFAULT_RPC_IDLE_FACTOR;
if ($rpc_idle_factor !~ /^\d+$/ || $rpc_idle_factor < 1) {
$rpc_idle_factor = $DEFAULT_RPC_IDLE_FACTOR;
}
my $config_rpc_timeout = $gconfig{'rpc_timeout'} || $DEFAULT_RPC_TIMEOUT;
if ($config_rpc_timeout !~ /^\d+$/ || $config_rpc_timeout < 1) {
$config_rpc_timeout = $DEFAULT_RPC_TIMEOUT;
}
# Fork and listen for calls ..
$pid = fork();
if ($pid < 0) {
die "fork() failed : $!";
}
elsif ($pid) {
exit;
}
untie(*STDIN);
untie(*STDOUT);
# Accept the TCP connection
my $rmask;
vec($rmask, fileno(MAIN), 1) = 1;
if ($use_ipv6) {
vec($rmask, fileno(MAIN6), 1) = 1;
}
$sel = select($rmask, undef, undef, $config_rpc_timeout);
if ($sel <= 0) {
print STDERR "fastrpc[$$]: accept timed out\n"
if ($gconfig{'rpcdebug'});
exit;
}
if (vec($rmask, fileno(MAIN), 1)) {
$acptaddr = accept(SOCK, MAIN);
}
elsif ($use_ipv6 && vec($rmask, fileno(MAIN6), 1)) {
$acptaddr = accept(SOCK, MAIN6);
}
else {
die "No connection on any socket!";
}
die "accept failed : $!" if (!$acptaddr);
my $oldsel = select(SOCK);
$| = 1;
select($oldsel);
my $rcount = 0;
my %xfer_kids;
while(1) {
# Clean up the list of waiting sub-processes
foreach my $p (keys %xfer_kids) {
my $waited_pid = waitpid($p, POSIX::WNOHANG());
delete($xfer_kids{$p}) if ($waited_pid > 0 || $waited_pid == -1);
}
# Wait for the request. Wait longer if this isn't the first one
my $rmask;
vec($rmask, fileno(SOCK), 1) = 1;
my $timeout = $rcount
? $config_rpc_timeout * $rpc_idle_factor
: $config_rpc_timeout;
my $sel = select($rmask, undef, undef, $timeout);
if ($sel <= 0) {
# Don't kill the control session while a tcpwrite/tcpread is
# running
my $nk = scalar(keys %xfer_kids);
if ($nk) {
print STDERR "fastrpc[$$]: idle timeout, but $nk ".
"transfer(s) active: ".
join(",", sort keys %xfer_kids)."\n"
if ($gconfig{'rpcdebug'});
next;
}
print STDERR "fastrpc[$$]: session timed out\n"
if ($gconfig{'rpcdebug'});
last;
}
my $line = <SOCK>;
last if (!$line);
my ($len, $auth) = split(/\s+/, $line);
die "Invalid session ID" if ($auth ne $sid);
my $rawarg;
while(length($rawarg) < $len) {
my $got;
my $rv = read(SOCK, $got, $len - length($rawarg));
exit if ($rv <= 0);
$rawarg .= $got;
}
print STDERR "fastrpc[$$]: raw $rawarg\n" if ($gconfig{'rpcdebug'});
my $dumper = substr($rawarg, 0, 5) eq '$VAR1' ? 1 : 0;
my $arg = &unserialise_variable($rawarg);
# Process it
my $rv;
if ($arg->{'action'} eq 'ping') {
# Just respond with an OK
print STDERR "fastrpc[$$]: ping\n" if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1 };
}
elsif ($arg->{'action'} eq 'check') {
# Check if some module is supported
print STDERR "fastrpc[$$]: check $arg->{'module'}\n" if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1,
'rv' => &foreign_check($arg->{'module'}, undef, undef,
$arg->{'api'}) };
}
elsif ($arg->{'action'} eq 'config') {
# Get the config for some module
print STDERR "fastrpc[$$]: config $arg->{'module'}\n" if ($gconfig{'rpcdebug'});
my %config = &foreign_config($arg->{'module'});
$rv = { 'status' => 1, 'rv' => \%config };
}
elsif ($arg->{'action'} eq 'write') {
# Transfer data to a my temp file
my $file = $arg->{'file'} ? $arg->{'file'} :
$arg->{'name'} ? &tempname($arg->{'name'}) :
&tempname();
print STDERR "fastrpc[$$]: write $file\n" if ($gconfig{'rpcdebug'});
open(FILE, ">$file");
binmode(FILE);
print FILE $arg->{'data'};
close(FILE);
$rv = { 'status' => 1, 'rv' => $file };
}
elsif ($arg->{'action'} eq 'tcpwrite') {
# Transfer data to a my temp file over TCP connection
my $file = $arg->{'file'} ? $arg->{'file'} :
$arg->{'name'} ? &tempname($arg->{'name'}) :
&tempname();
print STDERR "fastrpc[$$]: tcpwrite $file\n" if ($gconfig{'rpcdebug'});
my $tsock = time().$$;
my $tsock6 = $use_ipv6 ? time().$$."v6" : undef;
my $tport = $port + 1;
&allocate_socket($tsock, $tsock6, \$tport);
my $cpid = fork();
if (!defined($cpid)) {
$rv = { 'status' => 0, 'rv' => "fork() failed : $!" };
}
elsif ($cpid == 0) {
close(SOCK);
close(MAIN);
close(MAIN6) if ($use_ipv6);
# Accept connection in separate process
print STDERR "fastrpc[$$]: tcpwrite $file port $tport\n"
if ($gconfig{'rpcdebug'});
my $rmask;
vec($rmask, fileno($tsock), 1) = 1;
if ($use_ipv6) {
vec($rmask, fileno($tsock6), 1) = 1;
}
my $sel = select($rmask, undef, undef, 30);
exit if ($sel <= 0);
if (vec($rmask, fileno($tsock), 1)) {
accept(TRANS, $tsock) || exit;
}
elsif ($use_ipv6 && vec($rmask, fileno($tsock6), 1)) {
accept(TRANS, $tsock6) || exit;
}
if ($arg->{'xferauth'}) {
my $auth = <TRANS>;
exit if (!defined($auth));
chomp($auth);
exit if ($auth ne $sid);
}
print STDERR "fastrpc[$$]: tcpwrite $file accepted\n" if ($gconfig{'rpcdebug'});
my $buf;
my $err;
if (open(FILE, ">$file")) {
binmode(FILE);
print STDERR "fastrpc[$$]: tcpwrite $file writing\n" if ($gconfig{'rpcdebug'});
my $bs = &get_buffer_size();
while(read(TRANS, $buf, $bs) > 0) {
my $ok = (print FILE $buf);
if (!$ok) {
$err = "Write to $file failed : $!";
last;
}
}
close(FILE);
print STDERR "fastrpc[$$]: tcpwrite $file written\n" if ($gconfig{'rpcdebug'});
}
else {
print STDERR "fastrpc[$$]: tcpwrite $file open failed $!\n" if ($gconfig{'rpcdebug'});
$err = "Failed to open $file : $!";
}
print TRANS $err ? "$err\n" : "OK\n";
close(TRANS);
exit;
}
else {
$xfer_kids{$cpid} = 1;
print STDERR "fastrpc[$$]: tcpwrite $file started\n"
if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1,
'rv' => [ $file, $tport,
$arg->{'xferauth'} ? $sid : undef ] };
}
close($tsock);
close($tsock6) if ($use_ipv6);
}
elsif ($arg->{'action'} eq 'read') {
# Transfer data from a file
print STDERR "fastrpc[$$]: read $arg->{'file'}\n" if ($gconfig{'rpcdebug'});
my ($data, $got);
open(FILE, "<$arg->{'file'}");
binmode(FILE);
my $bs = &get_buffer_size();
while(read(FILE, $got, $bs) > 0) {
$data .= $got;
}
close(FILE);
$rv = { 'status' => 1, 'rv' => $data };
}
elsif ($arg->{'action'} eq 'tcpread') {
# Transfer data from a file over TCP connection
print STDERR "fastrpc[$$]: tcpread $arg->{'file'}\n" if ($gconfig{'rpcdebug'});
if (-d $arg->{'file'}) {
$rv = { 'status' => 1, 'rv' => [ undef, "$arg->{'file'} is a directory" ] };
}
elsif (!open(FILE, "<$arg->{'file'}")) {
$rv = { 'status' => 1, 'rv' => [ undef, "Failed to open $arg->{'file'} : $!" ] };
}
else {
binmode(FILE);
my $tsock = time().$$;
my $tsock6 = $use_ipv6 ? time().$$."v6" : undef;
my $tport = $port + 1;
&allocate_socket($tsock, $tsock6, \$tport);
my $cpid = fork();
if (!defined($cpid)) {
$rv = { 'status' => 0, 'rv' => "fork() failed : $!" };
}
elsif ($cpid == 0) {
close(SOCK);
close(MAIN);
close(MAIN6) if ($use_ipv6);
# Accept connection in separate process
my $rmask;
vec($rmask, fileno($tsock), 1) = 1;
if ($use_ipv6) {
vec($rmask, fileno($tsock6), 1) = 1;
}
my $sel = select($rmask, undef, undef, 30);
exit if ($sel <= 0);
if (vec($rmask, fileno($tsock), 1)) {
accept(TRANS, $tsock) || exit;
}
elsif (vec($rmask, fileno($tsock6), 1)) {
accept(TRANS, $tsock6) || exit;
}
if ($arg->{'xferauth'}) {
my $auth = <TRANS>;
exit if (!defined($auth));
chomp($auth);
exit if ($auth ne $sid);
}
my $buf;
while(read(FILE, $buf, 1024) > 0) {
print TRANS $buf;
}
close(FILE);
close(TRANS);
exit;
}
else {
$xfer_kids{$cpid} = 1;
print STDERR "fastrpc[$$]: tcpread $arg->{'file'} ".
"started\n"
if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1,
'rv' => [ $arg->{'file'}, $tport,
$arg->{'xferauth'} ? $sid : undef ] };
}
close(FILE);
close($tsock);
close($tsock6) if ($use_ipv6);
}
}
elsif ($arg->{'action'} eq 'require') {
# require a library
print STDERR "fastrpc[$$]: require $arg->{'module'}/$arg->{'file'}\n" if ($gconfig{'rpcdebug'});
eval {
&foreign_require($arg->{'module'},
$arg->{'file'});
};
if ($@) {
print STDERR "fastrpc[$$]: require error $@\n" if ($gconfig{'rpcdebug'});
$rv = { 'status' => 0, 'rv' => $@ };
}
else {
print STDERR "fastrpc[$$]: require done\n" if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1 };
}
}
elsif ($arg->{'action'} eq 'call') {
# execute a function
print STDERR "fastrpc[$$]: call $arg->{'module'}::$arg->{'func'}(",join(",", @{$arg->{'args'}}),")\n" if ($gconfig{'rpcdebug'});
my @rv;
eval {
local $main::error_must_die = 1;
@rv = &foreign_call($arg->{'module'},
$arg->{'func'},
@{$arg->{'args'}});
};
if ($@) {
print STDERR "fastrpc[$$]: call error $@\n" if ($gconfig{'rpcdebug'});
$rv = { 'status' => 0, 'rv' => $@ };
}
elsif (@rv == 1) {
$rv = { 'status' => 1, 'rv' => $rv[0] };
}
else {
$rv = { 'status' => 1, 'arv' => \@rv };
}
print STDERR "fastrpc[$$]: call $arg->{'module'}::$arg->{'func'} done = ",join(",", @rv),"\n" if ($gconfig{'rpcdebug'});
}
elsif ($arg->{'action'} eq 'eval') {
# eval some perl code
print STDERR "fastrpc[$$]: eval $arg->{'module'} $arg->{'code'}\n" if ($gconfig{'rpcdebug'});
my $erv;
if ($arg->{'module'}) {
my $pkg = $arg->{'module'};
$pkg =~ s/[^A-Za-z0-9]/_/g;
$erv = eval "package $pkg;\n".
$arg->{'code'}."\n";
}
else {
$erv = eval $arg->{'code'};
}
print STDERR "fastrpc[$$]: eval $arg->{'module'} $arg->{'code'} done = $rv error = $@\n" if ($gconfig{'rpcdebug'});
if ($@) {
$rv = { 'status' => 0, 'rv' => $@ };
}
else {
$rv = { 'status' => 1, 'rv' => $erv };
}
}
elsif ($arg->{'action'} eq 'quit') {
print STDERR "fastrpc[$$]: quit\n" if ($gconfig{'rpcdebug'});
$rv = { 'status' => 1 };
}
else {
print STDERR "fastrpc[$$]: unknown $arg->{'action'}\n" if ($gconfig{'rpcdebug'});
$rv = { 'status' => 0 };
}
$rawrv = &serialise_variable($rv, $dumper);
# Send back to the client
print SOCK length($rawrv),"\n";
print SOCK $rawrv;
last if ($arg->{'action'} eq 'quit');
$rcount++;
}
# allocate_socket(handle, ipv6-handle, &port)
sub allocate_socket
{
my ($fh, $fh6, $port) = @_;
my $proto = getprotobyname('tcp');
if (!socket($fh, PF_INET, SOCK_STREAM, $proto)) {
return "socket failed : $!";
}
setsockopt($fh, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
if ($fh6) {
if (!socket($fh6, PF_INET6(), SOCK_STREAM, $proto)) {
return "socket6 failed : $!";
}
setsockopt($fh6, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
setsockopt($fh6, 41, 26, pack("l", 1)); # IPv6 only
}
while(1) {
$$port++;
if ($$port < 0 || $$port > 65535) {
return "Failed to allocate a free port number: $$port";
}
$pack = pack_sockaddr_in($$port, INADDR_ANY);
next if (!bind($fh, $pack));
if ($fh6) {
$pack6 = pack_sockaddr_in6($$port, in6addr_any());
next if (!bind($fh6, $pack6));
}
last;
}
listen($fh, SOMAXCONN) || return "listen failed : $!";
if ($fh6) {
listen($fh6, SOMAXCONN) || return "listen6 failed : $!";
}
return undef;
}