tftpd: Chrooted mode.

This commit is contained in:
Mats Erik Andersson
2013-01-02 19:28:35 +01:00
parent fb9dcf487b
commit 7d4833481e
4 changed files with 313 additions and 6 deletions

View File

@@ -1,3 +1,25 @@
2013-01-02 Mats Erik Andersson <gnu@gisladisker.se>
tftpd: Chrooted mode.
* src/tftpd.c: Include <grp.h>, <pwd.h>, <xalloc.h>.
(chrootdir, group, user): New variables.
(DEFAULT_GROUP, DEFAULT_USER): New macros.
(options): New options `-g/--group', `-s/--secure-dir',
and `-u/--user'. Put new options in separate group.
(parse_opt): Parameter `arg' is now in use.
<`g', `s', `u'>: New cases.
(main): Assign default values to `group', `user'.
If chrooted mode was specified, execute chroot(),
setgid(), and setuid().
* tests/tftp.sh (do_secure_setting): New variable.
Set to true only for the super-user, then activating
two further read tests.
(REDIRECT): Set also for empty VERBOSE.
* doc/inetutils.texi <tftpd invocation>: Fill with
sensible content for old and new capabilities.
2012-12-27 Mats Erik Andersson <gnu@gisladisker.se>
traceroute: Conform to standard application set-up.

View File

@@ -3587,22 +3587,157 @@ access is refused.
@chapter @command{tftpd}: TFTP server
@cindex tftpd
@command{tftpd} is intended to be invoked via @command{inetd}
at all times.
@noindent
Synopsis:
@example
tftpd [@var{OPTIONS}]@dots{}
tftpd [@var{options}] [@var{directory} @dots{}]
@end example
@table @option
@item -g @var{group}
@itemx --group=@var{group}
@opindex -g
@opindex --group
Specify group membership of the process owner.
This is used only along with the option @option{-s}.
The default choice is @samp{nogroup}.
@item -l
@itemx --logging
@opindex -l
@opindex --logging
Enable logging.
@item -n
@itemx --nonexistent
@opindex -n
@opindex --nonexistent
Supress negative acknowledgement of requests for nonexistent relative
filenames.
@item -s @var{dir}
@itemx --secure-dir=@var{dir}
@opindex -s
@opindex --secure-dir
Let the serving process change its root directory to @var{dir}
before attending to any requests.
This directory is not observable by any client, but improves
server isolation, since servable contents must be located
below this chrooted directory @var{dir}.
@item -u @var{user}
@itemx --user=@var{user}
@opindex -u
@opindex --user
Specify the process owner for serving requests.
Only relevant along with the option @option{-s}.
The default name is @samp{nobody}.
@end table
@section Directory prefixes
@anchor{tftpd validation}
In addition to options, an invocation of @command{tftpd} can
specify an optional list of directory prefixes.
These are approved of according to two principles:
@itemize @bullet
@item
Relative pathnames are ignored.
@item
At most twenty prefixes are approved, the rest is discarded.
@end itemize
@noindent
A request for a file is decided upon as a consequence
of evaluating these criteria:
@itemize @bullet
@item
Every file request containing the substring @samp{/../} is denied,
as is a file name beginning with @samp{../}.
@item
Write requests must specify absolute locations.
@item
A file request, if specified as an @emph{absolute} pathname,
must begin with one of the approved directory prefixes,
should at least one such prefix have been accepted.
@item
In the absence of a prefix collection, any absolute pathname is
accepted, should the corresponding file exist.
@item
A file request, if specified as a @emph{relative} name,
will only be searched for below the acceptable prefixes,
should at least one such prefix have been approved.
@item
A request for a relatively named file, is denied in the absence
of approved directory prefixes.
@item
The resulting file must be world readable, or world writable,
for a read request, or a write request, to succeed.
@end itemize
@section Use cases
@anchor{tftpd setup cases}
The standard use case is an entry in @file{/etc/inetd.conf} like
@example
tftp dgram tcp4 wait root nobody /usr/sbin/tftpd \
@verb{ } tftpd /tftpboot /altboot
@end example
@noindent
This would allow the TFTP client to use any of
@smallexample
get kernel
get /tftpboot/kernel
get kernel.alt
get /altboot/kernel.alt
get /etc/motd
@end smallexample
@noindent
given that @file{/tftpboot/kernel} and @file{/altboot/kernel.alt} exist.
Observe that also @file{/etc/motd} is accessible, inspite there being
no explicit mention of standard file locations.
A stronger mode of running a TFTP server is to use the `secure mode',
meaning that the serving process is running in a chrooted mode.
Then a suitable configuration could be
@example
tftp dgram tcp4 wait root nobody /usr/sbin/tftpd \
@verb{ } tftpd --secure-dir=/srv/tftp-root /tftpboot /altboot
@end example
@noindent
Supposing that the files @file{/srv/tftp-root/tftpboot/kernel}
and @file{/srv/tftp-root/altboot/kernel.alt} were available,
all the previously suggested client requests for a kernel would
still be granted, but now any request for @file{/etc/motd}
would be declined, and would get a reply `File not found' back.
The chrooted setting is denying access outside of
@file{/srv/tftp-root}, yet is not indicating this lock-in
to the client, and is thus improving server isolation.
Since neither of @option{-u} and @option{-g} were specified,
the configuration reproduced above will in fact have the
transmitting server process running with the default
owner set to @samp{nobody:nogroup}.
@node rshd invocation
@chapter @command{rshd}: Remote shell server
@cindex rshd

