-#!/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).
--- /dev/null
+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;
+
--- /dev/null
+#!/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:
--- /dev/null
+#!/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:
--- /dev/null
+#!/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"
+
-#!/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:
--- /dev/null
+#!/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