diff --git a/.gitignore b/.gitignore index ff72dc839..ee683b45f 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ examples/scpclient/wolfscp apps/wolfssh/wolfssh apps/wolfsshd/wolfsshd apps/wolfsshd/test/test_configuration +apps/wolfsshd/test/sshd_privdrop_preload.so apps/wolfsshd/test/log.txt apps/wolfsshd/test/sshd_config_* apps/wolfsshd/test/authorized_keys_test diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index 50fd68fea..298bf18a8 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -397,9 +397,10 @@ else run_test "sshd_empty_password_test.sh" run_strictmodes_negative_test run_test "sshd_login_grace_test.sh" + run_test "sshd_privdrop_fail_test.sh" else printf "Skipping tests that need to setup local SSHD\n" - SKIPPED=$((SKIPPED+5)) + SKIPPED=$((SKIPPED+6)) fi # these tests run with X509 sshd-config loaded diff --git a/apps/wolfsshd/test/sshd_privdrop_fail_test.sh b/apps/wolfsshd/test/sshd_privdrop_fail_test.sh new file mode 100755 index 000000000..73a22c0d4 --- /dev/null +++ b/apps/wolfsshd/test/sshd_privdrop_fail_test.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +# Fail-closed privilege-drop regression: when the per-connection drop fails, the +# subsystem handlers must terminate the child, not continue as root. The drop is +# forced to fail against the stock wolfsshd with an LD_PRELOAD interposer +# (sshd_privdrop_preload.c), so no fault code lives in the daemon or library. +# Drives all three dropping subsystems: exec/shell, sftp, scp. + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "./sshd_privdrop_fail_test.sh 127.0.0.1 22222" + exit 1 +fi + +PWD=`pwd` +USER=`whoami` +TEST_HOST="$1" + +# Own daemon on a dedicated port for isolation from the runner's shared daemon. +TEST_PORT="22822" + +SSHD_BIN="../wolfsshd" +if [ ! -x "$SSHD_BIN" ]; then + echo "SKIP: $SSHD_BIN not built" + exit 77 +fi + +# macOS strips DYLD_INSERT_LIBRARIES from the sudo-launched daemon, so Linux only. +if [ "`uname -s`" != "Linux" ]; then + echo "SKIP: privilege-drop fault injection needs Linux LD_PRELOAD" + exit 77 +fi + +# Build the interposer next to this script; skip if no compiler. Relative "./" +# path so the LD_PRELOAD value has no space (see SSHD_ENV in start_sshd.sh). +PRELOAD_SRC="sshd_privdrop_preload.c" +PRELOAD_LIB="./sshd_privdrop_preload.so" +CC_BIN="${CC:-cc}" +if ! "$CC_BIN" -shared -fPIC -o "$PRELOAD_LIB" "$PRELOAD_SRC" -ldl 2>/dev/null; then + echo "SKIP: could not build $PRELOAD_LIB with $CC_BIN" + exit 77 +fi + +if [ -f ./log.txt ]; then + sudo rm -rf log.txt +fi +touch log.txt + +TEST_CLIENT="../../../examples/client/client" +SFTP_CLIENT="../../../examples/sftpclient/wolfsftp" +SCP_CLIENT="../../../examples/scpclient/wolfscp" +PRIVATE_KEY="../../../keys/hansel-key-ecc.der" +PUBLIC_KEY="../../../keys/hansel-key-ecc.pub" + +# Small payload for the sftp/scp transfers. The connection dies at the failed +# drop long before any data moves, so the contents do not matter. +PAYLOAD="privdrop_payload.txt" +echo "privdrop" > "$PAYLOAD" + +source ./start_sshd.sh + +cat < sshd_config_test_privdrop +Port $TEST_PORT +Protocol 2 +LoginGraceTime 600 +PermitRootLogin yes +PasswordAuthentication yes +PermitEmptyPasswords no +UsePrivilegeSeparation no +UseDNS no +HostKey $PWD/../../../keys/server-key.pem +AuthorizedKeysFile $PWD/authorized_keys_test +EOF + +# Preload and arm the interposer via SSHD_ENV (start_sshd.sh passes it through +# "sudo env"). "UsePrivilegeSeparation no" is the worst case: old fallback = noop. +SSHD_ENV="LD_PRELOAD=$PRELOAD_LIB WOLFSSHD_FAULT_PRIVDROP=1" +export SSHD_BIN SSHD_ENV + +# Teardown on every exit path; log.txt is kept for debugging like the other tests. +cleanup() { + stop_wolfsshd + rm -f sshd_config_test_privdrop "$PAYLOAD" "$PRELOAD_LIB" + return 0 +} +trap cleanup EXIT + +start_wolfsshd "sshd_config_test_privdrop" + +if [ -z "$PID" ]; then + echo "FAIL: privilege-drop daemon did not start" + exit 1 +fi + +DEADLINE=30 + +# Drives one client; the connection dies, so its exit status is not checked. +# Counts are per-call deltas since all three subsystems share the one log. +check_subsystem() { + LABEL="$1" + shift + + BEFORE_DROP=`grep -c "Error setting user ID" log.txt` + BEFORE_CLOSE=`grep -c "Attempting to close down connection" log.txt` + BEFORE_SPAWN=`grep -c "Spawned new process" log.txt` + + "$@" > /dev/null 2>&1 & + CLIENT_PID=$! + + # Wait for the connection child to fork and hit the failed drop, then take + # its real pid from the daemon's own "Spawned new process" log line. + WAITED=0 + CHILD="" + while [ "$WAITED" -lt "$DEADLINE" ]; do + AFTER_SPAWN=`grep -c "Spawned new process" log.txt` + AFTER_DROP=`grep -c "Error setting user ID" log.txt` + if [ "$AFTER_SPAWN" -gt "$BEFORE_SPAWN" ] && \ + [ "$AFTER_DROP" -gt "$BEFORE_DROP" ]; then + CHILD=`grep "Spawned new process" log.txt | tail -1 | \ + sed -n 's/.*process \([0-9][0-9]*\).*/\1/p'` + break + fi + sleep 1 + WAITED=`expr $WAITED + 1` + done + + kill $CLIENT_PID > /dev/null 2>&1 + wait $CLIENT_PID > /dev/null 2>&1 + + if [ -z "$CHILD" ]; then + echo "FAIL: $LABEL never reached the privilege drop" + exit 1 + fi + + # Positive fail-closed signal: that child must terminate. Wait for it to + # exit, on its own DEADLINE budget separate from the detection loop above. + EXITED=0 + while [ "$EXITED" -lt "$DEADLINE" ] && ps -p "$CHILD" > /dev/null 2>&1; do + sleep 1 + EXITED=`expr $EXITED + 1` + done + if ps -p "$CHILD" > /dev/null 2>&1; then + echo "FAIL: $LABEL connection process still running after ${DEADLINE}s" + exit 1 + fi + + # The buggy path logs this before exiting; the fix exit(1)s before the gate. + # The child is gone, so the line is either present now or never. + AFTER_CLOSE=`grep -c "Attempting to close down connection" log.txt` + if [ "$AFTER_CLOSE" -gt "$BEFORE_CLOSE" ]; then + echo "FAIL: $LABEL handler continued after a failed privilege drop" + exit 1 + fi + + printf " %s: connection process terminated on privilege-drop failure\n" \ + "$LABEL" +} + +# SHELL_Subsystem, via an exec session. +check_subsystem "exec" \ + "$TEST_CLIENT" -c 'echo privdrop' -u "$USER" -i "$PRIVATE_KEY" \ + -j "$PUBLIC_KEY" -h "$TEST_HOST" -p "$TEST_PORT" + +# SFTP_Subsystem. -g is a one-shot put, so the client cannot sit at a prompt. +check_subsystem "sftp" \ + "$SFTP_CLIENT" -u "$USER" -i "$PRIVATE_KEY" -j "$PUBLIC_KEY" \ + -g -l "$PAYLOAD" -r "/tmp/privdrop_remote_$$.txt" \ + -h "$TEST_HOST" -p "$TEST_PORT" + +# SCP_Subsystem. +check_subsystem "scp" \ + "$SCP_CLIENT" -u "$USER" -i "$PRIVATE_KEY" -j "$PUBLIC_KEY" \ + -S"$PWD/$PAYLOAD:." -H "$TEST_HOST" -p "$TEST_PORT" + +echo "PASS: all subsystems terminate on privilege-drop failure" +exit 0 diff --git a/apps/wolfsshd/test/sshd_privdrop_preload.c b/apps/wolfsshd/test/sshd_privdrop_preload.c new file mode 100644 index 000000000..c98f019b0 --- /dev/null +++ b/apps/wolfsshd/test/sshd_privdrop_preload.c @@ -0,0 +1,68 @@ +/* sshd_privdrop_preload.c + * + * Copyright (C) 2014-2024 wolfSSL Inc. + * + * This file is part of wolfSSH. + * + * wolfSSH is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSH is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfSSH. If not, see . + */ + +/* LD_PRELOAD interposer for sshd_privdrop_fail_test.sh only. */ +/* When WOLFSSHD_FAULT_PRIVDROP is set, setregid()/setreuid() return EPERM so the + * per-connection privilege drop in the stock wolfsshd fails. */ +/* No other daemon call uses these; the real call is forwarded when unarmed. */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +static int wsshd_fault_armed(void) +{ + return getenv("WOLFSSHD_FAULT_PRIVDROP") != NULL; +} + +int setregid(gid_t rgid, gid_t egid) +{ + int (*real)(gid_t, gid_t); + + if (wsshd_fault_armed()) { + errno = EPERM; + return -1; + } + real = (int (*)(gid_t, gid_t))dlsym(RTLD_NEXT, "setregid"); + if (real == NULL) { + errno = ENOSYS; + return -1; + } + return real(rgid, egid); +} + +int setreuid(uid_t ruid, uid_t euid) +{ + int (*real)(uid_t, uid_t); + + if (wsshd_fault_armed()) { + errno = EPERM; + return -1; + } + real = (int (*)(uid_t, uid_t))dlsym(RTLD_NEXT, "setreuid"); + if (real == NULL) { + errno = ENOSYS; + return -1; + } + return real(ruid, euid); +} diff --git a/apps/wolfsshd/test/start_sshd.sh b/apps/wolfsshd/test/start_sshd.sh index 2dfdc4fb1..ad0ea204f 100755 --- a/apps/wolfsshd/test/start_sshd.sh +++ b/apps/wolfsshd/test/start_sshd.sh @@ -81,8 +81,10 @@ EOF fi fi - # find a port - sudo ../wolfsshd -d -E ./log.txt -f "$CONFIG" + # SSHD_BIN picks the binary; SSHD_ENV passes env (e.g. LD_PRELOAD) that plain + # sudo would strip. SSHD_ENV is unquoted to split NAME=VALUE, so no spaces. + SSHD_BIN="${SSHD_BIN:-../wolfsshd}" + sudo env $SSHD_ENV "$SSHD_BIN" -d -E ./log.txt -f "$CONFIG" # set the PID of started sshd NEW_PID=`ps -e | grep wolfsshd | grep -oE "[0-9]+"` diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 0776839d9..9f1ba7f03 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -601,12 +601,10 @@ static int SCP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, pPasswd->pw_gid) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - /* stop everything if not able to reduce permissions level */ - exit(1); - } - - return WS_FATAL_ERROR; + /* could not drop to the authenticated user; terminate the + * per-connection process rather than continue at a higher + * privilege level */ + exit(1); } #else /* impersonate the logged on user for file permissions */ @@ -720,12 +718,10 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, pPasswd->pw_gid) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - /* stop everything if not able to reduce permissions level */ - exit(1); - } - - return WS_FATAL_ERROR; + /* could not drop to the authenticated user; terminate the + * per-connection process rather than continue at a higher + * privilege level */ + exit(1); } #else char r[MAX_PATH]; @@ -1410,7 +1406,8 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, return WS_FATAL_ERROR; } else if (childPid == 0) { - /* Child process */ + /* Child process. Any setup failure below exit(1)s rather than returning + * into the connection handler while still at a raised privilege level. */ const char *args[] = {"-sh", NULL, NULL, NULL}; char cmd[MAX_COMMAND_SZ]; char shell[MAX_COMMAND_SZ]; @@ -1430,29 +1427,17 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (dup2(stdinPipe[0], STDIN_FILENO) == -1) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error redirecting stdin pipe"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - exit(1); - } - - return WS_FATAL_ERROR; + exit(1); } if (dup2(stdoutPipe[1], STDOUT_FILENO) == -1) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error redirecting stdout pipe"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - exit(1); - } - - return WS_FATAL_ERROR; + exit(1); } if (dup2(stderrPipe[1], STDERR_FILENO) == -1) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error redirecting stderr pipe"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - exit(1); - } - - return WS_FATAL_ERROR; + exit(1); } } @@ -1460,47 +1445,27 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (wolfSSHD_AuthSetGroups(conn->auth, wolfSSH_GetUsername(ssh), pPasswd->pw_gid) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting groups"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - /* stop everything if not able to reduce permissions level */ - exit(1); - } - - return WS_FATAL_ERROR; + exit(1); } rc = SetupChroot(usrConf); if (rc < 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting chroot"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - /* stop everything if not able to reduce permissions level */ - exit(1); - } - - return WS_FATAL_ERROR; + exit(1); } else if (rc == 1) { rc = chdir("/"); if (rc != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to / after chroot"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - /* stop everything if not able to reduce permissions level */ - exit(1); - } - - return WS_FATAL_ERROR; + exit(1); } } if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, pPasswd->pw_gid) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - /* stop everything if not able to reduce permissions level */ - exit(1); - } - - return WS_FATAL_ERROR; + exit(1); } setenv("HOME", pPasswd->pw_dir, 1); @@ -1565,12 +1530,11 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, pPasswd->pw_gid) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); - if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { - /* stop everything if not able to reduce permissions level */ - exit(1); - } - - return WS_FATAL_ERROR; + /* could not drop to the authenticated user; kill the already + * forked shell child and terminate the per-connection process + * rather than continue at a higher privilege level */ + kill(childPid, SIGKILL); + exit(1); } sshFd = wolfSSH_get_fd(ssh); @@ -2176,7 +2140,8 @@ static void* HandleConnection(void* arg) #ifdef WOLFSSH_SHELL if (ret == WS_SUCCESS) { wolfSSH_Log(WS_LOG_INFO, "[SSHD] Entering new shell"); - SHELL_Subsystem(conn, ssh, pPasswd, usrConf, NULL); + ret = SHELL_Subsystem(conn, ssh, pPasswd, usrConf, + NULL); } #else wolfSSH_Log(WS_LOG_ERROR, @@ -2222,7 +2187,7 @@ static void* HandleConnection(void* arg) wolfSSH_Log(WS_LOG_INFO, "[SSHD] Entering exec session [%s]", wolfSSH_GetSessionCommand(ssh)); - SHELL_Subsystem(conn, ssh, pPasswd, usrConf, + ret = SHELL_Subsystem(conn, ssh, pPasswd, usrConf, wolfSSH_GetSessionCommand(ssh)); } #endif /* WOLFSSH_SHELL */