View File

@@ -79,10 +79,13 @@
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <grp.h>
#include <pwd.h>
#include "tftpsubs.h"
#include <unused-parameter.h>
#include <xalloc.h>
#include <argp.h>
#include <progname.h>
#include <libinetutils.h>
@@ -98,6 +101,17 @@ void usage (void);
static int peer;
static int rexmtval = TIMEOUT;
static int maxtimeout = 5 * TIMEOUT;
static char *chrootdir = NULL;
static char *group;
static char *user;
#ifndef DEFAULT_GROUP
# define DEFAULT_GROUP "nogroup"
#endif
#ifndef DEFAULT_USER
# define DEFAULT_USER "nobody"
#endif
/* Some systems define PKTSIZE in <arpa/tftp.h>. */
#ifndef PKTSIZE
@@ -133,16 +147,30 @@ static const char *verifyhost (struct sockaddr_storage *, socklen_t);
static struct argp_option options[] = {
#define GRP 0
{ "logging", 'l', NULL, 0,
"enable logging", 1},
"enable logging", GRP+1},
{ "nonexistent", 'n', NULL, 0,
"supress negative acknowledgement of requests for "
"nonexistent relative filenames", 1},
"nonexistent relative filenames", GRP+1},
#undef GRP
#define GRP 10
{ NULL, 0, NULL, 0, "", GRP},
{ "group", 'g', "GRP", 0,
"set group of process owner, used with '-s' and "
"defaults to 'nogroup'", GRP+1},
{ "secure-dir", 's', "DIR", 0,
"change root directory to DIR before searching and "
"serving content", GRP+1},
{ "user", 'u', "USR", 0,
"set name of process owner, used with '-s' and "
"defaults to 'nobody'", GRP+1},
#undef GRP
{ NULL, 0, NULL, 0, NULL, 0}
};
static error_t
parse_opt (int key, char *arg _GL_UNUSED_PARAMETER,
parse_opt (int key, char *arg,
struct argp_state *state _GL_UNUSED_PARAMETER)
{
switch (key)
@@ -151,10 +179,24 @@ parse_opt (int key, char *arg _GL_UNUSED_PARAMETER,
logging = 1;
break;
case 'g':
free (group);
group = xstrdup (arg);
break;
case 'n':
suppress_naks = 1;
break;
case 's':
chrootdir = xstrdup (arg);
break;
case 'u':
free (user);
user = xstrdup (arg);
break;
default:
return ARGP_ERR_UNKNOWN;
}
@@ -180,6 +222,9 @@ main (int argc, char *argv[])
int on, n;
struct sockaddr_storage sin;
group = xstrdup (DEFAULT_GROUP);
user = xstrdup (DEFAULT_USER);
set_program_name (argv[0]);
iu_argp_init ("tftpd", default_program_authors);
argp_parse (&argp, argc, argv, 0, &index, NULL);
@@ -202,12 +247,52 @@ main (int argc, char *argv[])
}
}
if (chrootdir && *chrootdir)
{
struct passwd *pwd = NULL;
struct group *grp = NULL;
/* Ignore user and group setting for non-root invokations. */
if (!getuid())
{
pwd = getpwnam (user);
if (!pwd)
{
syslog (LOG_ERR, "getpwnam('%s'): %m", user);
exit (EXIT_FAILURE);
}
grp = getgrnam (group);
if (!grp)
{
syslog (LOG_ERR, "getgrnam('%s'): %m", group);
exit (EXIT_FAILURE);
}
}
if (chroot (chrootdir) || chdir ("/"))
{
syslog (LOG_ERR, "chroot('%s'): %m", chrootdir);
exit (EXIT_FAILURE);
}
if (pwd && grp)
{
if (setgid (grp->gr_gid) || setuid (pwd->pw_uid))
{
syslog (LOG_ERR, "setgid/setuid: %m");
exit (EXIT_FAILURE);
}
}
}
on = 1;
if (ioctl (0, FIONBIO, &on) < 0)
{
syslog (LOG_ERR, "ioctl(FIONBIO): %m\n");
syslog (LOG_ERR, "ioctl(FIONBIO): %m");
exit (EXIT_FAILURE);
}
fromlen = sizeof (from);
n = recvfrom (0, buf, sizeof (buf), 0, (struct sockaddr *) &from, &fromlen);
if (n < 0)

