diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index eadb0a027..42720cbfe 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -75,6 +75,9 @@ #include #include #include +#ifdef HAVE_CRYPT_R +#include +#endif #include #include #include @@ -120,15 +123,94 @@ int (*wsshd_setregid_cb)(WGID_T, WGID_T) = setregid; int (*wsshd_setreuid_cb)(WUID_T, WUID_T) = setreuid; int (*wsshd_setegid_cb)(WGID_T) = setegid; int (*wsshd_seteuid_cb)(WUID_T) = seteuid; -struct passwd* (*wsshd_getpwnam_cb)(const char*) = getpwnam; -#define getpwnam wsshd_getpwnam_cb +struct passwd* (*wsshd_getpwnam_cb)(const char*) = NULL; int (*wsshd_setgroups_cb)(int, const WGID_T*) = wsshd_setgroups_default; #ifdef HAVE_SHADOW -struct spwd* (*wsshd_getspnam_cb)(const char*) = getspnam; -#define getspnam wsshd_getspnam_cb +struct spwd* (*wsshd_getspnam_cb)(const char*) = NULL; #endif #endif +#ifndef _WIN32 +struct passwd* wolfSSHD_GetPwnam(const char* name, WOLFSSHD_PW* store) +{ + struct passwd* res = NULL; + int err; + + if (name == NULL || store == NULL) { + errno = EINVAL; + return NULL; + } + + WMEMSET(store, 0, sizeof(*store)); +#ifdef WOLFSSHD_UNIT_TEST + if (wsshd_getpwnam_cb != NULL) { + errno = 0; + return wsshd_getpwnam_cb(name); + } +#endif + err = getpwnam_r(name, &store->pw, store->buf, sizeof(store->buf), &res); + if (res == NULL) { + WS_FORCEZERO(store, sizeof(*store)); + if (err == ERANGE) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] passwd entry for %s exceeds " + "WOLFSSHD_PW_BUF_SZ (%d)", name, WOLFSSHD_PW_BUF_SZ); + } + /* some libcs report no such user as ENOENT */ + errno = (err == ENOENT) ? 0 : err; + } + return res; +} + +void wolfSSHD_PwFree(WOLFSSHD_PW* store) +{ + if (store != NULL) { + WS_FORCEZERO(store, sizeof(*store)); + } +} +#endif + +#ifdef HAVE_SHADOW +#ifndef WSSHD_SPWD_BUF_SZ + #define WSSHD_SPWD_BUF_SZ 1024 +#endif + +typedef struct WSSHD_SPWD { + struct spwd sp; + char buf[WSSHD_SPWD_BUF_SZ]; +} WSSHD_SPWD; + +/* Looks up the shadow entry for name into store, which the caller wipes. */ +static struct spwd* AuthGetSpnam(const char* name, WSSHD_SPWD* store) +{ + struct spwd* res = NULL; +#ifdef HAVE_GETSPNAM_R + int err; +#endif + + WMEMSET(store, 0, sizeof(*store)); +#ifdef WOLFSSHD_UNIT_TEST + if (wsshd_getspnam_cb != NULL) { + return wsshd_getspnam_cb(name); + } +#endif +#ifdef HAVE_GETSPNAM_R + err = getspnam_r(name, &store->sp, store->buf, sizeof(store->buf), &res); + if (err != 0) { + if (err == ERANGE) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] shadow entry for %s exceeds " + "WSSHD_SPWD_BUF_SZ (%d)", name, WSSHD_SPWD_BUF_SZ); + } + res = NULL; + } +#else + /* The entry stays in libc static storage, which a wipe of store does + * not reach; the caller's copy of the hash is the only one it controls. */ + res = getspnam((char*)name); +#endif + return res; +} +#endif + #ifdef WOLFSSH_OSSH_CERTS /* Peer IP string buffer; sized above INET6_ADDRSTRLEN (46) with room to spare. */ #define WOLFSSHD_PEER_IP_SZ 64 @@ -534,6 +616,8 @@ static void ScanShadowFile(WFILE* f) AddShadowLineToFakeHashCache(line); WS_FORCEZERO(line, sizeof(line)); } + /* the read that ends the loop on a full cache still fills line */ + WS_FORCEZERO(line, sizeof(line)); } #ifndef WOLFSSH_USE_PAM @@ -616,6 +700,7 @@ void wolfSSHD_AuthInit(void) #ifdef HAVE_SHADOW char tmpl[WSSHD_FAKE_HASH_SZ]; struct spwd* rootShadow; + WSSHD_SPWD rootStore; #ifndef WOLFSSHD_UNIT_TEST WFILE* f = NULL; @@ -632,7 +717,7 @@ void wolfSSHD_AuthInit(void) #endif if (numCachedFakeHashes == 0) { - rootShadow = getspnam("root"); + rootShadow = AuthGetSpnam("root", &rootStore); if (rootShadow != NULL && rootShadow->sp_pwdp != NULL) { GetFakeHashFromTemplate(rootShadow->sp_pwdp, tmpl, sizeof(tmpl)); if (tmpl[0] != '\0' && tmpl[1] != '\0' && tmpl[1] != '*') { @@ -640,7 +725,9 @@ void wolfSSHD_AuthInit(void) cachedFakeHashes[0][sizeof(cachedFakeHashes[0]) - 1] = '\0'; numCachedFakeHashes = 1; } + WS_FORCEZERO(tmpl, sizeof(tmpl)); } + WS_FORCEZERO(&rootStore, sizeof(rootStore)); } if (numCachedFakeHashes == 0) { @@ -724,6 +811,13 @@ static int ExtractSalt(char* hash, char** salt, int saltSz) #endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) +#ifdef HAVE_CRYPT_R + /* the result lives in data, which the caller wipes */ + #define WSSHD_CRYPT(in, salt, data) crypt_r((in), (salt), (data)) +#else + #define WSSHD_CRYPT(in, salt, data) crypt((in), (salt)) +#endif + #ifdef WOLFSSHD_UNIT_TEST int CheckPasswordHashUnix(const char* input, const char* stored) #else @@ -746,6 +840,12 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) int locked = 0; char* hashedInput = NULL; word32 hashedInputSz = 0, storedSz = 0; +#ifdef HAVE_CRYPT_R + /* ~128 KB on glibc, ~32 KB on libxcrypt, so not on the stack */ + struct crypt_data* cryptData = NULL; +#else + void* cryptData = NULL; +#endif if (input == NULL || stored == NULL) { ret = WS_BAD_ARGUMENT; @@ -759,6 +859,20 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) return ret; } +#ifdef HAVE_CRYPT_R + if (ret == WSSHD_AUTH_SUCCESS) { + cryptData = (struct crypt_data*)WMALLOC(sizeof(*cryptData), NULL, + DYNTYPE_SSHD); + if (cryptData == NULL) { + ret = WS_MEMORY_E; + } + else { + /* zeroed, as crypt_r() requires of a fresh crypt_data */ + WMEMSET(cryptData, 0, sizeof(*cryptData)); + } + } +#endif + if (ret == WSSHD_AUTH_SUCCESS) { const char* salt = stored; @@ -786,7 +900,7 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) } } - hashedInput = crypt(input, salt); + hashedInput = WSSHD_CRYPT(input, salt, cryptData); /* glibc signals an unsupported salt with a "*"-prefixed sentinel, * not NULL; check both so the fallback engages on every libc. */ if (locked) { @@ -796,28 +910,34 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) && (hashedInput == NULL || hashedInput[0] == '*'); fbIdx++) { salt = fakeHashFallbacks[fbIdx]; - hashedInput = crypt(input, salt); + hashedInput = WSSHD_CRYPT(input, salt, cryptData); } } if (hashedInput == NULL) { ret = WS_FATAL_ERROR; } - else if (locked) { - ret = WSSHD_AUTH_FAILURE; - } else { hashedInputSz = (word32)WSTRLEN(hashedInput); - if (hashedInputSz == 0 || hashedInput[0] == '*' || + if (locked || hashedInputSz == 0 || hashedInput[0] == '*' || hashedInputSz != storedSz || ConstantCompare((const byte*)hashedInput, (const byte*)stored, storedSz) != 0) { ret = WSSHD_AUTH_FAILURE; } + /* the result is in crypt()'s static storage or in cryptData */ WS_FORCEZERO(hashedInput, hashedInputSz); } } +#ifdef HAVE_CRYPT_R + if (cryptData != NULL) { + WS_FORCEZERO(cryptData, sizeof(*cryptData)); + WFREE(cryptData, NULL, DYNTYPE_SSHD); + } +#else + WOLFSSH_UNUSED(cryptData); +#endif return ret; } #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ @@ -831,12 +951,12 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS int ret = WS_SUCCESS; char* pwStr = NULL; struct passwd* pwInfo; + WOLFSSHD_PW pwStore; #ifdef HAVE_SHADOW struct spwd* shadowInfo; + WSSHD_SPWD spStore; time_t now; int expired = 0; - /* getspnam() returns a static buffer; copy immediately before it can - * be overwritten by any subsequent call. */ char hashBuf[WSSHD_FAKE_HASH_SZ]; #endif /* The hash of the user's password stored on the system. */ @@ -862,7 +982,7 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS } if (ret == WS_SUCCESS) { - pwInfo = getpwnam((const char*)usr); + pwInfo = wolfSSHD_GetPwnam(usr, &pwStore); if (pwInfo == NULL) { /* User not found: use dummy hash to equalize timing. */ wolfSSH_Log(WS_LOG_INFO, @@ -871,11 +991,7 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS else { #ifdef HAVE_SHADOW if (pwInfo->pw_passwd[0] == 'x') { -#ifdef WOLFSSH_HAVE_LIBCRYPT - shadowInfo = getspnam((const char*)usr); -#else - shadowInfo = getspnam((char*)usr); -#endif + shadowInfo = AuthGetSpnam(usr, &spStore); if (shadowInfo == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error getting user password info"); @@ -901,8 +1017,6 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS ret = WS_FATAL_ERROR; } else { - /* Copy before any subsequent getspnam() call - * overwrites the static buffer. */ XSTRNCPY(hashBuf, shadowInfo->sp_pwdp, sizeof(hashBuf)); hashBuf[sizeof(hashBuf) - 1] = '\0'; storedHash = hashBuf; @@ -959,7 +1073,9 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS } #ifdef HAVE_SHADOW WS_FORCEZERO(hashBuf, sizeof(hashBuf)); + WS_FORCEZERO(&spStore, sizeof(spStore)); #endif + wolfSSHD_PwFree(&pwStore); WOLFSSH_UNUSED(authCtx); return ret; @@ -1625,10 +1741,11 @@ WOLFSSHD_STATIC int SearchForPubKey(const char* path, static int CheckUserUnix(const char* name) { int ret = WSSHD_AUTH_FAILURE; struct passwd* pwInfo; + WOLFSSHD_PW pwStore; wolfSSH_Log(WS_LOG_INFO, "[SSHD] Unix check user"); errno = 0; - pwInfo = getpwnam(name); + pwInfo = wolfSSHD_GetPwnam(name, &pwStore); if (pwInfo == NULL) { if (errno != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error calling getpwnam for user " @@ -1639,6 +1756,7 @@ static int CheckUserUnix(const char* name) { else { ret = WSSHD_AUTH_SUCCESS; } + wolfSSHD_PwFree(&pwStore); return ret; } @@ -1915,6 +2033,7 @@ static int CheckPublicKeyUnix(const char* name, { int ret = WSSHD_AUTH_SUCCESS; struct passwd* pwInfo; + WOLFSSHD_PW pwStore; #ifdef WOLFSSH_OSSH_CERTS if (pubKeyCtx->isOsshCert) { @@ -1933,7 +2052,7 @@ static int CheckPublicKeyUnix(const char* name, /* The requested user must be a real local account. */ if (ret == WSSHD_AUTH_SUCCESS) { errno = 0; - pwInfo = getpwnam((const char*)name); + pwInfo = wolfSSHD_GetPwnam(name, &pwStore); if (pwInfo == NULL) { if (errno != 0) { wolfSSH_Log(WS_LOG_ERROR, @@ -1974,7 +2093,7 @@ static int CheckPublicKeyUnix(const char* name, #endif /* WOLFSSH_OSSH_CERTS */ { errno = 0; - pwInfo = getpwnam((const char*)name); + pwInfo = wolfSSHD_GetPwnam(name, &pwStore); if (pwInfo == NULL) { if (errno != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error calling getpwnam for user " @@ -1989,6 +2108,7 @@ static int CheckPublicKeyUnix(const char* name, wolfSSHD_ConfigGetStrictModes(authCtx->conf)); } } + wolfSSHD_PwFree(&pwStore); WOLFSSH_UNUSED(usrCaKeysFile); WOLFSSH_UNUSED(authCtx); @@ -2412,11 +2532,13 @@ static int IsRootUser(const char* usr) int isRoot = 0; #ifndef _WIN32 struct passwd* pwInfo; + WOLFSSHD_PW pwStore; - pwInfo = getpwnam(usr); + pwInfo = wolfSSHD_GetPwnam(usr, &pwStore); if ((pwInfo != NULL && pwInfo->pw_uid == 0) || XSTRCMP(usr, "root") == 0) { isRoot = 1; } + wolfSSHD_PwFree(&pwStore); #else if (XSTRCMP(usr, "root") == 0) { isRoot = 1; @@ -3163,6 +3285,7 @@ static int SetDefaultUserID(WOLFSSHD_AUTH* auth) return 0; #else struct passwd* pwInfo; + WOLFSSHD_PW pwStore; int ret = WS_SUCCESS; if (wolfSSHD_ConfigGetPrivilegeSeparation(auth->conf) == @@ -3174,7 +3297,8 @@ static int SetDefaultUserID(WOLFSSHD_AUTH* auth) return WS_SUCCESS; } - pwInfo = getpwnam(WOLFSSH_USER_STRING(WOLFSSH_SSHD_USER)); + pwInfo = wolfSSHD_GetPwnam(WOLFSSH_USER_STRING(WOLFSSH_SSHD_USER), + &pwStore); #ifdef WOLFSSHD_UNIT_TEST /* Fallback for unit tests. */ if (pwInfo == NULL) { @@ -3194,6 +3318,7 @@ static int SetDefaultUserID(WOLFSSHD_AUTH* auth) auth->sGid = getgid(); auth->sUid = getuid(); } + wolfSSHD_PwFree(&pwStore); return ret; #endif } @@ -3709,17 +3834,22 @@ WOLFSSHD_CONFIG* wolfSSHD_AuthGetUserConf(const WOLFSSHD_AUTH* auth, * Match Group directives do not apply here */ #else struct passwd* p_passwd; + WOLFSSHD_PW pwStore; + int grpRet; - p_passwd = getpwnam((const char *)usr); + p_passwd = wolfSSHD_GetPwnam(usr, &pwStore); if (p_passwd == NULL) { + wolfSSHD_PwFree(&pwStore); return NULL; } /* Resolve the full group set (primary and supplementary) so a * Match Group directive matches on any of the user's groups. * Fail closed if the groups cannot be enumerated. */ - if (wolfSSHD_GetUserGroupNames(auth->heap, usr, p_passwd->pw_gid, - &grpNames, &grpCount) != WS_SUCCESS) { + grpRet = wolfSSHD_GetUserGroupNames(auth->heap, usr, + p_passwd->pw_gid, &grpNames, &grpCount); + wolfSSHD_PwFree(&pwStore); + if (grpRet != WS_SUCCESS) { return NULL; } #endif diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 7f6a80401..471a41efb 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -41,6 +41,27 @@ USER_NODE* AddNewUser(USER_NODE* list, byte type, const byte* username, word32 usernameSz, const byte* value, word32 valueSz); #endif +#ifndef _WIN32 +#include + +#ifndef WOLFSSHD_PW_BUF_SZ + #define WOLFSSHD_PW_BUF_SZ 4096 +#endif + +/* Caller-owned storage for an account lookup, so no copy of the entry is + * left in libc's static storage. */ +typedef struct WOLFSSHD_PW { + struct passwd pw; + char buf[WOLFSSHD_PW_BUF_SZ]; +} WOLFSSHD_PW; + +/* Looks up name into store. Returns &store->pw, or NULL when there is no such + * user (errno 0) or the lookup failed (errno set). Release the store with + * wolfSSHD_PwFree(), which wipes it, whatever the result. */ +struct passwd* wolfSSHD_GetPwnam(const char* name, WOLFSSHD_PW* store); +void wolfSSHD_PwFree(WOLFSSHD_PW* store); +#endif + int DefaultUserAuth(byte authType, WS_UserAuthData* authData, void* ctx); int DefaultUserAuthTypes(WOLFSSH* ssh, void* ctx); @@ -124,6 +145,7 @@ extern int (*wsshd_setregid_cb)(WGID_T, WGID_T); extern int (*wsshd_setreuid_cb)(WUID_T, WUID_T); extern int (*wsshd_setegid_cb)(WGID_T); extern int (*wsshd_seteuid_cb)(WUID_T); +/* NULL, the default, runs the real lookup; set, it replaces the lookup. */ extern struct passwd* (*wsshd_getpwnam_cb)(const char*); extern int (*wsshd_setgroups_cb)(int, const WGID_T*); #ifdef WOLFSSHD_HAVE_SHADOW diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index af2439d3c..aa8d377fc 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -13,8 +13,10 @@ test_cases=( "sshd_sftp_idle_cpu_test.sh" "sshd_bad_subsystem_test.sh" "sshd_scp_fail.sh" + "sshd_scp_home_test.sh" "sshd_term_close_test.sh" "sshd_stdin_eof_test.sh" + "sshd_exit_signal_test.sh" "sshd_stdin_stall_test.sh" "sshd_channel_close_test.sh" "ssh_kex_algos.sh" diff --git a/apps/wolfsshd/test/sshd_exit_signal_test.sh b/apps/wolfsshd/test/sshd_exit_signal_test.sh new file mode 100755 index 000000000..74438ead5 --- /dev/null +++ b/apps/wolfsshd/test/sshd_exit_signal_test.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# bash, unlike most of this directory: the option array below needs it. + +# A remote command killed by a signal must not report success. wolfSSHd sends +# 128 plus the signal number as the exit status, as a shell reports it. +# +# Needs the OpenSSH client, which exits with the remote exit status. + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "./sshd_exit_signal_test.sh 127.0.0.1 22222" + exit 1 +fi + +HOST="$1" +PORT="$2" +USER_NAME="${3:-`whoami`}" + +command -v ssh >/dev/null 2>&1 || { + echo "ssh not found, skipping" + exit 77 +} + +command -v timeout >/dev/null 2>&1 || { + echo "timeout not found, skipping" + exit 77 +} + +# ssh refuses a group/world readable identity file. +KEYDIR=`mktemp -d 2>/dev/null` || KEYDIR=`mktemp -d -t sshdsig` +if [ -z "$KEYDIR" ] || [ ! -d "$KEYDIR" ]; then + echo "could not create temp dir" + exit 1 +fi +trap 'rm -rf "$KEYDIR"' EXIT + +cp ../../../keys/hansel-key-ecc.pem "$KEYDIR/id_ecdsa" || exit 1 +chmod 600 "$KEYDIR/id_ecdsa" + +SSH_OPTS=(-i "$KEYDIR/id_ecdsa" -p "$PORT" + -o IdentitiesOnly=yes + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null + -o PreferredAuthentications=publickey -o PasswordAuthentication=no + -o BatchMode=yes -o ConnectTimeout=5 -o LogLevel=ERROR) + +if ! timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" true >/dev/null 2>&1 +then + echo "no session with these options, skipping" + exit 77 +fi + +# Control: a plain exit status comes through unchanged. +timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'exit 3' \ + > /dev/null 2> "$KEYDIR/exit.err" +RESULT=$? +if [ "$RESULT" != 3 ]; then + echo "expected exit status 3, got $RESULT" + cat "$KEYDIR/exit.err" + exit 1 +fi + +# The shell running the command kills itself, so it is what the daemon reaps. +timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'kill -KILL $$' \ + > /dev/null 2> "$KEYDIR/kill.err" +RESULT=$? +if [ "$RESULT" != 137 ]; then + echo "expected exit status 137 for SIGKILL, got $RESULT" + cat "$KEYDIR/kill.err" + exit 1 +fi + +exit 0 diff --git a/apps/wolfsshd/test/sshd_scp_home_test.sh b/apps/wolfsshd/test/sshd_scp_home_test.sh new file mode 100755 index 000000000..15e16eb0b --- /dev/null +++ b/apps/wolfsshd/test/sshd_scp_home_test.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +# An SCP transfer with a relative remote path resolves it from the user's home +# directory, not from the daemon's working directory. + +# Not named PWD: the shell rewrites that variable on every cd, so a saved +# copy would not survive the cd to the repository root below. +TESTDIR=`pwd` +. ./wolfssh_options.sh +cd ../../.. + +TEST_SCP_CLIENT="./examples/scpclient/wolfscp" +USER="${3:-`whoami`}" +PRIVATE_KEY="./keys/hansel-key-ecc.der" +PUBLIC_KEY="./keys/hansel-key-ecc.pub" + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "./sshd_scp_home_test.sh 127.0.0.1 22222" + exit 1 +fi + +if ! wolfssh_has SCP || [ ! -x "$TEST_SCP_CLIENT" ]; then + echo "SCP client not available in this build, skipping" + exit 77 +fi + +# The check reads the file back from the home directory directly. +case "$1" in + 127.0.0.1|localhost) ;; + *) echo "remote host, skipping"; exit 77 ;; +esac + +HOMEDIR=`getent passwd "$USER" 2>/dev/null | cut -d: -f6` +[ -n "$HOMEDIR" ] || HOMEDIR=`eval echo "~$USER"` +if [ -z "$HOMEDIR" ] || [ ! -d "$HOMEDIR" ]; then + echo "no home directory for $USER, skipping" + exit 77 +fi + +NAME="scp-home-$$.dat" +OUTDIR="`pwd`/test-$$" +mkdir "$OUTDIR" || exit 1 +trap 'rm -rf "$OUTDIR"; rm -f "$HOMEDIR/$NAME"' EXIT + +if ! dd if=/dev/urandom of="$OUTDIR/$NAME" bs=1024 count=64 2>/dev/null; then + echo "Could not create the test file $OUTDIR/$NAME" + exit 1 +fi + +echo "$TEST_SCP_CLIENT -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -L$OUTDIR/$NAME:. -H $1 -p $2" +$TEST_SCP_CLIENT -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -L"$OUTDIR/$NAME":. \ + -H $1 -p $2 +if [ "$?" != "0" ]; then + echo "Expecting to pass transfer" + exit 1 +fi + +if ! cmp -s "$OUTDIR/$NAME" "$HOMEDIR/$NAME"; then + echo "File did not land in $HOMEDIR" + exit 1 +fi + +cd "$TESTDIR" +exit 0 diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index d8dc67f89..4ded133e3 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -18,6 +18,7 @@ #include #endif #ifndef _WIN32 + #include #include #include #endif @@ -2770,10 +2771,10 @@ static int test_AuthInit(void) wolfSSHD_SetCachedFakeHashForTest(NULL); - /* Force the real getspnam(), independent of what earlier tests left - * this global as. */ + /* Force the real lookup, independent of what earlier tests left this + * global as. */ savedGetspnam = wsshd_getspnam_cb; - wsshd_getspnam_cb = getspnam; + wsshd_getspnam_cb = NULL; rootShadow = getspnam("root"); if (rootShadow == NULL || rootShadow->sp_pwdp == NULL) { @@ -3030,6 +3031,85 @@ static int test_ScanShadowFile_truncatedLine(void) #endif /* WOLFSSHD_HAVE_SHADOW */ #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ +#ifndef _WIN32 +/* The lookup fills the caller's store, and freeing it wipes the store. */ +static int test_GetPwnam(void) +{ + int ret = WS_SUCCESS; + struct passwd* (*savedGetpwnam)(const char*); + struct passwd* self; + struct passwd* pw; + WOLFSSHD_PW store; + char name[64]; + word32 i; + + savedGetpwnam = wsshd_getpwnam_cb; + wsshd_getpwnam_cb = NULL; + + self = getpwuid(getuid()); + if (self == NULL || self->pw_name == NULL || + WSTRLEN(self->pw_name) >= sizeof(name)) { + Log(" Skipping test: no passwd entry for this uid.\n"); + wsshd_getpwnam_cb = savedGetpwnam; + return WS_SUCCESS; + } + WSTRNCPY(name, self->pw_name, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + + Log(" Testing scenario: wolfSSHD_GetPwnam() for this user."); + pw = wolfSSHD_GetPwnam(name, &store); + if (pw != &store.pw || pw->pw_uid != getuid() || + WSTRCMP(pw->pw_name, name) != 0 || + (byte*)pw->pw_name < (byte*)store.buf || + (byte*)pw->pw_name >= (byte*)store.buf + sizeof(store.buf)) { + Log(" FAILED: entry not in the caller's store.\n"); + ret = WS_FATAL_ERROR; + } + wolfSSHD_PwFree(&store); + for (i = 0; ret == WS_SUCCESS && i < (word32)sizeof(store); i++) { + if (((byte*)&store)[i] != 0) { + Log(" FAILED: store not wiped.\n"); + ret = WS_FATAL_ERROR; + } + } + if (ret == WS_SUCCESS) { + Log(" PASSED.\n"); + Log(" Testing scenario: wolfSSHD_GetPwnam() for no such user."); + WMEMSET(&store, 0xA5, sizeof(store)); + errno = EIO; + if (wolfSSHD_GetPwnam("no_such_wolfsshd_user_xyz", &store) != NULL || + errno != 0) { + Log(" FAILED: expected NULL with errno 0.\n"); + ret = WS_FATAL_ERROR; + } + for (i = 0; ret == WS_SUCCESS && i < (word32)sizeof(store); i++) { + if (((byte*)&store)[i] != 0) { + Log(" FAILED: store not wiped.\n"); + ret = WS_FATAL_ERROR; + } + } + if (ret == WS_SUCCESS) { + Log(" PASSED.\n"); + } + wolfSSHD_PwFree(&store); + } + if (ret == WS_SUCCESS) { + Log(" Testing scenario: wolfSSHD_GetPwnam() with no name."); + errno = 0; + if (wolfSSHD_GetPwnam(NULL, &store) != NULL || errno == 0) { + Log(" FAILED: expected NULL with errno set.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + wsshd_getpwnam_cb = savedGetpwnam; + return ret; +} +#endif + #ifdef WOLFSSL_BASE64_ENCODE /* Build SSH wire-format public key blob. Returns size or 0 if too small. */ static word32 BuildWireKeyBlob(const char* type, const byte* payload, @@ -7364,6 +7444,9 @@ const TEST_CASE testCases[] = { TEST_DECL(test_AuthSetGroups_setgroups_fail), TEST_DECL(test_AuthSetGroups_getgrouplist_fail), #endif +#ifndef _WIN32 + TEST_DECL(test_GetPwnam), +#endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) TEST_DECL(test_CheckPasswordHashUnix), #ifdef WOLFSSHD_HAVE_SHADOW diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index c2f033687..b5c240838 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -438,6 +438,8 @@ static byte* getBufferFromFile(const char* fileName, word32* bufSz, void* heap, readSz = (word32)WFREAD(NULL, buf, 1, fileSz, file); if (readSz < (size_t)fileSz) { WFCLOSE(NULL, file); + /* whatever part of a key was read */ + WS_FORCEZERO(buf, (word32)fileSz + 1); WFREE(buf, heap, DYNTYPE_SSHD); return NULL; } @@ -1772,8 +1774,23 @@ static int SCP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, * privilege level */ exit(1); } + + /* Relative SCP paths resolve from home, inside a chroot too, as for + * SFTP and the shell. Like OpenSSH, a chroot without the home dir falls + * back to "/" quietly. */ + if (ret == WS_SUCCESS && chdir(pPasswd->pw_dir) != 0) { + if (error != 1) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to user home dir " + "%s, using /", pPasswd->pw_dir); + } + if (chdir("/") != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to /"); + } + } #else - /* impersonate the logged on user for file permissions */ + /* Impersonate the logged on user for file permissions. Relative SCP + * paths stay relative to the daemon's directory: connections share one + * process, and SCP has no per-session default path like SFTP's. */ if (ImpersonateLoggedOnUser(wolfSSHD_GetAuthToken(conn->auth)) == FALSE) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error impersonating logged on user"); @@ -2190,6 +2207,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, } if (tmp != NULL) { + WS_FORCEZERO(tmp, (word32)(sizeof(wchar_t) * cmdSz)); WFREE(tmp, NULL, DYNTYPE_SSHD); } @@ -2596,6 +2614,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, cleanup: if (conCmdPtr != NULL) { + WS_FORCEZERO(conCmdPtr, (word32)(sizeof(wchar_t) * conCmdSz)); WFREE(conCmdPtr, NULL, DYNTYPE_SSHD); } if (cnslIn != NULL) { @@ -2604,6 +2623,8 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (cnslOut != NULL) { CloseHandle(cnslOut); } + /* The resize callback must not reach ptyIn once it is closed. */ + wolfSSH_SetTerminalResizeCtx(ssh, NULL); if (ptyIn != NULL) { CloseHandle(ptyIn); } @@ -2618,6 +2639,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, wolfSSHD_AuthCloseToken(conn->auth); } if (cmd != NULL) { + WS_FORCEZERO(cmd, (word32)(sizeof(wchar_t) * cmdSz)); WFREE(cmd, NULL, DYNTYPE_SSHD); } WS_FORCEZERO(channelBuffer, sizeof channelBuffer); @@ -2679,6 +2701,40 @@ static int SHELL_SetNonBlocking(int fd) #define WOLFSSHD_CHILD_REAP_WAIT_US 50000 #endif +/* Close a descriptor that is still open and mark it closed. */ +static void SHELL_CloseFd(int* fd) +{ + if (*fd >= 0) { + close(*fd); + *fd = -1; + } +} + +/* Take down a shell child that the parent cannot service: kill it, reap it, + * and close the parent's descriptors to it. */ +static void SHELL_AbortChild(pid_t childPid, int* childFd, int* stdoutPipe, + int* stderrPipe, int* stdinPipe) +{ + int status; + int i; + + if (kill(childPid, SIGKILL) == 0) { + while (waitpid(childPid, &status, 0) < 0 && errno == EINTR) { + /* retry */ + } + } + else { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to kill shell child %d, " + "errno %d", (int)childPid, errno); + } + SHELL_CloseFd(childFd); + for (i = 0; i < 2; i++) { + SHELL_CloseFd(&stdoutPipe[i]); + SHELL_CloseFd(&stderrPipe[i]); + SHELL_CloseFd(&stdinPipe[i]); + } +} + /* Send out the last of a shell's output once the child has been reaped. The * SSH socket is non blocking, so retry a bounded number of times on a full * window, a rekey or a would block. 'ext' selects the extended (stderr) data @@ -2908,11 +2964,33 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, const char *args[] = {"-sh", NULL, NULL, NULL}; char cmd[MAX_COMMAND_SZ]; char shell[MAX_COMMAND_SZ]; + char* userName; int ret; signal(SIGINT, SIG_DFL); signal(SIGCHLD, SIG_DFL); + /* The shell needs none of the session, so drop the host key and the + * session keys this image inherited before giving up root. The + * command can live in the session, so keep a copy of it. */ + userName = WSTRDUP(wolfSSH_GetUsername(ssh), NULL, DYNTYPE_SSHD); + if (forcedCmd != NULL) { + forcedCmd = WSTRDUP(forcedCmd, NULL, DYNTYPE_SSHD); + if (forcedCmd == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error copying forced command"); + exit(1); + } + } + if (userName == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error copying user name"); + exit(1); + } + wolfSSH_free(ssh); + ssh = NULL; + wolfSSH_CTX_free(conn->ctx); + conn->ctx = NULL; + if (!ptyReq || forcedCmd) { close(stdoutPipe[0]); close(stderrPipe[0]); @@ -2939,7 +3017,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, } /* set additional groups if needed */ - if (wolfSSHD_AuthSetGroups(conn->auth, wolfSSH_GetUsername(ssh), + if (wolfSSHD_AuthSetGroups(conn->auth, userName, pPasswd->pw_gid) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting groups"); exit(1); @@ -2987,8 +3065,11 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, rc = chdir(pPasswd->pw_dir); if (rc != 0) { /* not error'ing out if unable to find home directory */ - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to user home dir %s", - pPasswd->pw_dir); + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to user home dir %s, " + "using /", pPasswd->pw_dir); + if (chdir("/") != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to /"); + } } /* default to /bin/sh if user shell is not set */ @@ -3024,15 +3105,6 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, exit(ret); /* exit child process and close down SSH connection */ } - if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, - pPasswd->pw_gid) != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); - /* 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); struct termios tios; @@ -3048,24 +3120,52 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, /* The shell child is already forked, and nothing below can reach the * peer without an id to address, so take it down with us. */ wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No session channel to service"); - kill(childPid, SIGKILL); + SHELL_AbortChild(childPid, &childFd, stdoutPipe, stderrPipe, + stdinPipe); + if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error lowering permissions level"); + exit(1); + } return WS_FATAL_ERROR; } signal(SIGINT, SIG_DFL); rc = tcgetattr(childFd, &tios); - if (rc != 0) { - return WS_FATAL_ERROR; + if (rc == 0) { + rc = tcsetattr(childFd, TCSAFLUSH, &tios); } - rc = tcsetattr(childFd, TCSAFLUSH, &tios); if (rc != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting up the shell's pty"); + SHELL_AbortChild(childPid, &childFd, stdoutPipe, stderrPipe, + stdinPipe); + if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error lowering permissions level"); + exit(1); + } return WS_FATAL_ERROR; } + /* Drop privileges only after the checks above, which may have to kill + * the child while it is still root. */ + if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, + pPasswd->pw_gid) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); + /* 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); + } + /* set initial size of terminal based on saved size */ #if defined(HAVE_SYS_IOCTL_H) - wolfSSH_DoModes(ssh->modes, ssh->modesSz, childFd); + if (ssh->modesSz > 0 && + wolfSSH_DoModes(ssh->modes, ssh->modesSz, childFd) != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error applying terminal modes"); + } { struct winsize s; @@ -3561,6 +3661,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, stdinPipe[1] = -1; } + /* The resize callback must not reach childFd once it is closed. */ + wolfSSH_SetTerminalResizeCtx(ssh, NULL); + /* Close the pty master so the shell gets SIGHUP and can exit on its own */ if (childFd >= 0) { close(childFd); @@ -3600,8 +3703,16 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, "status"); } else { - if (wolfSSH_SetExitStatus(ssh, (word32)WEXITSTATUS(waitStatus)) != - WS_SUCCESS) { + word32 exitStatus = 1; + + /* A signaled child reports 128 plus the signal, as a shell does */ + if (WIFEXITED(waitStatus)) { + exitStatus = (word32)WEXITSTATUS(waitStatus); + } + else if (WIFSIGNALED(waitStatus)) { + exitStatus = 128 + (word32)WTERMSIG(waitStatus); + } + if (wolfSSH_SetExitStatus(ssh, exitStatus) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Issue setting child's exit " "status"); } @@ -3927,6 +4038,11 @@ static void* HandleConnection(void* arg) WOLFSSHD_CONFIG* usrConf; char* usr; const char* forcedCmd = NULL; + #ifndef _WIN32 + WOLFSSHD_PW pwStore; + + WMEMSET(&pwStore, 0, sizeof(pwStore)); + #endif /* get configuration for user */ usr = wolfSSH_GetUsername(ssh); @@ -3938,10 +4054,10 @@ static void* HandleConnection(void* arg) ret = WS_FATAL_ERROR; } - #ifndef WIN32 + #ifndef _WIN32 if (ret == WS_SUCCESS || ret == WS_SFTP_COMPLETE || ret == WS_SCP_INIT) { - pPasswd = getpwnam((const char *)usr); + pPasswd = wolfSSHD_GetPwnam(usr, &pwStore); if (pPasswd == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error getting user info"); ret = WS_FATAL_ERROR; @@ -4085,6 +4201,9 @@ static void* HandleConnection(void* arg) } wolfSSHD_ConfigFree(usrConf); + #ifndef _WIN32 + wolfSSHD_PwFree(&pwStore); + #endif } error = wolfSSH_get_error(ssh); @@ -4987,7 +5106,8 @@ static int SetupConsole(char* inCmd) if (CreateProcessW(NULL, cmd, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &ext.StartupInfo, &processInfo) != TRUE) { - return WS_FATAL_ERROR; + /* the return value, once cleanup has run */ + processState = (DWORD)WS_FATAL_ERROR; } else { DWORD ava = 0; @@ -5025,6 +5145,7 @@ static int SetupConsole(char* inCmd) } if (cmd != NULL) { + WS_FORCEZERO(cmd, sizeof(wchar_t) * cmdSz); HeapFree(GetProcessHeap(), 0, cmd); } @@ -5050,7 +5171,7 @@ int main(int argc, char** argv) isService = 0; } if (WSTRCMP(argv[i], "-r") == 0) { - if (argc < i + 1) { + if (i + 1 >= argc) { /* was expecting command to run after -r argument */ return -1; } diff --git a/configure.ac b/configure.ac index 9100e4097..50bca48c7 100644 --- a/configure.ac +++ b/configure.ac @@ -506,7 +506,27 @@ AS_IF([test "x$ENABLED_SSHD" = "xyes"],[ AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_HAVE_LIBCRYPT"], [AC_MSG_RESULT([no]) AC_MSG_ERROR(libcrypt not found)])]) + # crypt_r() keeps the hash in a buffer the daemon can wipe. Probe through + # so a positive result is build-safe; macOS has neither. + AC_MSG_CHECKING([for crypt_r]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[struct crypt_data d; d.initialized = 0; + (void)crypt_r("", "", &d);]])], + [AC_DEFINE([HAVE_CRYPT_R], [1], [crypt_r() available and declared]) + AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no])]) ]) + # Only the five-argument glibc/musl form; QNX and Solaris have four. + AC_MSG_CHECKING([for getspnam_r]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[struct spwd sp; struct spwd* res; char buf[64]; + int rc = getspnam_r("", &sp, buf, sizeof(buf), &res); + (void)rc;]])], + [AC_DEFINE([HAVE_GETSPNAM_R], [1], [five-argument getspnam_r() available]) + AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no])]) ]) AC_CONFIG_LINKS([keys/gretel-key-rsa.pub:keys/gretel-key-rsa.pub diff --git a/src/internal.c b/src/internal.c index fb3f86fbb..ac45cccd6 100644 --- a/src/internal.c +++ b/src/internal.c @@ -13301,7 +13301,8 @@ int wolfSSH_DoModes(const byte* modes, word32 modesSz, int fd) * byte left over. */ - tcgetattr(fd, &term); + if (tcgetattr(fd, &term) != 0) + return -1; while (idx + TERMINAL_MODE_SZ <= modesSz && modes[idx] != WOLFSSH_TTY_OP_END && modes[idx] < WOLFSSH_TTY_INVALID) { @@ -13540,7 +13541,8 @@ int wolfSSH_DoModes(const byte* modes, word32 modesSz, int fd) idx += TERMINAL_MODE_SZ; } - tcsetattr(fd, TCSANOW, &term); + if (tcsetattr(fd, TCSANOW, &term) != 0) + return -1; return 0; } diff --git a/tests/api.c b/tests/api.c index 97b926d0c..3c57bf1f1 100644 --- a/tests/api.c +++ b/tests/api.c @@ -8034,6 +8034,31 @@ static void test_wolfSSH_ConvertConsole(void) } +#if defined(WOLFSSH_TERM) && !defined(NO_TERMIOS) && \ + defined(HAVE_SYS_IOCTL_H) && !defined(USE_WINDOWS_API) +#include + +static void test_wolfSSH_DoModes(void) +{ + static const byte modes[] = { + WOLFSSH_ECHO, 0x00, 0x00, 0x00, 0x01, + WOLFSSH_TTY_OP_END + }; + int fds[2]; + + AssertIntEQ(wolfSSH_DoModes(NULL, sizeof(modes), -1), -1); + + /* a pipe is not a terminal, so its attributes cannot be read */ + AssertIntEQ(pipe(fds), 0); + AssertIntEQ(wolfSSH_DoModes(modes, sizeof(modes), fds[0]), -1); + close(fds[0]); + close(fds[1]); +} +#else +static void test_wolfSSH_DoModes(void) { ; } +#endif + + static void test_wstrcat(void) { #ifndef WSTRING_USER @@ -9294,6 +9319,7 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_SetUsername(); test_wolfSSH_SetChannelType(); test_wolfSSH_ConvertConsole(); + test_wolfSSH_DoModes(); test_wolfSSH_CTX_UsePrivateKey_buffer(); test_wolfSSH_CTX_UseCert_buffer(); test_wolfSSH_CTX_UseCert_file(); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index a91b43734..c7ffe6635 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -1028,6 +1028,8 @@ WOLFSSH_API int wolfSSH_ConvertConsole(WOLFSSH* ssh, WOLFSSH_HANDLE handle, #endif +/* Applies the peer's pty-req terminal modes to fd. Returns 0, or -1 for a bad + * mode list or when fd's attributes cannot be read or set. */ WOLFSSH_API int wolfSSH_DoModes(const byte* modes, word32 modesSz, int fd); WOLFSSH_API WS_SessionType wolfSSH_GetSessionType(const WOLFSSH* ssh); WOLFSSH_API const char* wolfSSH_GetSessionCommand(const WOLFSSH* ssh);