wolfssh/apps/wolfsshd/test/sshd_privdrop_fail_test.sh

208 lines
7.0 KiB
Bash
Executable File

#!/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
# Not PWD: that is bash's own variable and the shell rewrites it on every cd,
# so a saved copy is gone by the time it is read.
TESTDIR=`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
ROOT="$TESTDIR/../../.."
TEST_CLIENT="$ROOT/examples/client/client"
SFTP_CLIENT="$ROOT/examples/sftpclient/wolfsftp"
SCP_CLIENT="$ROOT/examples/scpclient/wolfscp"
# Absolute, and that matters. Unlike the other sshd tests this one runs from
# its own directory rather than the repository root, and the example clients
# call ChangeToWolfSshRoot() before parsing anything, so every path they are
# handed is resolved from the repository root instead of here. Relative paths
# were read as <root>/../../../keys/... and the clients died at "Error setting
# private key" without ever opening a socket.
PRIVATE_KEY="$ROOT/keys/hansel-key-ecc.der"
PUBLIC_KEY="$ROOT/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. Absolute for
# the same reason: the sftp client reads it after the chdir above.
PAYLOAD="$TESTDIR/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 $ROOT/keys/server-key.pem
AuthorizedKeysFile $TESTDIR/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 and the client logs are kept for
# debugging like the other tests, and removed on the success path below.
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
# Every failure below is ambiguous without the client's own output.
client_said() {
echo " client said: `tail -n 3 "$CLIENT_LOG" | tr '\n' '|'`"
echo " full client output in $CLIENT_LOG"
}
# 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`
# Keep the client's output. Its exit status is meaningless here (the
# connection is killed under it), but if it dies before opening a socket
# the daemon-side counters below stay flat and look like a daemon fault.
CLIENT_LOG="$TESTDIR/privdrop_client_$LABEL.log"
"$@" > "$CLIENT_LOG" 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
if [ "$AFTER_SPAWN" -eq "$BEFORE_SPAWN" ]; then
echo "FAIL: $LABEL daemon never forked a connection process"
else
echo "FAIL: $LABEL never reached the privilege drop"
fi
client_said
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"
client_said
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"
client_said
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"$PAYLOAD:." -H "$TEST_HOST" -p "$TEST_PORT"
rm -f "$TESTDIR"/privdrop_client_*.log
echo "PASS: all subsystems terminate on privilege-drop failure"
exit 0