commit: b7e0fba407b95fb0ce52eedd40fd8e9a1ad3d47b
parent ecb2ad2ea31d41979d5735773659d112027a1f8e
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Mon, 17 Nov 2025 01:30:28 +0100
suc: new
Diffstat:
| M | Makefile | 9 | +++++++-- |
| M | su.c | 2 | +- |
| A | suc.1 | 60 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | suc.c | 243 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 311 insertions(+), 3 deletions(-)
diff --git a/Makefile b/Makefile
@@ -12,9 +12,9 @@ MANDIR ?= $(PREFIX)/share/man
# Dynamically linked executables are unsupported
LDSTATIC = -static
-SYS_EXE = login su
+SYS_EXE = login su suc
TEST_EXE = common_test
-MAN1 = login.1
+MAN1 = login.1 suc.1
all: $(SYS_EXE)
@@ -31,6 +31,10 @@ su_SRC = su.c common.c
su: $(su_SRC) Makefile
$(CC) -std=c17 $(CFLAGS) -o $@ $(su_SRC) -lcrypt $(LDFLAGS) $(LDSTATIC)
+suc_SRC = suc.c common.c
+suc: $(suc_SRC) Makefile
+ $(CC) -std=c17 $(CFLAGS) -o $@ $(suc_SRC) -lcrypt $(LDFLAGS) $(LDSTATIC)
+
common_test: common_test.c common.c Makefile
$(CC) -std=c17 $(CFLAGS) -o $@ common_test.c common.c -lcrypt $(LDFLAGS) $(LDSTATIC)
@@ -44,6 +48,7 @@ install: all
cp $(SYS_EXE) $(DESTDIR)$(SYS_BINDIR)/
chmod 0755 $(DESTDIR)$(SYS_BINDIR)/login
chmod 4755 $(DESTDIR)$(SYS_BINDIR)/su
+ chmod 4755 $(DESTDIR)$(SYS_BINDIR)/suc
mkdir -p $(DESTDIR)$(MANDIR)/man1/
cp $(MAN1) $(DESTDIR)$(MANDIR)/man1/
diff --git a/su.c b/su.c
@@ -31,7 +31,6 @@ main(int argc, char *argv[])
{
bool opt_l = false;
bool opt_p = false;
- int c = EOF;
const char *username = "root";
struct passwd *pwent = NULL;
char *shell = NULL;
@@ -43,6 +42,7 @@ main(int argc, char *argv[])
return 1;
}
+ int c = EOF;
/* flawfinder: ignore CWE-120, CWE-20 */
while((c = getopt(argc, argv, ":c:ls:p")) != EOF)
{
diff --git a/suc.1 b/suc.1
@@ -0,0 +1,60 @@
+.\" SPDX-FileCopyrightText: 2022 Haelwenn (lanodan) Monnier <contact+skeud@hacktivis.me>
+.\" SPDX-License-Identifier: MPL-2.0
+.Dd Nov 17, 2025
+.Dt SUC 1
+.Os
+.Sh NAME
+.Nm suc
+.Nd switch-user and chainload into a command / shell
+.Sh SYNOPSIS
+.Nm
+.Op Fl lp
+.Op Fl s Ar shell
+.Op Fl u Ar username
+.Op Ar command Op Ar argument...
+.Sh DESCRIPTION
+The
+.Nm
+utility switches into another user (root if
+.Fl u
+is unspecified) and then executes
+.Ar command ,
+or if unspecified, fallbacks to the user's shell.
+.Ss OPTIONS
+.Bl -tag -width __
+.It Fl l
+Make the fallback shell a login shell via prefixing it's basename
+passed to argv0 with a dash.
+.It Fl p
+Preserve environment.
+.It Fl u Ar username
+Switch to
+.Ar username
+.It Fl s Ar shell
+Use
+.Ar shell
+as the target shell, only allowed when root is the user launching the
+.Nm
+utility.
+.El
+.Sh EXIT STATUS
+If
+.Ar command
+or fallback shell is invoked, the exit status of
+.Nm
+shall be their exit status.
+Otherwise, the
+.Nm
+utility shall exit with one of the following values:
+.Bl -tag -width Ds
+.It 1
+An error occured or the authentication failed
+.It 126
+User's shell was found but couldn't be invoked.
+.It 127
+User's shell wasn't found.
+.El
+.Sh STANDARDS
+N/A
+.Sh AUTHORS
+.An Haelwenn (lanodan) Monnier Aq Mt contact+skeud@hacktivis.me
diff --git a/suc.c b/suc.c
@@ -0,0 +1,243 @@
+// SPDX-FileCopyrightText: 2022-2023 Haelwenn (lanodan) Monnier <contact+skeud@hacktivis.me>
+// SPDX-License-Identifier: MPL-2.0
+
+#define _POSIX_C_SOURCE 202405L
+// for explicit_bzero, initgroups
+#define _DEFAULT_SOURCE
+
+#ifdef __linux__
+// I love linux extensions (no)
+#include <shadow.h> /* getspnam */
+#endif
+
+#include "common.h" // skeud_getpass, skeud_crypt_check
+
+#include <assert.h> // assert
+#include <errno.h> // errno
+#include <grp.h> // initgroups
+#include <limits.h> // NAME_MAX
+#include <pwd.h> // getpwnam
+#include <stdbool.h> // bool
+#include <stdio.h> // fprintf, perror
+#include <stdlib.h> // abort, setenv
+#include <string.h> // strcmp, explicit_bzero
+#include <unistd.h> // getuid, getopt, opt*, chdir, setuid, setgid
+
+extern char **environ;
+char *envclear[] = {NULL};
+const char *argv0 = "suc";
+
+int
+main(int argc, char *argv[])
+{
+ bool opt_l = false;
+ bool opt_p = false;
+ const char *username = "root";
+ struct passwd *pwent = NULL;
+ char *shell = NULL;
+
+ if(geteuid() != 0)
+ {
+ fputs("suc: error: Not effectively super-user. Missing setuid?\n", stderr);
+ return 1;
+ }
+
+ int c = EOF;
+ /* flawfinder: ignore CWE-120, CWE-20 */
+ while((c = getopt(argc, argv, ":ls:pu:")) != EOF)
+ {
+ switch(c)
+ {
+ case 'l': // login-mode
+ opt_l = true;
+ break;
+ case 's': // shell
+ if(getuid() != 0)
+ {
+ fputs("suc: error: Only the super-user can override the target shell\n", stderr);
+ return 1;
+ }
+
+ shell = optarg;
+ break;
+ case 'p': // preserve environment
+ if(getuid() != 0)
+ {
+ fputs("suc: error: Only the super-user can preserve the environment\n", stderr);
+ return 1;
+ }
+
+ opt_p = true;
+ break;
+ case 'u': // username
+ username = optarg;
+ break;
+ case ':':
+ fprintf(stderr, "suc: error: Option -%c requires an operand\n", optopt);
+ return 1;
+ case '?':
+ fprintf(stderr, "suc: error: Unrecognized option: '-%c'\n", optopt);
+ return 1;
+ default:
+ fputs("suc: error: Unknown getopt state, aborting\n", stderr);
+ abort();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ errno = 0;
+ pwent = getpwnam(username);
+
+ if(pwent == NULL)
+ {
+ if(errno != 0)
+ {
+ perror("suc: error: Failed getting passwd entry");
+ }
+ else
+ {
+ fprintf(stderr, "suc: error: getpwnam: No entry found for user %s\n", username);
+ }
+
+ return 1;
+ }
+
+ if(shell == NULL)
+ {
+ if(pwent->pw_shell)
+ {
+ shell = pwent->pw_shell;
+ }
+ else
+ {
+ fprintf(stderr, "suc: error: No shell entry for user %s\n", username);
+
+ return 1;
+ }
+ }
+
+ fprintf(stderr, "suc: info: Authenticating as %s\n", username);
+
+ if(getuid() != 0)
+ {
+ char *pw_hash = NULL;
+ if(pwent->pw_passwd)
+ {
+ pw_hash = pwent->pw_passwd;
+ }
+
+#ifdef __linux__
+ // Always fetched to avoid potentially leaking passwd contents
+ errno = 0;
+ struct spwd *swent = getspnam(username);
+ if(errno != 0)
+ {
+ perror("suc: warning: getspnam");
+ }
+ else
+ {
+ if(pw_hash && pw_hash[0] == 'x' && pw_hash[1] == 0)
+ {
+ pw_hash = swent->sp_pwdp;
+ }
+
+ explicit_bzero(swent, sizeof(swent));
+ swent = NULL;
+ }
+#endif /* __linux__ */
+
+ char *password = NULL;
+ ssize_t got = skeud_getpass(&password);
+ if(got < 0)
+ {
+ free(password);
+ return 1;
+ }
+
+ bool valid_p = skeud_crypt_check(pw_hash, password);
+ explicit_bzero(password, got);
+ free(password);
+ if(pw_hash) explicit_bzero(pw_hash, sizeof(pw_hash));
+
+ if(!valid_p)
+ {
+ sleep(2);
+ fprintf(stderr, "suc: error: Invalid username or password\n");
+ return 1;
+ }
+ }
+
+ if(!opt_p)
+ {
+ char *term = getenv("TERM");
+ environ = envclear;
+ if(term)
+ {
+ setenv("TERM", term, 1);
+ }
+ }
+
+ if(setgid(pwent->pw_gid) < 0)
+ {
+ perror("suc: error: setgid");
+ return 1;
+ }
+ if(initgroups(username, pwent->pw_gid) < 0)
+ {
+ perror("suc: error: initgroups");
+ return 1;
+ }
+ if(setuid(pwent->pw_uid) < 0)
+ {
+ perror("suc: error: setuid");
+ return 1;
+ }
+
+ const char *home = pwent->pw_dir ? pwent->pw_dir : "/";
+ setenv("HOME", home, 1);
+
+ static char shell0[NAME_MAX] = "sh";
+ if(opt_l)
+ {
+ const char *sh_basename = strrchr(shell, '/');
+
+ sh_basename = sh_basename ? sh_basename + 1 : shell;
+
+ strlcpy(shell0, "-sh", NAME_MAX - 1);
+ strlcpy(shell0 + 1, sh_basename, NAME_MAX - 1);
+
+ if(chdir(home) != 0) perror("suc: chdir");
+ }
+
+ explicit_bzero(pwent, sizeof(pwent));
+ pwent = NULL;
+
+ setenv("USER", username, 1);
+ setenv("LOGNAME", username, 1);
+ setenv("SHELL", shell, 1);
+ setenv("IFS", " \t\n", 1);
+
+ static char *args_shell0[] = {shell0, NULL};
+
+ char **args = argc > 0 ? argv : args_shell0;
+ char *arg0 = argc > 0 ? argv[0] : shell;
+
+ errno = 0;
+ /* flawfinder: ignore CWE-78 */
+ int ret = execvp(arg0, args);
+ if(ret < 0)
+ {
+ if(errno == ENOENT)
+ {
+ perror("suc: execve");
+ return 127;
+ }
+ else
+ {
+ perror("suc: execve");
+ return 126;
+ }
+ }
+}