View File

@@ -30,6 +30,20 @@
#
# OpenBSD uses /etc/services directly, not via /etc/nsswitch.conf.
#
# Currently implemented tests (10 or 12 in total):
#
# * Read three files in binary mode, from 127.0.0.1 and ::1,
# needing one, two, and multiple data packets, respectively.
#
# * Read one moderate size ascii file from 127.0.0.1 and ::1.
#
# * Reload configuration and read a small binary file twice.
#
# * (root only) Reload configuration for chrooted mode.
# Read one binary file with a relative name, and one ascii
# file with absolute location.
. ./tools.sh
$need_dd || exit_no_dd
@@ -95,6 +109,10 @@ USER=`func_id_user`
# Late supplimentary subtest.
do_conf_reload=true
do_secure_setting=true
# Disable chrooted mode for non-root invocation.
test `func_id_uid` -eq 0 || do_secure_setting=false
# Random base directory at testing time.
TMPDIR=`$MKTEMP -d $PWD/tmp.XXXXXXXXXX` ||
@@ -195,7 +213,7 @@ write_conf ||
# would like to suppress the verbose output. The variable
# REDIRECT is set to '2>/dev/null' in non-verbose mode.
#
test -n "${VERBOSE+yes}" || REDIRECT='2>/dev/null'
test -n "$VERBOSE" || REDIRECT='2>/dev/null'
eval "$INETD -d -p'$INETD_PID' '$INETD_CONF' $REDIRECT &"
@@ -385,6 +403,53 @@ else
$silence echo >&2 'Informational: Inhibiting config reload test.'
fi
if $do_secure_setting; then
# Allow an underprivileged process owner to read files.
chmod g=rx,o=rx $TMPDIR
cat > "$INETD_CONF" <<-EOF
$PORT dgram ${PROTO}4 wait $USER $TFTPD tftpd -l -s $TMPDIR /tftp-test
$PORT dgram ${PROTO}6 wait $USER $TFTPD tftpd -l -s $TMPDIR /tftp-test
EOF
# Let inetd reload configuration.
kill -HUP $inetd_pid
# Test two files: file-small and asciifile.txt
#
addr=`echo "$ADDRESSES" | $SED 's/ .*//'`
name=`echo "$FILELIST" | $SED 's/ .*//'`
rm -f "$name" "$ASCIIFILE"
EFFORTS=`expr $EFFORTS + 2`
echo "binary
get $name
ascii
get /tftp-test/$ASCIIFILE" | \
eval "$TFTP" ${VERBOSE:+-v} "$addr" $PORT $bucket
cmp "$TMPDIR/tftp-test/$name" "$name" 2>/dev/null
result=$?
if test $? -ne 0; then
$silence echo >&2 "Failed chrooted access to $name."
RESULT=$result
else
$silence echo >&2 "Success with chrooted access to $name."
SUCCESSES=`expr $SUCCESSES + 1`
fi
cmp "$TMPDIR/tftp-test/$ASCIIFILE" "$ASCIIFILE" 2>/dev/null
result=$?
if test $? -ne 0; then
$silence echo >&2 "Failed chrooted access to /tftp-test/$ASCIIFILE."
RESULT=$result
else
$silence echo >&2 "Success with chrooted /tftp-test/$ASCIIFILE."
SUCCESSES=`expr $SUCCESSES + 1`
fi
else
$silence echo >&2 'Informational: Inhibiting chroot test.'
fi
# Minimal clean up. Main work in posttesting().
$silence echo
test $RESULT -eq 0 && $silence false \