[snapshot] hs-unix-listener hs12/hs-unix-listener
authorHeiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Thu, 23 Mar 2017 15:59:17 +0000 (16:59 +0100)
committerHeiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Fri, 24 Mar 2017 21:54:28 +0000 (22:54 +0100)
heiko/README
heiko/client-server/X/IO/Socket/UNIX.pm [new file with mode: 0644]
heiko/client-server/sendmail.client [new file with mode: 0755]
heiko/client-server/sendmail.server [new file with mode: 0755]
heiko/reproducer [new file with mode: 0755]
heiko/sendmail
heiko/sendmail.not-used [new file with mode: 0755]

index a8311eb215f4921a3aded961640c6889afbdcae1..80acb7a27480807442f8c03822dc551e02f93300 100755 (executable)
@@ -1,14 +1,56 @@
-#!/bin/sh
-# This is the reproducer. Call it as a shell script.
-# To test the sendmail wrapper call it as
-# ./README <wrapper>
+Exim is installed suid=root for several reasons. (I'm not happy with
+this, but currently it is the status quo).
 
-set -e
+Problem
+-------
 
-user=${USER-$LOGNAME}
-test -z "$user" && user=$(id -un)
+In environments that bounds the capabilities of a process (like
+apache-mod-itk, probably in some systemd environments too) Exim can't be
+called the usual way.
 
-sendmail=${1:-/usr/sbin/sendmail}
+In such capability bounding environments, the setuid/setgid
+system calls do not work anymore.
 
+Solution
+--------
 
