the-bastion/bin/shell/connect.pl
2022-12-30 17:52:42 +01:00

244 lines
9.6 KiB
Perl
Executable file

#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
# this line absolutely needs to be sync with the exec() of osh.pl
# that is launching us. we don't use GetOpts or such, as this is not
# user-modifiable anyway. We're mainly passing parameters we will need
# in this short script. some of them can sometimes be undef. this is normal.
my ($ip, $port, $sshClientHasOptionE, $userPasswordClue, $saveFile, $insert_id, $db_name, $uniq_id, $self, @command) =
@ARGV;
# on signal (HUP happens a lot), still try to log in db
sub exit_sig {
my ($sig) = @_;
if (defined $insert_id and defined $db_name) {
# at that point, we might not have required the proper libs yet, do it
require File::Basename;
require '' ## no critic (BarewordIncludes) ## I trust __FILE__, no worries
. File::Basename::dirname(__FILE__) . '/../../lib/perl/OVH/Bastion.pm';
# and log
OVH::Bastion::log_access_update(
account => $self,
insert_id => $insert_id,
db_name => $db_name,
uniq_id => $uniq_id,
signal => $sig,
);
}
# nullify my own handlers so that they don't get re-executed when my parent
# exits because of the signal, and I get sent back a SIGHUP (see Prctl below)
$SIG{$_} = 'IGNORE' for qw{ INT HUP TERM SEGV };
# signal my current process group
kill $sig, 0;
exit(117); # EXIT_GOT_SIGNAL
}
$SIG{$_} = \&exit_sig for qw{ INT HUP TERM SEGV };
# beautify for ps
local $0 = '' . __FILE__ . ' ' . join(' ', @command);
# set signal for when my parent dies (Linux only)
eval {
require Linux::Prctl; # pragma optional module
# 1 is SIGHUP
Linux::Prctl::set_pdeathsig(1);
};
# As we're going to system() something passed to us via @ARGV,
# we want to be sure we're being called by something we know.
# Yes. I'm fucking paranoid.
if (open(my $fh, '<', "/proc/" . getppid() . '/cmdline')) {
my $cmdline = do { local $/ = undef; <$fh> };
close($fh);
my @pargv = split(/\x00/, $cmdline);
# now check our parent infos.
# regular case: ssh
if (@pargv == 1 and $pargv[0] =~ /^sshd: /) {
; # ok, our parent is sshd, legitimate use
}
# pingssh case
elsif (@pargv == 4 and $pargv[0] =~ m{/perl$} and $pargv[1] =~ m{/osh\.pl} and $pargv[2] eq '-c') {
; # ok pingssh case
}
# admin debug case: local su
elsif (@pargv == 5 and $pargv[0] eq 'su' and $pargv[1] eq '-l' and $pargv[3] eq '-c') {
print STDERR "\n\nHmm, hijack of "
. $pargv[2]
. " by root detected... debug I guess... okay, but it's really because it's you.\n\n";
}
# mosh
elsif ($pargv[0] eq 'mosh-server') {
; # we're being called by mosh-server, alrighty
}
# clush plugin
elsif ($pargv[1] =~ m{^/opt/bastion/bin/plugin/(open|restricted)/clush$}) {
; # we're being called by the clush plugin, ok
}
# interactive mode: our parent is osh.pl
elsif ($pargv[0] eq 'perl' and $pargv[1] eq '/opt/bastion/bin/shell/osh.pl') {
; # we're being called by the interactive mode of osh.pl, ok
}
# else: it sucks.
else {
#foreach (@pargv) { print "<".$_.">\n" };
die("SECURITY VIOLATION, ABORTING.");
}
}
else {
; # grsec can deny us this. if that's the case, nevermind ... bypass this check
}
# in any case, force this
if (-e '/usr/local/bin/ttyrec') {
$command[0] = '/usr/local/bin/ttyrec';
}
else {
$command[0] = '/usr/bin/ttyrec';
}
# then finally launch the command !
my $sysret = system(@command);
# ... days or months may have passed once we arrive here, which is
# why we only used common::sense above (which is known to be light).
# using other packages would just waste memory for months as we would
# only really use them AFTER the command above has exited.
# so. now, we can require those files we need, rejoice, we have
# saved a lot of RAM in the meantime !
# special case for Time::HiRes, use a `use' instead of a `require'
# in an attempt to fix a strange 'Undefined subroutine &Time::HiRes::gettimeofday'
# that happens one every 10K connections or so
use Time::HiRes qw{ gettimeofday };
my ($timestamp, $timestampusec) = gettimeofday();
require File::Basename;
require '' ## no critic (BarewordIncludes) ## I trust __FILE__, no worries
. File::Basename::dirname(__FILE__) . '/../../lib/perl/OVH/Bastion.pm';
# ssh -E also silences normal errors on console, print them eventually
if ($sshClientHasOptionE) {
if (open(my $sshdebug, '<', $saveFile . '.sshdebug')) {
while (<$sshdebug>) {
print
unless
/^debug|^key_load_public:|OpenSSL|^Authenticated to|^Transferred:|^Bytes per second:|^\s*$|client-session/;
}
close($sshdebug);
}
}
# now guessify if the ssh worked or not
my @comments;
my $header;
if (open(my $fh_ttyrec, '<', $saveFile)) {
read $fh_ttyrec, $header, 2000; # 2K if there's the host key changed warning
close($fh_ttyrec);
}
elsif (-r "$saveFile.zst") {
my $fnret =
OVH::Bastion::execute(cmd => ['zstd', '-d', '-c', "$saveFile.zst"], max_stdout_bytes => 2000, must_succeed => 1);
$header = join("\n", @{$fnret->value->{'stdout'} || []}) if $fnret;
}
if ($header) {
if ($header =~ /Permission denied \(publickey/) {
push @comments, 'permission_denied';
OVH::Bastion::osh_crit(
"BASTION SAYS: The remote server ($ip) refused all the keys we tried (see the list just above), there are FOUR things to verify:"
);
OVH::Bastion::osh_warn(
"1) Check the remote account's authorized_keys on $ip, did you add the proper key there? (personal key or group key)\n2) Did you tell the bastion you added a key to the remote server, so it knows it has to use it? See the actually used keys just above. If you didn't, do it with selfAddPersonalAccess or groupAddServer.\n3) Check the from=\"\" part of the remote account's authorized_keys' keyline. Are all the bastion IPs present? Master and slave(s)? See groupInfo or selfListEgressKeys to get the proper keyline to copy/paste.\n4) Did you check the 3 above points carefully? Really? Because if you did, you wouldn't be reading this 4th bullet point, as your problem would already be fixed ;)"
);
}
if ($header =~ /Permission denied \(keyboard-interactive/) {
push @comments, 'permission_denied';
if (!OVH::Bastion::config('keyboardInteractiveAllowed')->value) {
OVH::Bastion::osh_crit(
"BASTION SAYS: The remote server ($ip) wanted to use keyboard-interactive authentication, but it's not enabled on this bastion!"
);
}
}
if ($header =~ /Too many authentication failures/) {
push @comments, 'too_many_auth_fail';
OVH::Bastion::osh_crit(
"BASTION SAYS: The remote server ($ip) disconnected us before we got a chance to try all the keys we wanted to (see the list just above)."
);
OVH::Bastion::osh_warn(
"This usually happens if there are too many keys to try, for example if you have numerous personal keys of if $ip is in many groups you have access to."
);
OVH::Bastion::osh_warn(
"Either reduce the number of keys to try, or modify $ip\'s sshd \"MaxAuthTries\" configuration option.");
}
push @comments, 'hostkey_changed' if $header =~ /IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY/;
push @comments, 'hostkey_saved' if $header =~ /Warning: Permanently added /;
if ($header =~ /ssh: connect to host \S+ port \d+: Connection timed out/) {
push @comments, 'connection_timeout';
}
elsif ($header =~ /ssh: connect to host \S+ port \d+: Connection refused/) {
push @comments, 'connection_refused';
}
elsif ($header =~ /ssh: connect to host \S+ port \d+: /) {
push @comments, 'connection_error';
}
elsif ($header =~ /authentication is disabled to avoid man-in-the-middle attacks/) {
push @comments, 'passauth_disabled';
# be nice and explain to the user cf ticket BASTION-10
if ($userPasswordClue) {
my $bastionName = OVH::Bastion::config('bastionName')->value;
OVH::Bastion::osh_crit(
"BASTION SAYS: Password authentication is blocked because of the hostkey mismatch on $ip.");
OVH::Bastion::osh_crit(
"If you are aware of this change, remove the hostkey cache with `$bastionName --osh selfForgetHostKey --host $ip --port $port'"
);
}
}
# if strict host key checking is enabled, be nice and explain how to remove this error
if ($header =~ /you have requested strict checking/) {
my $bastionName = OVH::Bastion::config('bastionName')->value;
OVH::Bastion::osh_crit("BASTION SAYS: Connection has been blocked because of the hostkey mismatch on $ip.");
OVH::Bastion::osh_crit(
"If you are aware of this change, remove the hostkey cache with `$bastionName --osh selfForgetHostKey --host $ip --port $port'"
);
}
}
else {
push @comments, 'ttyrec_error';
}
# update our sql line if we successfully inserted it back in osh.pl
OVH::Bastion::log_access_update(
account => $self,
insert_id => $insert_id,
db_name => $db_name,
uniq_id => $uniq_id,
returnvalue => $sysret,
comment => @comments ? join(' ', @comments) : undef,
timestampend => $timestamp,
timestampendusec => $timestampusec,
);
if ($sysret == -1) {
OVH::Bastion::osh_crit("Couldn't start " . join('|', @command) . ($! ? " ($!)" : ", is it installed?"));
exit($sysret);
}
exit($sysret >> 8);