From 385decd6bf62c116565ece1e0992ff7a79d48474 Mon Sep 17 00:00:00 2001 From: Ron Yorston Date: Mon, 13 Mar 2023 10:25:56 +0000 Subject: runuser: new applet Add a cut down, Windows-specific implementation of `runuser` from util-linux. This allows elevated privileges to be dropped when running in an SSH session. It also works when using `su` or starting busybox-w32 'as administrator'. There are complications: - The method used to drop privileges leaves the access token in the TokenIsElevated state. Detecting this is likely to be fragile. - The unprivileged shell is started by CreateProcessAsUserA(). In older versions of Windows this has to be loaded dynamically. Adds about 900 bytes. (GitHub issue #240) --- configs/mingw32_defconfig | 3 +- configs/mingw64_defconfig | 3 +- include/mingw.h | 2 + util-linux/runuser.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++ win32/mingw.c | 43 ++++++++++++----- win32/process.c | 2 +- 6 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 util-linux/runuser.c diff --git a/configs/mingw32_defconfig b/configs/mingw32_defconfig index 360ea3b50..611fd327a 100644 --- a/configs/mingw32_defconfig +++ b/configs/mingw32_defconfig @@ -1,7 +1,7 @@ # # Automatically generated make config: don't edit # Busybox version: 1.37.0.git -# Tue Feb 7 09:34:52 2023 +# Sun Mar 12 09:41:00 2023 # CONFIG_HAVE_DOT_CONFIG=y # CONFIG_PLATFORM_POSIX is not set @@ -707,6 +707,7 @@ CONFIG_XXD=y # CONFIG_RENICE is not set CONFIG_REV=y # CONFIG_RTCWAKE is not set +CONFIG_RUNUSER=y # CONFIG_SCRIPT is not set # CONFIG_SCRIPTREPLAY is not set # CONFIG_SETARCH is not set diff --git a/configs/mingw64_defconfig b/configs/mingw64_defconfig index 0dc1d0d51..6db0accbf 100644 --- a/configs/mingw64_defconfig +++ b/configs/mingw64_defconfig @@ -1,7 +1,7 @@ # # Automatically generated make config: don't edit # Busybox version: 1.37.0.git -# Tue Feb 7 09:34:52 2023 +# Sun Mar 12 09:41:00 2023 # CONFIG_HAVE_DOT_CONFIG=y # CONFIG_PLATFORM_POSIX is not set @@ -707,6 +707,7 @@ CONFIG_XXD=y # CONFIG_RENICE is not set CONFIG_REV=y # CONFIG_RTCWAKE is not set +CONFIG_RUNUSER=y # CONFIG_SCRIPT is not set # CONFIG_SCRIPTREPLAY is not set # CONFIG_SETARCH is not set diff --git a/include/mingw.h b/include/mingw.h index b5b2fe169..e49483307 100644 --- a/include/mingw.h +++ b/include/mingw.h @@ -590,3 +590,5 @@ int has_path(const char *file); int is_relative_path(const char *path); char *get_last_slash(const char *path); const char *applet_to_exe(const char *name); +char *get_user_name(void); +char *quote_arg(const char *arg); diff --git a/util-linux/runuser.c b/util-linux/runuser.c new file mode 100644 index 000000000..f6abd9a74 --- /dev/null +++ b/util-linux/runuser.c @@ -0,0 +1,117 @@ +/* vi: set sw=4 ts=4: */ +/* + * runuser - run a shell without elevated privileges. + * This is a much restricted, Windows-specific reimplementation of + * runuser from util-linux. + * + * Copyright (c) 2023 Ronald M Yorston + * + * Licensed under GPLv2 or later, see file LICENSE in this source tree. + */ +//config:config RUNUSER +//config: bool "runuser" +//config: default y +//config: depends on PLATFORM_MINGW32 && SH_IS_ASH +//config: help +//config: Run a shell without elevated privileges + +//applet:IF_RUNUSER(APPLET(runuser, BB_DIR_USR_BIN, BB_SUID_DROP)) + +//kbuild:lib-$(CONFIG_RUNUSER) += runuser.o + +//usage:#define runuser_trivial_usage +//usage: "USER [ARG...]" +//usage:#define runuser_full_usage "\n\n" +//usage: "Run a shell without elevated privileges. The user name\n" +//usage: "must be that of the user who was granted those privileges.\n" +//usage: "Any arguments are passed to the shell.\n" + +#include "libbb.h" +#include +#include + +int runuser_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; +int runuser_main(int argc UNUSED_PARAM, char **argv) +{ + const char *user; + SAFER_LEVEL_HANDLE safer; + HANDLE token; + STARTUPINFO si; + PROCESS_INFORMATION pi; + TOKEN_MANDATORY_LABEL TIL; + // Medium integrity level S-1-16-8192 + unsigned char medium[12] = { + 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, + 0x00, 0x20, 0x00, 0x00 + }; + char *cmd, **a; + DWORD code; + // This shouldn't be necessary but without it the binary complains + // it can't find CreateProcessAsUserA on older versions of Windows. + DECLARE_PROC_ADDR(BOOL, CreateProcessAsUserA, HANDLE, LPCSTR, LPSTR, + LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, + LPVOID, LPCSTR, LPSTARTUPINFOA, LPPROCESS_INFORMATION); + + if (!INIT_PROC_ADDR(advapi32.dll, CreateProcessAsUserA)) + bb_simple_error_msg_and_die("not supported"); + + if (getuid() != 0) + bb_simple_error_msg_and_die("may not be used by non-root users"); + + if (argc < 2) + bb_show_usage(); + + user = get_user_name(); + if (user == NULL || strcmp(argv[1], user) != 0) + bb_simple_error_msg_and_die("invalid user"); + + /* + * Run a shell using a token with reduced privilege. Hints from: + * + * https://stackoverflow.com/questions/17765568/ + */ + if (SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER, + SAFER_LEVEL_OPEN, &safer, NULL) && + SaferComputeTokenFromLevel(safer, NULL, &token, 0, NULL)) { + + // Set medium integrity + TIL.Label.Sid = (PSID)medium; + TIL.Label.Attributes = SE_GROUP_INTEGRITY; + if (SetTokenInformation(token, TokenIntegrityLevel, &TIL, + sizeof(TOKEN_MANDATORY_LABEL))) { + + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + si.dwFlags = STARTF_USESTDHANDLES; + + // Build the command line + cmd = xstrdup("sh"); + for (a = argv + 2; *a; ++a) { + char *q = quote_arg(*a); + char *newcmd = xasprintf("%s %s", cmd, q); + if (q != *a) + free(q); + free(cmd); + cmd = newcmd; + } + + if (!CreateProcessAsUserA(token, bb_busybox_exec_path, + cmd, NULL, NULL, TRUE, 0, NULL, + NULL, &si, &pi)) { + errno = err_win_to_posix(); + bb_perror_msg_and_die("can't execute 'sh'"); + } + + WaitForSingleObject(pi.hProcess, INFINITE); + if (GetExitCodeProcess(pi.hProcess, &code)) { + return (int)code; + } + } + } + + return EXIT_FAILURE; +} diff --git a/win32/mingw.c b/win32/mingw.c index 712728bd6..c7eeea088 100644 --- a/win32/mingw.c +++ b/win32/mingw.c @@ -1109,7 +1109,7 @@ static char *getsysdir(void) } #define NAME_LEN 100 -static char *get_user_name(void) +char *get_user_name(void) { static char *user_name = NULL; char *s; @@ -1136,18 +1136,42 @@ static char *get_user_name(void) return user_name; } +#if ENABLE_RUNUSER +/* + * When runuser drops privileges TokenIsElevated still returns TRUE. + * Use other means to determine if we're actually unprivileged. + * This is likely to be fragile. + */ +static int +actually_unprivileged(HANDLE h) +{ + DWORD restricted = 0; + DWORD size; + + if (GetTokenInformation(h, TokenHasRestrictions, &restricted, + sizeof(restricted), &size)) { + // The token generated by runuser seems to 'have restrictions'. + return restricted != 0; + } + + return FALSE; +} +#else +# define actually_unprivileged(h) (FALSE) +#endif + int getuid(void) { int ret = DEFAULT_UID; HANDLE h; if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &h)) { - TOKEN_ELEVATION elevation; - DWORD size = sizeof(TOKEN_ELEVATION); + TOKEN_ELEVATION elevation = { 0 }; + DWORD size; if (GetTokenInformation(h, TokenElevation, &elevation, sizeof(elevation), &size)) { - if (elevation.TokenIsElevated) + if (elevation.TokenIsElevated && !actually_unprivileged(h)) ret = 0; } CloseHandle(h); @@ -1174,17 +1198,12 @@ struct passwd *getpwuid(uid_t uid) { static struct passwd p; - if (uid == 0) { + if (uid == 0) p.pw_name = (char *)"root"; - p.pw_dir = getsysdir(); - } - else if (uid == DEFAULT_UID && (p.pw_name=get_user_name()) != NULL) { - p.pw_dir = gethomedir(); - } - else { + else if (uid != DEFAULT_UID || (p.pw_name=get_user_name()) == NULL) return NULL; - } + p.pw_dir = gethomedir(); p.pw_passwd = (char *)""; p.pw_gecos = p.pw_name; p.pw_shell = NULL; diff --git a/win32/process.c b/win32/process.c index d78041251..0585f66a6 100644 --- a/win32/process.c +++ b/win32/process.c @@ -113,7 +113,7 @@ parse_interpreter(const char *cmd, interp_t *interp) * See https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=vs-2019#parsing-c-command-line-arguments * (Parsing C++ Command-Line Arguments) */ -static char * +char * quote_arg(const char *arg) { int len = 0, n = 0; -- cgit v1.2.3-55-g6feb