mirror of
https://git.savannah.gnu.org/git/inetutils.git
synced 2026-01-12 00:19:39 +08:00
tftpd: Chrooted mode.
This commit is contained in:
22
ChangeLog
22
ChangeLog
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
93
src/tftpd.c
93
src/tftpd.c
@@ -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)
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user