Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/wolfsshd/test/run_all_sshd_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
176 changes: 176 additions & 0 deletions apps/wolfsshd/test/sshd_privdrop_fail_test.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF > 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
68 changes: 68 additions & 0 deletions apps/wolfsshd/test/sshd_privdrop_preload.c
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

/* 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 <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <dlfcn.h>

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);
}
6 changes: 4 additions & 2 deletions apps/wolfsshd/test/start_sshd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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]+"`
Expand Down
Loading
Loading