-exec sudo capsh --secbits=0x01 -- -c "date | /usr/sbin/sendmail $user"
+During the last days I tried several approaches to solve the
+above problem.
+
+1) Create a client/server couple to get some kind of privilege
+   separation. In a Linux environment, the server can even get
+   the UID of the peer on a UNIX socket. This way we can mimic
+   the behaviour of an Exim called by a local user.
+
+   The work in client-server/ is work in progress. You can ignore it
+   for now.
+
+2) Use a SSH connection to do the privilege separation
+   See the 'sendmail' script. It needs a small initialization
+   for each user that want's to use it.
+
+   This may be *insecure* if you lose your private key *and*
+   the attacker can modify the 'authorized_keys' file. But if he can
+   modify this file, he doesn't need your key anyway.
+
+   But - in environments where the user get's only a limited sftp-only
+   environment, this approach doesn't work anymore.
+
+3) Set the file capabilities on the Exim binary:
+
+    setcap \
+        cap_chown,cap_dac_override,cap_fowner,cap_setgid,cap_setuid+ep \
+        /usr/sbin/exim4
+
+
+   *WARNING* I'm not sure about the security implications
+   It seems, that with this setting we do not need setuid=root anymore!
+
+   I believe, with this setting Exim can override
+
+Conclusion
+----------
+
+From a security point of view, the (1)  solution would be the best,
+because I'm not sure about the implications of (3), and I'm sure about
+the security problems of (2).
diff --git a/heiko/client-server/X/IO/Socket/UNIX.pm b/heiko/client-server/X/IO/Socket/UNIX.pm
new file mode 100644 (file)
index 0000000..fcc6ad3
--- /dev/null
@@ -0,0 +1,58 @@
+package X::IO::Socket::UNIX;
+
+use parent 'IO::Socket::UNIX';
+use strict;
+use warnings;
+
+sub new {
+    my $class = ref $_[0] ? ref shift : shift;
+    my %opt = @_;
+    {
+        no strict 'refs';
+        no warnings 'redefine';
+        *{ __PACKAGE__ . '::debug' } = delete $opt{Debug}
+          ? sub {
+            say STDERR map { s/\n/\\n/gr =~ s/\r/\\r/gr } @_;
+          }
+          : sub { };
+    }
+    return $class->SUPER::new(%opt);
+}
+
+sub say {
+    my $self = shift;
+    debug("-> @_");
+    CORE::say {$self} @_;
+    return $self;
+}
+
+sub print {
+    my $self = shift;
+    debug("-> @_");
+    CORE::print {$self} @_;
+    return $self;
+}
+
+sub expect {
+    my $self    = shift;
+    my $pattern = shift;
+    $pattern = qr/^$pattern/ unless ref $pattern;
+    my @got;
+    while (<$self>) {
+        chomp;
+        my @found = ();
+
+        debug("<- $_");
+        die "$0: unexpected response:\n$_\n"
+          if not @found = /($pattern)/;
+
+        push @got, @found[1 .. $#found];
+
+        /^\d+-/ and next;
+        last;
+    }
+    return @got;
+}
+
+1;
+
diff --git a/heiko/client-server/sendmail.client b/heiko/client-server/sendmail.client
new file mode 100755 (executable)
index 0000000..a800c14
--- /dev/null
@@ -0,0 +1,40 @@
+#!/usr/bin/perl
+#??
+#ssh root@localhost exec -n $0 exim "$@"
+use v5.10.1;
+use strict;
+use warnings;
+use feature 'say';
+use experimental 'smartmatch';
+use Getopt::Long;
+
+use FindBin '$Bin';
+use lib $Bin;
+
+use X::IO::Socket::UNIX;
+
+## MAIN
+
+GetOptions('debug' => \my $debug,)
+  or die;
+
+my $peer = "\0/tmp/S.exim";
+my $socket = X::IO::Socket::UNIX->new(Debug => $debug, Peer => $peer) or die $@;
+
+$socket->expect(2);
+$socket->say("AUTH uid=$> gid=$)")->expect(2);
+
+my $args = join "\0", $0, @ARGV;
+$socket->say('ARGS size=' . length($args))->expect(2);
+$socket->print($args)->expect(2);
+
+$socket->say('DATA')->expect(3);
+
+while (<STDIN>) {
+    $socket->print($_);
+}
+$socket->shutdown(1);
+$socket->expect(2);
+$socket->close;
+
+# vim:ft=perl:
diff --git a/heiko/client-server/sendmail.server b/heiko/client-server/sendmail.server
new file mode 100755 (executable)
index 0000000..f488b7c
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/perl
+use v5.10.1;
+use strict;
+use warnings;
+use experimental 'smartmatch';
+
+use FindBin '$Bin';
+use lib $Bin;
+
+use X::IO::Socket::UNIX;
+
+my $path = "\0/tmp/S.exim";
+say "using $path";
+
+if ($path !~ /^\0/) {
+    unlink $path
+    or !-e $path
+    or die "$0: Can't unlink $path: $!\n";
+}
+
+my $listener = X::IO::Socket::UNIX->new(
+    Debug  => 1,
+    Listen => 1,
+    Local  => $path,
+) or die $@;
+
+END {
+    unlink $listener if $listener;
+}
+
+while (my $connection = $listener->accept) {
+    my $pid = fork // die "$0: Can't fork: $!\n";
+
+    if ($pid) {
+        close $connection;
+        next;
+    }
+
+    exit handle_connection($connection);
+}
+
+exit 0;
+
+sub handle_connection {
+    my $connection = shift;
+
+    $connection->say('200 READY');
+
+    my ($user_id, $group_id) = $connection->expect('AUTH uid=(\d+) gid=(\d+)');
+    my $user_name  = getpwuid($user_id);
+    my $group_name = getgrgid($group_id);
+
+    $connection->say("200 AUTH OK for $user_name:$group_name");
+    {
+        my ($chunksize) = $connection->expect('ARGS size=(\d+)');
+        local $/ = \$chunksize;
+        $connection->say(sprintf "200 ARGS OK for %d octets", $chunksize);
+        defined($_ = <$connection>) and length == ${$/}
+          or die sprintf "$0: expected %d, got %d\n",
+          ${$/}, length;
+    }
+    $connection->say('200 ARGS OK, got ', length, ' bytes');
+
+    # now we have the information we need to startup the
+    # local receiver process
+
+    $connection->expect('DATA');
+    $connection->say('300 OK');
+
+    while (<$connection>) {
+        print STDERR "* $_";
+    }
+    $connection->say('200 OK');
+}
+
+# vim:ft=perl:
diff --git a/heiko/reproducer b/heiko/reproducer
new file mode 100755 (executable)
index 0000000..5fa3516
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/sh
+# This is the reproducer. Call it as a shell script.
+# To test the sendmail wrapper call it as
+# ./reproducer [<wrapper>]
+
+set -e
+
+user=${USER-$LOGNAME}
+test -z "$user" && user=$(id -un)
+
+sendmail=${1:-/usr/sbin/sendmail}
+exec sudo capsh --secbits=0x01 -- -c "date | $sendmail $user"
+
index b39cc76e7d249fefa8c99ad576e840d7c499ee0d..30e1a6746f0544656b3d7b9ad61b60053f0d27c0 100755 (executable)
@@ -1,98 +1,31 @@
-#!/usr/bin/env perl
+#!/bin/bash
+#
+# poor man's privilege separation/escalation
+# We use this to escape the capabily bounds
+#
+# THIS IS UNSECURE! If an attacker gets the private
+# SSH key we use, he can login via SSH into your
+# account! The SSH key is *unprotected*
 
-use strict;
-use warnings;
-use feature qw'say';
-use IO::Socket::INET;
-use Pod::Usage;
-use Getopt::Long;
-use Sys::Hostname;
+set -e
 
-exit main() unless caller;
+EXIM=/usr/sbin/exim
 
-package X::IO::Socket::INET {
-    use parent 'IO::Socket::INET';
-    use strict;
-    use warnings;
+id_file="$HOME/.ssh/id_rsa-exim"
+this=$(pwd)/$(basename $0)
 
-    sub new {
-        my $class = ref $_[0] ? ref shift : shift;
-        my %opt = @_;
-        {
-            no strict 'refs';
-            *{__PACKAGE__ .'::debug'} = delete $opt{Debug} ? sub { say STDERR @_ } : sub {};
-        }
-        return $class->SUPER::new(%opt);
-    }
+if test "$1" = "init"; then
+    ssh-keygen -C 'exim caller' -f "$id_file" -N ''
+    echo 'from="127.0.0.1,::1"' "$(<$id_file.pub)" >> "$HOME/.ssh/authorized_keys"
+    ssh -i "$id_file" "$USER@localhost" echo OK && exit 0
+    echo "$0: Something went wrong!" >&2
+    exit 1
+fi
 
-    sub say {
-        my $self = shift;
-        debug("-> @_");
-        say {$self} @_;
-    }
+# yes, bash knows 'exec -a', but do we get a bash after
+# login?
+ssh -i "$id_file" "$USER@localhost" \
+    perl -e \'exec {q{\'$EXIM\'}} @ARGV\' -- "$this" "$@"
 
-    sub expect {
-        my $self = shift;
-        my $pattern = shift;
-        $pattern = qr/^$pattern/ unless ref $pattern;
-        my @got;
-        while (<$self>) {
-            chomp;
-            push @got, $_;
-            debug("<- $_");
-            die "$0: unexpected\n"
-                if not /$pattern/;
-            /^\d+-/ and next;
-            last;
-        }
-        return @got;
-    }
-    1;
-}
 
-sub main {
-
-    my @rcpts;
-    my ($opt_sendmail_t, $opt_from, $opt_debug);
-
-    my $host = hostname or die "$0: Can't get local hostname\n";
-    my $user = getpwuid($<) or die "$0: Can't get user name\n";
-    my $from = "$user\@$host";
-
-    GetOptions(
-        'debug' => \$opt_debug,
-        't'     => \$opt_sendmail_t,
-        'f=s'   => \$opt_from,
-    ) or pod2usage;
-    @rcpts = @ARGV;
-
-    my $socket = X::IO::Socket::INET->new(
-        PeerAddr => 'localhost',
-        PeerPort => 'smtp',
-        Debug => $opt_debug,
-    ) or die "$0: socket: $!\n";
-
-    $socket->expect(2);
-    $socket->say('EHLO ' . hostname());
-
-    $socket->expect(2);
-    $socket->say("MAIL FROM:<$from>");
-
-    $socket->expect(2);
-
-
-    return 0;
-}
-
-__END__
-
-=head1 NAME
-
- sendmail - sendmail drop in
-
-=head1 SYNOPSIS
-
- sendmail [-f <sender>] <recipient>...
- sendmail -t
-
-=cut
+# vim:ft=sh:
diff --git a/heiko/sendmail.not-used b/heiko/sendmail.not-used
new file mode 100755 (executable)
index 0000000..98c62ea
--- /dev/null
@@ -0,0 +1,136 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use v5.10.1;
+use feature qw'say';
+
+use Pod::Usage;
+use Getopt::Long;
+use Sys::Hostname;
+
+exit main() unless caller;
+
+package X::IO::Socket::INET {
+
+    use parent 'IO::Socket::INET';
+    use strict;
+    use warnings;
+
+    sub new {
+        my $class = ref $_[0] ? ref shift : shift;
+        my %opt = @_;
+        {
+            no strict 'refs';
+            *{__PACKAGE__ .'::debug'} = delete $opt{Debug} ? sub { say STDERR @_ } : sub {};
+        }
+        return $class->SUPER::new(%opt);
+    }
+
+    sub say {
+        my $self = shift;
+        debug("-> @_");
+        say {$self} @_;
+    }
+
+    sub expect {
+        my $self = shift;
+        my $pattern = shift;
+        $pattern = qr/^$pattern/ unless ref $pattern;
+        my @got;
+        while (<$self>) {
+            chomp;
+            push @got, $_;
+            debug("<- $_");
+            die "$0: unexpected response:\n$_\n"
+                if not /$pattern/;
+            /^\d+-/ and next;
+            last;
+        }
+        return @got;
+    }
+    1;
+}
+
+sub main {
+
+    my @rcpts;
+
+    # Mimic the Exim behaviour, to append $qualify_domain and
+    my ($qualify_sender, $qualify_recipient)
+        #= exim_vars(qw'qualify_domain qualify_recipient');
+        = (hostname, hostname);
+
+    my %o = (
+        from => do {
+            getpwuid($<) or die "$0: Can't get user name\n" },
+    );
+
+    GetOptions(
+        \%o,
+        'debug',
+        't',
+        'oi|i',
+        'from=s'
+    ) or pod2usage;
+    @rcpts = splice @ARGV, 0, @ARGV, ();
+
+    # qualify sender and recipient(s)
+    $o{from} .= "\@$qualify_sender" unless $o{from} =~ /@/;
+    $_ .= "\@$qualify_recipient" foreach grep !/@/, @rcpts;
+
+    my $socket = X::IO::Socket::INET->new(
+        PeerAddr => 'localhost',
+        PeerPort => 'smtp',
+        Debug => $o{debug},
+    ) or die "$0: socket: $!\n";
+
+    $socket->expect(2);
+
+    $socket->say('EHLO ' . hostname());
+    $socket->expect(2);
+
+    $socket->say("MAIL FROM:<$o{from}>");
+    $socket->expect(2);
+
+    foreach my $rcpt (@rcpts) {
+        $socket->say("RCPT TO:<$rcpt>");
+        $socket->expect(2);
+    }
+
+    $socket->say("DATA");
+    $socket->expect(3);
+
+    while (<>) {
+        chomp;
+        last if /^\.$/;
+        $socket->say($_);
+    }
+    $socket->say('.');
+    $socket->expect(2);
+
+    return 0;
+}
+
+# extract Exim variables from the configuration
+sub exim_vars {
+
+# This does not work, if we're under restrictioned capabilities
+    state $exim = (grep { -x } map { "$_/exim" } split /:/, $ENV{PATH})[0]
+        or die "$0: Exim binary not found in $ENV{PATH}\n";
+    chomp(my @vars = `$exim -n -bP @_`);
+    return @vars;
+}
+
+__END__
+
+=head1 NAME
+
+ sendmail - sendmail drop in
+
+=head1 SYNOPSIS
+
+ sendmail [-f <sender>] <recipient>...
+ sendmail -t
+
+=cut