From 4626cbaf78767fc8e9c86dd04785386c59ae0839 Mon Sep 17 00:00:00 2001 From: Damien Miller Date: Fri, 8 Jan 2016 14:24:56 +1100 Subject: [PATCH] Support Illumos/Solaris fine-grained privileges Includes a pre-auth privsep sandbox and several pledge() emulations. bz#2511, patch by Alex Wilson. ok dtucker@ --- Makefile.in | 6 +- configure.ac | 38 +++++++++++- mux.c | 2 + openbsd-compat/port-solaris.c | 114 ++++++++++++++++++++++++++++++++++ openbsd-compat/port-solaris.h | 3 + platform-pledge.c | 71 +++++++++++++++++++++ platform.h | 5 ++ sandbox-solaris.c | 107 +++++++++++++++++++++++++++++++ sftp-server.c | 3 + ssh-agent.c | 1 + uidswap.c | 18 ++++-- 11 files changed, 358 insertions(+), 10 deletions(-) create mode 100644 platform-pledge.c create mode 100644 sandbox-solaris.c diff --git a/Makefile.in b/Makefile.in index 15cf69e1ff7..9e326411c10 100644 --- a/Makefile.in +++ b/Makefile.in @@ -91,7 +91,8 @@ LIBSSH_OBJS=${LIBOPENSSH_OBJS} \ sc25519.o ge25519.o fe25519.o ed25519.o verify.o hash.o blocks.o \ kex.o kexdh.o kexgex.o kexecdh.o kexc25519.o \ kexdhc.o kexgexc.o kexecdhc.o kexc25519c.o \ - kexdhs.o kexgexs.o kexecdhs.o kexc25519s.o + kexdhs.o kexgexs.o kexecdhs.o kexc25519s.o \ + platform-pledge.o SSHOBJS= ssh.o readconf.o clientloop.o sshtty.o \ sshconnect.o sshconnect1.o sshconnect2.o mux.o \ @@ -110,7 +111,8 @@ SSHDOBJS=sshd.o auth-rhosts.o auth-passwd.o auth-rsa.o auth-rh-rsa.o \ sftp-server.o sftp-common.o \ roaming_common.o roaming_serv.o \ sandbox-null.o sandbox-rlimit.o sandbox-systrace.o sandbox-darwin.o \ - sandbox-seccomp-filter.o sandbox-capsicum.o sandbox-pledge.o + sandbox-seccomp-filter.o sandbox-capsicum.o sandbox-pledge.o \ + sandbox-solaris.o MANPAGES = moduli.5.out scp.1.out ssh-add.1.out ssh-agent.1.out ssh-keygen.1.out ssh-keyscan.1.out ssh.1.out sshd.8.out sftp-server.8.out sftp.1.out ssh-keysign.8.out ssh-pkcs11-helper.8.out sshd_config.5.out ssh_config.5.out MANPAGES_IN = moduli.5 scp.1 ssh-add.1 ssh-agent.1 ssh-keygen.1 ssh-keyscan.1 ssh.1 sshd.8 sftp-server.8 sftp.1 ssh-keysign.8 ssh-pkcs11-helper.8 sshd_config.5 ssh_config.5 diff --git a/configure.ac b/configure.ac index b6854320cdb..0b399ce2cc0 100644 --- a/configure.ac +++ b/configure.ac @@ -469,6 +469,11 @@ AC_CHECK_HEADERS([sys/un.h], [], [], [ SIA_MSG="no" SPC_MSG="no" SP_MSG="no" +SPP_MSG="no" + +# Support for Solaris/Illumos privileges (this test is used by both +# the --with-solaris-privs option and --with-sandbox=solaris). +SOLARIS_PRIVS="no" # Check for some target-specific stuff case "$host" in @@ -575,6 +580,8 @@ case "$host" in LIBS="$LIBS /usr/lib/textreadmode.o" AC_DEFINE([HAVE_CYGWIN], [1], [Define if you are on Cygwin]) AC_DEFINE([USE_PIPES], [1], [Use PIPES instead of a socketpair()]) + AC_DEFINE([NO_UID_RESTORATION_TEST], [1], + [Define to disable UID restoration test]) AC_DEFINE([DISABLE_SHADOW], [1], [Define if you want to disable shadow passwords]) AC_DEFINE([NO_X11_UNIX_SOCKETS], [1], @@ -889,13 +896,18 @@ mips-sony-bsd|mips-sony-newsos4) else AC_MSG_RESULT([no]) fi + AC_CHECK_FUNC([setppriv], + [ AC_CHECK_HEADERS([priv.h], [ + SOLARIS_PRIVS="yes" + ]) + ]) AC_ARG_WITH([solaris-contracts], [ --with-solaris-contracts Enable Solaris process contracts (experimental)], [ AC_CHECK_LIB([contract], [ct_tmpl_activate], [ AC_DEFINE([USE_SOLARIS_PROCESS_CONTRACTS], [1], [Define if you have Solaris process contracts]) - SSHDLIBS="$SSHDLIBS -lcontract" + LIBS="$LIBS -lcontract" SPC_MSG="yes" ], ) ], ) @@ -905,10 +917,27 @@ mips-sony-bsd|mips-sony-newsos4) AC_CHECK_LIB([project], [setproject], [ AC_DEFINE([USE_SOLARIS_PROJECTS], [1], [Define if you have Solaris projects]) - SSHDLIBS="$SSHDLIBS -lproject" + LIBS="$LIBS -lproject" SP_MSG="yes" ], ) ], ) + AC_ARG_WITH([solaris-privs], + [ --with-solaris-privs Enable Solaris/Illumos privileges (experimental)], + [ + AC_MSG_CHECKING([for Solaris/Illumos privilege support]) + if test "x$SOLARIS_PRIVS" = "xyes" ; then + AC_MSG_RESULT([found]) + AC_DEFINE([NO_UID_RESTORATION_TEST], [1], + [Define to disable UID restoration test]) + AC_DEFINE([USE_SOLARIS_PRIVS], [1], + [Define if you have Solaris privileges]) + SPP_MSG="yes" + else + AC_MSG_RESULT([not found]) + AC_MSG_ERROR([*** must have support for Solaris privileges to use --with-solaris-privs]) + fi + ], + ) TEST_SHELL=$SHELL # let configure find us a capable shell ;; *-*-sunos4*) @@ -3156,6 +3185,10 @@ elif test "x$sandbox_arg" = "xrlimit" || \ AC_MSG_ERROR([rlimit sandbox requires select to work with rlimit]) SANDBOX_STYLE="rlimit" AC_DEFINE([SANDBOX_RLIMIT], [1], [Sandbox using setrlimit(2)]) +elif test "x$sandbox_arg" = "xsolaris" || \ + ( test -z "$sandbox_arg" && test "x$SOLARIS_PRIVS" = "xyes" ) ; then + SANDBOX_STYLE="solaris" + AC_DEFINE([SANDBOX_SOLARIS], [1], [Sandbox using Solaris/Illumos privileges]) elif test -z "$sandbox_arg" || test "x$sandbox_arg" = "xno" || \ test "x$sandbox_arg" = "xnone" || test "x$sandbox_arg" = "xnull" ; then SANDBOX_STYLE="none" @@ -4945,6 +4978,7 @@ echo " MD5 password support: $MD5_MSG" echo " libedit support: $LIBEDIT_MSG" echo " Solaris process contract support: $SPC_MSG" echo " Solaris project support: $SP_MSG" +echo " Solaris privilege support: $SPP_MSG" echo " IP address in \$DISPLAY hack: $DISPLAY_HACK_MSG" echo " Translate v4 in v6 hack: $IPV4_IN6_HACK_MSG" echo " BSD Auth support: $BSD_AUTH_MSG" diff --git a/mux.c b/mux.c index 09704497f33..f9c3af651c7 100644 --- a/mux.c +++ b/mux.c @@ -1891,6 +1891,7 @@ mux_client_request_session(int fd) if (pledge("stdio proc tty", NULL) == -1) fatal("%s pledge(): %s", __func__, strerror(errno)); + platform_pledge_mux(); signal(SIGHUP, control_client_sighandler); signal(SIGINT, control_client_sighandler); @@ -2001,6 +2002,7 @@ mux_client_request_stdio_fwd(int fd) if (pledge("stdio proc tty", NULL) == -1) fatal("%s pledge(): %s", __func__, strerror(errno)); + platform_pledge_mux(); debug3("%s: stdio forward request sent", __func__); diff --git a/openbsd-compat/port-solaris.c b/openbsd-compat/port-solaris.c index 25382f1c907..962cd1685e6 100644 --- a/openbsd-compat/port-solaris.c +++ b/openbsd-compat/port-solaris.c @@ -227,3 +227,117 @@ solaris_set_default_project(struct passwd *pw) } } #endif /* USE_SOLARIS_PROJECTS */ + +#ifdef USE_SOLARIS_PRIVS +# ifdef HAVE_PRIV_H +# include +# endif + +void +solaris_drop_privs_pinfo_net_fork_exec(void) +{ + priv_set_t *pset = NULL, *npset = NULL; + + /* + * Note: this variant avoids dropping DAC filesystem rights, in case + * the process calling it is running as root and should have the + * ability to read/write/chown any file on the system. + * + * We start with the basic set, then *add* the DAC rights to it while + * taking away other parts of BASIC we don't need. Then we intersect + * this with our existing PERMITTED set. In this way we keep any + * DAC rights we had before, while otherwise reducing ourselves to + * the minimum set of privileges we need to proceed. + * + * This also means we drop any other parts of "root" that we don't + * need (e.g. the ability to kill any process, create new device nodes + * etc etc). + */ + + if ((pset = priv_allocset()) == NULL || + (npset = priv_allocset()) == NULL) + fatal("priv_allocset: %s", strerror(errno)); + + priv_basicset(npset); + + if (priv_addset(npset, PRIV_FILE_CHOWN) != 0 || + priv_addset(npset, PRIV_FILE_DAC_READ) != 0 || + priv_addset(npset, PRIV_FILE_DAC_SEARCH) != 0 || + priv_addset(npset, PRIV_FILE_DAC_WRITE) != 0 || + priv_addset(npset, PRIV_FILE_OWNER) != 0) + fatal("priv_addset: %s", strerror(errno)); + + if (priv_delset(npset, PRIV_FILE_LINK_ANY) != 0 || + priv_delset(npset, PRIV_NET_ACCESS) != 0 || + priv_delset(npset, PRIV_PROC_EXEC) != 0 || + priv_delset(npset, PRIV_PROC_FORK) != 0 || + priv_delset(npset, PRIV_PROC_INFO) != 0 || + priv_delset(npset, PRIV_PROC_SESSION) != 0) + fatal("priv_delset: %s", strerror(errno)); + + if (getppriv(PRIV_PERMITTED, pset) != 0) + fatal("getppriv: %s", strerror(errno)); + + priv_intersect(pset, npset); + + if (setppriv(PRIV_SET, PRIV_PERMITTED, npset) != 0 || + setppriv(PRIV_SET, PRIV_LIMIT, npset) != 0 || + setppriv(PRIV_SET, PRIV_INHERITABLE, npset) != 0) + fatal("setppriv: %s", strerror(errno)); + + priv_freeset(pset); + priv_freeset(npset); +} + +void +solaris_drop_privs_root_pinfo_net(void) +{ + priv_set_t *pset = NULL; + + if ((pset = priv_allocset()) == NULL) + fatal("priv_allocset: %s", strerror(errno)); + + /* Start with "basic" and drop everything we don't need. */ + priv_basicset(pset); + + if (priv_delset(pset, PRIV_FILE_LINK_ANY) != 0 || + priv_delset(pset, PRIV_NET_ACCESS) != 0 || + priv_delset(pset, PRIV_PROC_INFO) != 0 || + priv_delset(pset, PRIV_PROC_SESSION) != 0) + fatal("priv_delset: %s", strerror(errno)); + + if (setppriv(PRIV_SET, PRIV_PERMITTED, pset) != 0 || + setppriv(PRIV_SET, PRIV_LIMIT, pset) != 0 || + setppriv(PRIV_SET, PRIV_INHERITABLE, pset) != 0) + fatal("setppriv: %s", strerror(errno)); + + priv_freeset(pset); +} + +void +solaris_drop_privs_root_pinfo_net_exec(void) +{ + priv_set_t *pset = NULL; + + if ((pset = priv_allocset()) == NULL) + fatal("priv_allocset: %s", strerror(errno)); + + /* Start with "basic" and drop everything we don't need. */ + priv_basicset(pset); + + if (priv_delset(pset, PRIV_FILE_LINK_ANY) != 0 || + priv_delset(pset, PRIV_NET_ACCESS) != 0 || + priv_delset(pset, PRIV_PROC_EXEC) != 0 || + priv_delset(pset, PRIV_PROC_INFO) != 0 || + priv_delset(pset, PRIV_PROC_SESSION) != 0) + fatal("priv_delset: %s", strerror(errno)); + + if (setppriv(PRIV_SET, PRIV_PERMITTED, pset) != 0 || + setppriv(PRIV_SET, PRIV_LIMIT, pset) != 0 || + setppriv(PRIV_SET, PRIV_INHERITABLE, pset) != 0) + fatal("setppriv: %s", strerror(errno)); + + priv_freeset(pset); +} + +#endif diff --git a/openbsd-compat/port-solaris.h b/openbsd-compat/port-solaris.h index cd442e78b10..b077e186ae9 100644 --- a/openbsd-compat/port-solaris.h +++ b/openbsd-compat/port-solaris.h @@ -26,5 +26,8 @@ void solaris_contract_pre_fork(void); void solaris_contract_post_fork_child(void); void solaris_contract_post_fork_parent(pid_t pid); void solaris_set_default_project(struct passwd *); +void solaris_drop_privs_pinfo_net_fork_exec(void); +void solaris_drop_privs_root_pinfo_net(void); +void solaris_drop_privs_root_pinfo_net_exec(void); #endif diff --git a/platform-pledge.c b/platform-pledge.c new file mode 100644 index 00000000000..4a6ec15e1b2 --- /dev/null +++ b/platform-pledge.c @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015 Joyent, Inc + * Author: Alex Wilson + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include + +#include +#include + +#include "platform.h" + +#include "openbsd-compat/openbsd-compat.h" + +/* + * Drop any fine-grained privileges that are not needed for post-startup + * operation of ssh-agent + * + * Should be as close as possible to pledge("stdio cpath unix id proc exec", ...) + */ +void +platform_pledge_agent(void) +{ +#ifdef USE_SOLARIS_PRIVS + /* + * Note: Solaris priv dropping is closer to tame() than pledge(), but + * we will use what we have. + */ + solaris_drop_privs_root_pinfo_net(); +#endif +} + +/* + * Drop any fine-grained privileges that are not needed for post-startup + * operation of sftp-server + */ +void +platform_pledge_sftp_server(void) +{ +#ifdef USE_SOLARIS_PRIVS + solaris_drop_privs_pinfo_net_fork_exec(); +#endif +} + +/* + * Drop any fine-grained privileges that are not needed for the post-startup + * operation of the SSH client mux + * + * Should be as close as possible to pledge("stdio proc tty", ...) + */ +void +platform_pledge_mux(void) +{ +#ifdef USE_SOLARIS_PRIVS + solaris_drop_privs_root_pinfo_net_exec(); +#endif +} diff --git a/platform.h b/platform.h index 1c7a45d8fc2..e687c99b6e5 100644 --- a/platform.h +++ b/platform.h @@ -31,3 +31,8 @@ void platform_setusercontext_post_groups(struct passwd *); char *platform_get_krb5_client(const char *); char *platform_krb5_get_principal_name(const char *); int platform_sys_dir_uid(uid_t); + +/* in platform-pledge.c */ +void platform_pledge_agent(void); +void platform_pledge_sftp_server(void); +void platform_pledge_mux(void); diff --git a/sandbox-solaris.c b/sandbox-solaris.c new file mode 100644 index 00000000000..98714e17051 --- /dev/null +++ b/sandbox-solaris.c @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2015 Joyent, Inc + * Author: Alex Wilson + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#ifdef SANDBOX_SOLARIS +#ifndef USE_SOLARIS_PRIVS +# error "--with-solaris-privs must be used with the Solaris sandbox" +#endif + +#include + +#include +#include +#include +#include +#include +#include +#ifdef HAVE_PRIV_H +# include +#endif + +#include "log.h" +#include "ssh-sandbox.h" +#include "xmalloc.h" + +struct ssh_sandbox { + priv_set_t *pset; +}; + +struct ssh_sandbox * +ssh_sandbox_init(struct monitor *monitor) +{ + struct ssh_sandbox *box = NULL; + + box = xcalloc(1, sizeof(*box)); + box->pset = priv_allocset(); + + if (box->pset == NULL) { + free(box); + return NULL; + } + + /* Start with "basic" and drop everything we don't need. */ + priv_basicset(box->pset); + + /* Drop everything except the ability to use already-opened files */ + if (priv_delset(box->pset, PRIV_FILE_LINK_ANY) != 0 || + priv_delset(box->pset, PRIV_NET_ACCESS) != 0 || + priv_delset(box->pset, PRIV_PROC_EXEC) != 0 || + priv_delset(box->pset, PRIV_PROC_FORK) != 0 || + priv_delset(box->pset, PRIV_PROC_INFO) != 0 || + priv_delset(box->pset, PRIV_PROC_SESSION) != 0) { + free(box); + return NULL; + } + + /* These may not be available on older Solaris-es */ +# if defined(PRIV_FILE_READ) && defined(PRIV_FILE_WRITE) + if (priv_delset(box->pset, PRIV_FILE_READ) != 0 || + priv_delset(box->pset, PRIV_FILE_WRITE) != 0) { + free(box); + return NULL; + } +# endif + + return box; +} + +void +ssh_sandbox_child(struct ssh_sandbox *box) +{ + if (setppriv(PRIV_SET, PRIV_PERMITTED, box->pset) != 0 || + setppriv(PRIV_SET, PRIV_LIMIT, box->pset) != 0 || + setppriv(PRIV_SET, PRIV_INHERITABLE, box->pset) != 0) + fatal("setppriv: %s", strerror(errno)); +} + +void +ssh_sandbox_parent_finish(struct ssh_sandbox *box) +{ + priv_freeset(box->pset); + box->pset = NULL; + free(box); +} + +void +ssh_sandbox_parent_preauth(struct ssh_sandbox *box, pid_t child_pid) +{ + /* Nothing to do here */ +} + +#endif /* SANDBOX_SOLARIS */ diff --git a/sftp-server.c b/sftp-server.c index 62e76a50527..79ef45b10b7 100644 --- a/sftp-server.c +++ b/sftp-server.c @@ -1598,6 +1598,9 @@ sftp_server_main(int argc, char **argv, struct passwd *user_pw) fatal("unable to make the process undumpable"); #endif /* defined(HAVE_PRCTL) && defined(PR_SET_DUMPABLE) */ + /* Drop any fine-grained privileges we don't need */ + platform_pledge_sftp_server(); + if ((cp = getenv("SSH_CONNECTION")) != NULL) { client_addr = xstrdup(cp); if ((cp = strchr(client_addr, ' ')) == NULL) { diff --git a/ssh-agent.c b/ssh-agent.c index 2048a11c41d..6c50e0f03b7 100644 --- a/ssh-agent.c +++ b/ssh-agent.c @@ -1417,6 +1417,7 @@ main(int ac, char **av) if (pledge("stdio cpath unix id proc exec", NULL) == -1) fatal("%s: pledge: %s", __progname, strerror(errno)); + platform_pledge_agent(); while (1) { prepare_select(&readsetp, &writesetp, &max_fd, &nalloc, &tvp); diff --git a/uidswap.c b/uidswap.c index 0702e1d9e1b..8bf6b244e5c 100644 --- a/uidswap.c +++ b/uidswap.c @@ -134,7 +134,7 @@ temporarily_use_uid(struct passwd *pw) void permanently_drop_suid(uid_t uid) { -#ifndef HAVE_CYGWIN +#ifndef NO_UID_RESTORATION_TEST uid_t old_uid = getuid(); #endif @@ -142,8 +142,14 @@ permanently_drop_suid(uid_t uid) if (setresuid(uid, uid, uid) < 0) fatal("setresuid %u: %.100s", (u_int)uid, strerror(errno)); -#ifndef HAVE_CYGWIN - /* Try restoration of UID if changed (test clearing of saved uid) */ +#ifndef NO_UID_RESTORATION_TEST + /* + * Try restoration of UID if changed (test clearing of saved uid). + * + * Note that we don't do this on Cygwin, or on Solaris-based platforms + * where fine-grained privileges are available (the user might be + * deliberately allowed the right to setuid back to root). + */ if (old_uid != uid && (setuid(old_uid) != -1 || seteuid(old_uid) != -1)) fatal("%s: was able to restore old [e]uid", __func__); @@ -199,7 +205,7 @@ restore_uid(void) void permanently_set_uid(struct passwd *pw) { -#ifndef HAVE_CYGWIN +#ifndef NO_UID_RESTORATION_TEST uid_t old_uid = getuid(); gid_t old_gid = getgid(); #endif @@ -227,7 +233,7 @@ permanently_set_uid(struct passwd *pw) if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) < 0) fatal("setresuid %u: %.100s", (u_int)pw->pw_uid, strerror(errno)); -#ifndef HAVE_CYGWIN +#ifndef NO_UID_RESTORATION_TEST /* Try restoration of GID if changed (test clearing of saved gid) */ if (old_gid != pw->pw_gid && pw->pw_uid != 0 && (setgid(old_gid) != -1 || setegid(old_gid) != -1)) @@ -241,7 +247,7 @@ permanently_set_uid(struct passwd *pw) (u_int)pw->pw_gid); } -#ifndef HAVE_CYGWIN +#ifndef NO_UID_RESTORATION_TEST /* Try restoration of UID if changed (test clearing of saved uid) */ if (old_uid != pw->pw_uid && (setuid(old_uid) != -1 || seteuid(old_uid) != -1))