Compare commits

..

No commits in common. 'master' and 'noproxy' have entirely different histories.

  1. 1
      .gitignore
  2. 18
      README.md
  3. 16
      TODO
  4. 96
      config.template
  5. 23
      makefile
  6. 77
      us.1
  7. 756
      us.c
  8. 105
      us.conf.5

1
.gitignore vendored

@ -1 +0,0 @@
us

@ -3,21 +3,3 @@
Opens a shell as a different user without needing to authenticate as that user. Opens a shell as a different user without needing to authenticate as that user.
This is similar to the behavior of `su` but it allows to execute privileged This is similar to the behavior of `su` but it allows to execute privileged
and/or commands as any other user without needing their password. and/or commands as any other user without needing their password.
It doesn't use PAM, instead relies on shadow files and the passwd database
to check if the given password is correct. Support for different methods of
authentication is planned.
## Installing
To install use `make install` and to uninstall use `make uninstall`, all
other documentation is provided as man pages, `man us` will give general
information about the utility and how to use it and `man us.conf` contains
info about the config file format.
## Supported platforms
All platforms that store the encrypted password in `/etc/passwd` should be
supported, which to this day are none. Linux is supported through `shadow(3)`
and OpenBSD through its API. Support for more systems is planned.
This program is libc-agnostic as (apart from authentication) it is POSIX
compliant code.

16
TODO

@ -0,0 +1,16 @@
- Modify the following environment variables: (listed in environ(7))
* USER -> to target user
* LOGNAME -> to target user
* SHELL -> to the target user's SHELL
* HOME -> to the target user's HOME
- fork before exec, that is because processes might try to kill us or the
command but since they may run under elevated privileges they will get
permission denied error. If we remain the parent processes, unprivileged
proceses can send signals to us and we will relay them to our children
running at the same privilege as us. This is useful when:
- The child command hangs and we want to cose it, kinda
problematic but we could run kill with us as well
- The parent shell dies and children need to be killed, then
since one of their children (us) has higher privileges
they can't kill us and we would end up as zombies

@ -0,0 +1,96 @@
SECURITY CONSIDERATIONS
=======================
1. commands must be given by absolute path, that's because if you do otherwise
nopassword commands could be hijacked:
in the config:
nopass badguy as root cmd zzz
in the shell:
~ $ export PATH=/home/badguy/test:$PATH
~ $ mkdir test
~ $ printf '#!/bin/sh\nrm -rf --no-preserve-root' > test/zzz
~ $ chmod +x test/zzz
~ $ us zzz #this deletes the filesystem without password!
IDEA 1
======
# this is a comment
# rules are goruped by user/group
# rules are structured somewhat like json, example:
# Only 'command' is allowed to run without a password, all the rest is blocked
ale {
allow {
command nopass
}
deny {
/.*/
}
}
IDEA 2 - THE DOAS WAY
=====================
# this is a comment
# every line is a rule
# rules are structured like this:
permit|deny [options] identity [as target] [cmd command [args ...]]
# look at doas.conf(5) for more information
IDEA 2-3
========
# reverse-doas way
-> identity permit|deny [command [args ...]] [options]
# but how would I distinguish between command and options?
-> identity [options] permit|deny [command [args ...]]
# spaces are not a very good separatow when in comes to commands
-> identity,[options],permit|deny,[command [args ...]]
#
# this is kinda similar to a crontab, basically options are required
#
# config structure:
-> identity options as action [command [args ...]]
^ ^ ^ ^
can be * | | permit, deny
can be nil (NULL) |
can be *
# permit user "ale" to execute command "shutdown" as root without password:
-> ale nopass root permit shutdown
# permit members of the wheel group to execute any comands as any user:
-> :wheel nil * permit
# deny users of the wheel group to execute commands that begin with "sys":
# this could be circumvented by having the command inside a shell script
-> :wheel nil * deny /sys.*/
# deny all users to execute all comands as any other user
-> * nil * deny
#
# let's scramble things up to make more sense
#
[action] options identity as [command [args ...]]
^ ^ ^ ^
| | can both be * (any)
| can be none, comma separated
none: permit
'!': deny (negate rule)
# allow users of the wheel group to execute any command as root:
-> none :wheel root
# deny all users to execute commands that start with "sys"
-> ! none * * /sys.*/
IDEA 3 - THE SUCKLESS WAY
=========================
configuration should happen inside a source file called config.h, to apply
changes to the configuration the program has to be recompiled

@ -1,34 +1,27 @@
.POSIX:
CC ?= gcc CC ?= gcc
CFLAGS = -Wall -pedantic --std=c99 -O2 CFLAGS = -Wall -pedantic --std=c99 -O2
DBG_CFLAGS = -Wall -Werror -pedantic --std=c99 -O0 -g DBG_CFLAGS = -Wall -Werror -pedantic --std=c99 -O0 -g
SYSTEM != uname LDFLAGS = -lpam -lpam_misc
LDFLAGS != if [ '${SYSTEM}' != 'OpenBSD' ]; then echo '-lcrypt'; fi
PREFIX = /usr/local PREFIX = /usr/local
MANPREFIX = ${PREFIX}/share/man MANPREFIX = ${PREFIX}/share/man
us: us.c us: us.c
dbg: dbg:
${CC} ${LDFLAGS} ${DBG_CFLAGS} us.c -o us gcc ${LDFLAGS} ${DBG_CFLAGS} us.c -o us
install: us install: us
mkdir -p ${DESTDIR}${PREFIX}/bin mkdir -p ${DESTDIR}${PREFIX}/bin
cp -f us ${DESTDIR}${PREFIX}/bin/us cp -f us ${DESTDIR}${PREFIX}/bin/us
chown 0:0 ${DESTDIR}${PREFIX}/bin/us chown root:root ${DESTDIR}${PREFIX}/bin/us
chmod 4755 ${DESTDIR}${PREFIX}/bin/us chmod 4755 ${DESTDIR}${PREFIX}/bin/us
mkdir -p ${DESTDIR}${MANPREFIX}/man1 # mkdir -p ${DESTDIR}${MANPREFIX}/man1
cp -f us.1 ${DESTDIR}${MANPREFIX}/man1/us.1 # cp -f us.1 ${DESTDIR}${MANPREFIX}/man1/us.1
chmod 644 ${DESTDIR}${MANPREFIX}/man1/us.1 # chmod 644 ${DESTDIR}${MANPREFIX}/man1/us.1
mkdir -p ${DESTDIR}${MANPREFIX}/man5
cp -f us.conf.5 ${DESTDIR}${MANPREFIX}/man5/us.conf.5
chmod 644 ${DESTDIR}${MANPREFIX}/man5/us.conf.5
uninstall: uninstall:
rm -f ${DESTDIR}${PREFIX}/bin/us \ rm -f ${DESTDIR}${PREFIX}/bin/us
${DESTDIR}${MANPREFIX}/man1/us.1 # ${DESTDIR}${MANPREFIX}/man1/us.1
${DESTDIR}${MANPREFIX}/man1/us.conf.5
clean: clean:
rm -f us us-dbg rm -f us us-dbg

77
us.1

@ -1,77 +0,0 @@
.TH US 1 "JULY 2021" "Alessandro Mauri"
.SH NAME
us \- execute command with another identity
.SH SYNOPSIS
.SY us
.OP \-hseA
.OP \-u user
.OP \-g group
.OP \-C config
.OP command
.OP args
.YS
.SH DESCRIPTION
.PP
The
.BR us
utility executes the given command as another identity, which by default is
root. If no command is specified, it starts a shell as that user.
.PP
In order to execute anything users need to authenticate and the user + target
identity configuration must be allowed in the configuration file, see
.BR us.conf(5)
for more information.
.PP
By default when a command or shell gets executed a new environment gets created,
USER is set with the target user, as well as LOGNAME, SHELL and HOME get all set
with the default values for the target user.
PATH, TERM, EDITOR, VISUAL, DISPLAY and XAUTHORITY instead are kept between
execution.
Lastly a new variable US_USER is added (but not overridden) which contains the
calling user's username.
.PP
Invoking the program logs by default to
.BR syslog(2)
the outcome of the invocation, this behavior can be changed in the config.
.SH OPTIONS
.IP \-h
Print usage info message.
.IP \-s
Use the calling user's SHELL instead of the target user's one.
.IP \-e
Keep the entire environment between execution instead of just PATH, TERM,
EDITOR, VISUAL, DISPLAY and XAUTHORITY; user variables still get overridden.
.IP \-A
Instead of prompting for a password,
.BR us
executes the command specified in the variable US_ASKPASS and reads it's stdout
as the password. If US_ASKPASS is not specified then it will fall back
prompting the password.
.IP "\-u user"
Change the target identity to
.I user
(default is root).
.IP "\-g group"
Set the group of the target user to
.I group
instead of the target user's default, also add it to the group list.
.IP "\-C config"
Use the specified config file
.SH "RETURN VALUE"
The
.BR us
utility returns 0 on success and != 0 on failure which may occur on various
occasions. An error message will be outputted to specify the reason.
.SH "SEE ALSO"
.BR su(1)
.BR us.conf(5)
.SH AUTHOR
Alessandro Mauri <alemauri001@tuta.io>

756
us.c

@ -17,16 +17,9 @@
*/ */
#define _POSIX_C_SOURCE 200809L #define _POSIX_C_SOURCE 200809L
#ifdef __linux__
#define _DEFAULT_SOURCE #define _DEFAULT_SOURCE
#else
#define _BSD_SOURCE
#endif
#include <sys/types.h> #include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -35,111 +28,34 @@
#include <unistd.h> #include <unistd.h>
#include <limits.h> #include <limits.h>
#include <grp.h> #include <grp.h>
#include <termios.h> #include <security/pam_appl.h>
#include <stdarg.h> #include <security/pam_misc.h>
#include <ctype.h>
#include <signal.h>
#include <time.h>
#include <fcntl.h>
#include <syslog.h>
#if !defined(_XOPEN_CRYPT) || _XOPEN_CRYPT == -1
#include <crypt.h>
#endif
#if defined(__linux__)
#include <shadow.h>
#endif
#define MAX_HASH 1024
#define PASS_MAX 1024
#define CONF_LINE_MAX 1024
#define GROUPS_MAX 256
#define STR_MAX 1024
#define FLAG_PERSIST 0x1
#define FLAG_NOPASS 0x2
#define FLAG_NOLOG 0x4
#define SESSION_FILE_DIR "/var/run"
#define SESSION_TIMEOUT (60*5)
#define FAIL_PAUSE 3
struct env_elem {
char *name;
char *value;
};
struct config {
int type;
int flags;
char *who;
char *as;
struct env_elem *env;
int env_n;
};
struct user_info {
union {
struct passwd pw;
struct group gr;
} d;
char str[STR_MAX];
};
static void *emalloc(size_t);
static char *estrdup(const char *);
void *erealloc(void *, size_t);
static void usage(int);
static void die(const char *, ...);
static int perm_set(struct passwd *, struct group *);
static int authenticate(uid_t, int, int);
static struct passwd* user_to_passwd(const char *, struct user_info *);
static struct group* group_to_grp(const char *, struct user_info *);
static int get_config(struct config **, int *);
extern char **environ; static void usage (void);
char *config_file = "/etc/us.conf"; static int perm_set (struct passwd *, struct group *);
int tty_fd = STDOUT_FILENO; static int authenticate (const char *);
struct termios tio_before = {0}; static struct passwd* user_to_passwd (const char *);
static struct group* group_to_grp (const char *);
//static int execvpe(const char *, char *const *, char *const *);
void int_handler(int signum) // FIXME: misc_conv is a separate library, should stick to plain PAM or make
{ // our own pam module
(void)signum; static struct pam_conv conv = {misc_conv, NULL};
if (tio_before.c_iflag || tio_before.c_oflag || tio_before.c_iflag)
tcsetattr(tty_fd, TCSANOW, &tio_before); extern char **environ;
putchar('\n');
exit(signum);
}
int main(int argc, char *argv[]) int main (int argc, char *argv[])
{ {
char *t_usr = "root", *t_grp = NULL; char *t_usr = "root", *t_grp = NULL;
struct passwd *t_pw; struct passwd *t_pw;
struct group *t_gr; struct group *t_gr;
struct user_info t_gr_info = {0}, t_pw_info = {0};
int opt, err; int opt, err;
int shellflag = 0, envflag = 0, askpass = 0; int shellflag = 0, envflag = 0;
while ((opt = getopt(argc, argv, "A:u:g:C:se")) != -1) {
/* Save the terminal setup, don't fail since we don't know if we'll
* need it, save it because some shells don't reset termios upon
* program exit, if we don't reset it after a SIGINT or SIGTERM then
* the controlling terminal will be stuck in no echo */
if (tcgetattr(tty_fd, &tio_before) == -1) {
tio_before.c_iflag = 0;
tio_before.c_oflag = 0;
tio_before.c_cflag = 0;
}
struct sigaction action;
memset(&action, 0, sizeof(action));
action.sa_handler = int_handler;
if (sigaction(SIGINT, &action, NULL) == -1)
die("Error setting interrupt handler:");
if (sigaction(SIGTERM, &action, NULL) == -1)
die("Error setting interrupt handler:");
while ((opt = getopt(argc, argv, "Au:g:C:seh")) != -1) {
switch (opt) { switch (opt) {
case 'A': case 'A':
askpass = 1; printf("-A is not yet implemented\n");
exit(EXIT_FAILURE);
break; break;
case 'u': case 'u':
t_usr = optarg; t_usr = optarg;
@ -148,7 +64,8 @@ int main(int argc, char *argv[])
t_grp = optarg; t_grp = optarg;
break; break;
case 'C': case 'C':
config_file = optarg; printf("-C is not yet implemented\n");
exit(EXIT_FAILURE);
break; break;
case 's': case 's':
shellflag = 1; shellflag = 1;
@ -156,146 +73,36 @@ int main(int argc, char *argv[])
case 'e': case 'e':
envflag = 1; envflag = 1;
break; break;
case 'h':
usage(1);
exit(EXIT_SUCCESS);
break;
case '?': case '?':
usage(0); usage();
exit(EINVAL); exit(EINVAL);
break; break;
} }
} }
/* Get user info */ /* Get user info */
const char *uname;
char *shell; char *shell;
uid_t ruid = getuid(); uid_t ruid = getuid();
struct passwd *my_pw = NULL; struct passwd *my_pw = getpwuid(ruid);
struct user_info my_info = {0};
getpwuid_r(ruid, &my_info.d.pw, my_info.str, STR_MAX, &my_pw);
if (!my_pw) { if (!my_pw) {
fprintf(stderr, "getpwid: %s\n", strerror(errno)); fprintf(stderr, "getpwid: %s\n", strerror(errno));
return errno; return errno;
} }
char *my_name = my_pw->pw_name; uname = my_pw->pw_name;
gid_t my_groups[GROUPS_MAX];
int n_groups = 0;
if ((n_groups = getgroups(GROUPS_MAX, my_groups)) == -1)
die("getgroups:");
/* Get target user and group info */ /* Authenticate */
t_pw = user_to_passwd(t_usr, &t_pw_info); if (authenticate(uname) != PAM_SUCCESS)
if (!t_pw) exit(EXIT_FAILURE);
die("user_to_passwd:");
t_gr = group_to_grp(t_grp, &t_gr_info);
gid_t t_groups[GROUPS_MAX];
int nt_groups = GROUPS_MAX;
if (getgrouplist(t_pw->pw_name, t_pw->pw_gid, t_groups, &nt_groups) == -1)
die("getgrouplist:");
/* Don't have to wait for children */
struct sigaction sa = {0};
sa.sa_handler = SIG_DFL;
sa.sa_flags = SA_NOCLDWAIT;
if (sigaction(SIGCHLD, &sa, NULL) == -1)
die("sigaction:");
/* From now on most actions require root */
if (setuid(0) == -1)
die("setuid:");
if (setgid(0) == -1)
die("setgid:");
/* get info from the config file and check if the action we want to
* do is permitted */
struct env_elem *env_extra = NULL;
struct config *conf = NULL;
int conf_num, conf_flags = 0, env_extra_n = 0;
if (get_config(&conf, &conf_num) <= 0)
die("get_config: invalid config");
int here = 0;
for (int i = 0; i < conf_num; i++) {
struct passwd *who_pw, *as_pw;
struct group *who_gr, *as_gr;
struct user_info who_info = {0}, as_info = {0};
int who_usr = conf[i].who[0] == ':' ? 0 : 1;
int as_usr = conf[i].as[0] == ':' ? 0 : 1;
if (who_usr) {
who_pw = user_to_passwd(conf[i].who, &who_info);
if (!who_pw)
die("%s not a valid user", conf[i].who);
if (my_pw->pw_uid != who_pw->pw_uid)
continue;
} else {
who_gr = group_to_grp(conf[i].who+1, &who_info);
if (!who_gr)
die("%s not a valid group", conf[i].who);
gid_t w_gid = who_gr->gr_gid;
int x = 0;
for (; x < n_groups && w_gid != my_groups[x]; x++);
if (x == n_groups)
continue;
}
if (as_usr) {
as_pw = user_to_passwd(conf[i].as, &as_info);
if (!as_pw)
die("%s not a valid user", conf[i].as);
if (t_pw->pw_uid != as_pw->pw_uid)
continue;
} else {
as_gr = group_to_grp(conf[i].as+1, &as_info);
if (!as_gr)
die("%s not a valid group", conf[i].as);
gid_t a_gid = as_gr->gr_gid;
int x = 0;
for (; x < nt_groups && a_gid != t_groups[x]; x++);
if (x == nt_groups)
continue;
}
here = 1;
if (conf[i].type == 0)
die("Permission denied");
conf_flags |= conf[i].flags;
if (conf[i].env_n) {
env_extra = erealloc(env_extra,
(env_extra_n+conf[i].env_n+1)*sizeof(struct env_elem));
for (int j = env_extra_n, x = 0; j < env_extra_n + conf[i].env_n; j++, x++)
env_extra[j] = conf[i].env[x];
env_extra_n += conf[i].env_n;
env_extra[env_extra_n] = (struct env_elem){NULL,NULL};
}
/* Get target user and group info */
t_pw = user_to_passwd(t_usr);
if (!t_pw) {
fprintf(stderr, "user_to_passwd: %s\n", strerror(errno));
return errno;
} }
/* We don't need conf anymore */ t_gr = group_to_grp(t_grp);
for (int i = 0; i < conf_num; i++) {
free(conf[i].who);
free(conf[i].as);
if (conf[i].env && conf[i].env_n)
free(conf[i].env);
}
free(conf);
/* No configuration was fount, can't proceed */
if (!here)
die("no rule found for user %s", my_name);
/* Authenticate, we will be root from now on */
if (!(conf_flags & FLAG_NOPASS))
if (authenticate(my_pw->pw_uid, askpass, conf_flags & FLAG_PERSIST)) {
if (!(conf_flags & FLAG_NOLOG))
exit(EXIT_FAILURE);
char cmd[1024] = {0};
for (int i = optind, x = 0; argv[i] && x < 1024; i++)
x += snprintf(cmd, 1024-x, "%s ", argv[i]);
openlog("us", LOG_NOWAIT, LOG_AUTH);
syslog(LOG_NOTICE, "user %s tried to run %s as %s"
"but failed", my_name, cmd, t_pw->pw_name);
closelog();
exit(EXIT_FAILURE);
}
/* Get target user's shell */ /* Get target user's shell */
if (!shellflag) if (!shellflag)
@ -307,18 +114,40 @@ int main(int argc, char *argv[])
/* Set argc and argv */ /* Set argc and argv */
int c_argc = argc - optind; int c_argc = argc - optind;
char **c_argv = NULL; char **c_argv;
if (c_argc) { if (c_argc) {
c_argv = emalloc(sizeof(char *) * (c_argc + 1)); c_argv = malloc(sizeof(char *) * (c_argc + 1));
for (int i = 0; optind < argc; optind++, i++) if (!c_argv) {
c_argv[i] = estrdup(argv[optind]); fprintf(stderr, "malloc: %s\n", strerror(errno));
exit(errno);
}
for (int i = 0; optind < argc; optind++, i++) {
c_argv[i] = strdup(argv[optind]);
if (!c_argv[i]) {
fprintf(stderr, "strdup: %s\n", strerror(errno));
exit(errno);
}
}
} else { } else {
c_argc = 1; c_argc = 1;
c_argv = emalloc(sizeof(char *) * (c_argc + 1)); c_argv = malloc(sizeof(char *) * (c_argc + 1));
c_argv[0] = estrdup(shell); if (!c_argv) {
fprintf(stderr, "malloc: %s\n", strerror(errno));
exit(errno);
}
c_argv[0] = strdup(shell);
if (!c_argv[0]) {
fprintf(stderr, "strdup: %s\n", strerror(errno));
exit(errno);
}
} }
c_argv[c_argc] = NULL; c_argv[c_argc] = NULL;
struct env_elem {
char *name;
char *value;
};
struct env_elem env_keep[] = { struct env_elem env_keep[] = {
{"PATH", NULL}, {"PATH", NULL},
{"TERM", NULL}, {"TERM", NULL},
@ -337,12 +166,10 @@ int main(int argc, char *argv[])
{NULL, NULL} {NULL, NULL}
}; };
/* Copy what has to be copied and then clear the environment, we'll if (envflag) { /* clear env */
* make a fresh one later */
if (envflag) {
for (int i = 0; env_keep[i].name; i++) for (int i = 0; env_keep[i].name; i++)
env_keep[i].value = estrdup(getenv(env_keep[i].name)); env_keep[i].value = strdup(getenv(env_keep[i].name));
environ = NULL; environ = NULL; // in place of clearenv
} }
for (int i = 0; env_mod[i].name; i++) { for (int i = 0; env_mod[i].name; i++) {
@ -353,30 +180,19 @@ int main(int argc, char *argv[])
} }
} }
for (int i = 0; envflag &&env_keep[i].name; i++) { if (envflag) {
if (env_keep[i].value) { for (int i = 0; env_keep[i].name; i++) {
err = setenv(env_keep[i].name, env_keep[i].value, 1); if (env_keep[i].value) {
if (err == -1) { err = setenv(env_keep[i].name, env_keep[i].value, 1);
fprintf(stderr, "setenv: %s\n", strerror(errno)); if (err == -1) {
goto fail_end; fprintf(stderr, "setenv: %s\n", strerror(errno));
} goto fail_end;
} }
}
for (int i = 0; env_extra && env_extra[i].name; i++) {
if (env_extra[i].value) {
err = setenv(env_extra[i].name, env_extra[i].value, 1);
if (err == -1) {
fprintf(stderr, "setenv: %s\n", strerror(errno));
goto fail_end;
} }
} else {
unsetenv(env_extra[i].name);
} }
} }
// do not override, we might be under more levels of 'us'
/* Do not override, we might be under more levels of 'us' */ err = setenv("US_USER", my_pw->pw_name, 0);
err = setenv("US_USER", my_name, 0);
errno = 0; errno = 0;
/* Set permissions */ /* Set permissions */
@ -385,19 +201,10 @@ int main(int argc, char *argv[])
goto fail_end; goto fail_end;
} }
if (!(conf_flags & FLAG_NOLOG)) {
char cmd[1024] = {0};
for (int i = 0, x = 0; c_argv[i] && x < 1024; i++)
x += snprintf(cmd, 1024-x, "%s ", c_argv[i]);
openlog("us", LOG_NOWAIT, LOG_AUTH);
syslog(LOG_INFO, "user %s ran %s as %s", my_name, cmd, t_pw->pw_name);
closelog();
}
/* Execute the command */ /* Execute the command */
err = execvp(c_argv[0], c_argv); err = execvp(c_argv[0], c_argv);
if (err == -1) if (err == -1)
fprintf(stderr, "execvp: %s\n", strerror(errno)); fprintf(stderr, "execl: %s\n", strerror(errno));
/* Cleanup and return */ /* Cleanup and return */
fail_end: fail_end:
@ -408,21 +215,20 @@ int main(int argc, char *argv[])
return errno; return errno;
} }
static inline void usage(int complete) static inline void usage (void)
{ {
printf("usage: us [-hseA] [-u user] [-g group] [-C config] command [args]\n"); // TODO: planned options
if (!complete) // -a [program]: like sudo's askpass
return; // -u [user]: change the default user from root to user
printf("-h print this message\n" // -g [group]: change the primary group to [gorup]
"-s use the user's shell instead of /bin/sh\n" // both -a and -g will accept numbers with #[num] like sudo
"-e keep the user's entire environment\n" // -c [file]: manually select config file
"-A use the command in US_ASKPASS as askpass helper\n" // something about environment
"-u user set new user to 'user' instead of root\n" // something about non interactiveness
"-g group set new group to 'group'\n" printf("usage: us [-se] [-u user] [-g group] command [args]\n");
"-C config use specifi config file\n");
} }
static int perm_set(struct passwd *pw, struct group *gr) static int perm_set (struct passwd *pw, struct group *gr)
{ {
if (!pw) { if (!pw) {
errno = EINVAL; errno = EINVAL;
@ -462,394 +268,98 @@ static int perm_set(struct passwd *pw, struct group *gr)
return 0; return 0;
} }
static int authenticate(uid_t uid, int ask, int persist) static int authenticate (const char *uname)
{ {
// TODO: implement u2f compat pam_handle_t *pamh;
// TODO: check root access, maybe int pam_err, count = 0;
/* try to check if a valid saved session exists */
char tmp_file[512] = {0}; do {
if (persist) { pam_err = pam_start("User Switcher", uname, &conv, &pamh);
pid_t sid = getsid(0); if (pam_err != PAM_SUCCESS) {
if (sid == (pid_t)-1) fprintf(stderr, "pam_start: %s\n", pam_strerror(pamh, pam_err));
die("getsid:"); return pam_err;
if (snprintf(tmp_file, 512, SESSION_FILE_DIR "/us.%d", sid) >= 512)
die("snprintf: output truncated");
int session_file = open(tmp_file, O_RDONLY | O_CLOEXEC);
int valid_session = 1;
struct timespec now_t[2] = {0}, old_t[2] = {0};
if (session_file != -1) {
struct stat st;
if (fstat(session_file, &st) == -1) {
valid_session = 0;
} else if (st.st_uid != 0 || st.st_gid != 0) {
valid_session = 0;
} else if (!S_ISREG(st.st_mode)) {
valid_session = 0;
} else if (st.st_mode & S_IRWXO ||
st.st_mode & S_IROTH ||
st.st_mode & S_IWOTH || st.st_mode & S_IXOTH) {
valid_session = 0;
} else {
int r = read(session_file, old_t, sizeof(struct timespec)*2);
if (r != sizeof(struct timespec)*2)
valid_session = 0;
close(session_file);
}
} else {
valid_session = 0;
} }
if (valid_session && session_file != -1) { pam_err = pam_authenticate(pamh, 0);
if (clock_gettime(CLOCK_MONOTONIC, &(now_t[0])) == -1) if (pam_err == PAM_SUCCESS) {
die("clock_gettime:"); pam_err = pam_acct_mgmt(pamh, 0);
if (clock_gettime(CLOCK_REALTIME, &(now_t[1])) == -1)
die("clock_gettime:");
if (now_t[0].tv_sec <= old_t[0].tv_sec ||
old_t[0].tv_sec < now_t[0].tv_sec - SESSION_TIMEOUT ||
now_t[1].tv_sec <= old_t[1].tv_sec ||
old_t[1].tv_sec < now_t[1].tv_sec - SESSION_TIMEOUT)
valid_session = 0;
} }
if (valid_session)
return 0; if (pam_err != PAM_SUCCESS) {
} printf("Auth failed: %s\n", pam_strerror(pamh, pam_err));
/* get the encrypted password */ pam_end(pamh, pam_err);
struct passwd *pw = getpwuid(uid);
char *hash_p, hash[MAX_HASH];
char *p = pw->pw_passwd;
if (!strcmp(p, "x") || *p == '*' || *p == '!') {
#if defined(__linux__)
/* Get exclusive access to the shadow file, then read
* this should prevent any race conditions */
if (lckpwdf() == -1)
die("lckpwdf");
setspent();
struct spwd *sp = getspnam(pw->pw_name);
if (sp == NULL)
die("getspnam");
hash_p = sp->sp_pwdp;
endspent();
ulckpwdf();
#elif defined(__OpenBSD__)
struct passwd *op = getpwuid_shadow(uid);
if (!op)
die("getpwuid_shadow:");
hash_p = op->pw_passwd;
#endif
} else {
hash_p = pw->pw_passwd;
}
strncpy(hash, hash_p, MAX_HASH - 2);
hash[MAX_HASH - 1] = '\0';
if (strlen(hash) >= MAX_HASH - 3)
die("password hash too long :^)");
int fd = STDIN_FILENO;
char *askpass = getenv("US_ASKPASS");
char pass[PASS_MAX] = {0};
struct termios tio_pass;
if (ask && askpass) {
pid_t pid, parent = getpid();
int pipefd[2];
if (pipe(pipefd) == -1)
die("pipe:");
switch (pid = fork()) {
case -1:
die("fork:");
break;
case 0:
/* we are still root, drop off permissions before
* disasters happen, also in case askpass fails
* before sending anything to stdout, terminate tha
* main process since it would hang forever */
if (setuid(uid) == -1) {
kill(parent, SIGTERM);
die("askpass: setuid:");
}
if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
kill(parent, SIGTERM);
die("askpass: dup2:");
}
close(pipefd[0]);
execl("/bin/sh", "sh", "-c", askpass, (char *)NULL);
kill(parent, SIGTERM);
die("askpass: execl:");
break;
default:
fd = pipefd[0];
close(pipefd[1]);
break;
} }
} else {
printf("Password (%s): ", pw->pw_name);
fflush(stdout);
if (tcgetattr(tty_fd, &tio_before) == -1)
die("tcgetattr:");
tio_pass = tio_before;
/* Do not echo and accept when enter is pressed */
tio_pass.c_lflag &= ~ECHO;
tio_pass.c_lflag |= ICANON;
if (tcsetattr(tty_fd, TCSANOW, &tio_pass) == -1)
die("tcsetattr:");
}
int r = read(fd, pass, PASS_MAX-1);
if (!r || r == -1) {
if (errno)
fprintf(stderr, "read: %s\n", strerror(errno));
else
printf("Password can't be zero length\n");
/* read() may have been interrupted, wait askpass even tough it
* should not be necessary before bailing */
waitpid(-1, NULL, 0);
exit(EXIT_FAILURE);
}
pass[PASS_MAX-1] = '\0';
/* Remove the terminating (if there is) \n in password */
int l = strlen(pass);
if (pass[l-1] == '\n')
pass[--l] = '\0';
if (ask && askpass) {
close(fd);
} else {
if (tcsetattr(tty_fd, TCSANOW, &tio_before) == -1)
die("tcsetattr:");
}
char *enc = crypt(pass, hash); count++;
/* Remove password from memory, just to be sure */ } while (pam_err != PAM_SUCCESS && count < 3);
memset(pass, 0, PASS_MAX);
if (strncmp(hash, enc, PASS_MAX)) { if (pam_err != PAM_SUCCESS) {
sleep(FAIL_PAUSE); fprintf(stderr, "better luck next time\n");
printf("Authentication failure\n"); return pam_err;
setuid(uid);
return -1;
}
printf("\n");
if (persist) {
if (!tmp_file[0])
return 0;
int session_file = creat(tmp_file, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (session_file == -1)
return 0;
chown(tmp_file, 0, 0);
struct timespec t[2] = {0};
if (clock_gettime(CLOCK_MONOTONIC, &(t[0])) == -1)
die("clock_gettime:");
if (clock_gettime(CLOCK_REALTIME, &(t[1])) == -1)
die("clock_gettime:");
write(session_file, t, sizeof(struct timespec)*2);
close(session_file);
} }
return 0; // FIXME: check again for the validity of the login for more security
// as in: https://docs.oracle.com/cd/E19120-01/open.solaris/819-2145/pam-20/index.html
// FIXME: ^C [SIGINT] will interrupt this call possibly causing a
// vulnerability
return pam_end(pamh, pam_err);
} }
/* user_to_passwd() and group_to_passwd() both receive the user/group name static struct passwd* user_to_passwd (const char *user)
* in string form, if the sring begins with '#' then the following number
* is interpreted as the uid/gid and used instead of the name.
* Both functions return a pointer to a passwd/group struct stored inside
* the info structure */
static struct passwd* user_to_passwd(const char *user, struct user_info *info)
{ {
if (!user) { if (!user) {
errno = EINVAL; errno = EINVAL;
return NULL; return NULL;
} }
struct passwd *pw; struct passwd* pw;
long uid_l; long uid_l;
errno = 0; errno = 0;
if (user[0] != '#') { if (user[0] != '#') {
getpwnam_r(user, &(info->d.pw), info->str, STR_MAX, &pw); pw = getpwnam(user);
} else { } else {
uid_l = strtol(&user[1], NULL, 10); uid_l = strtol(&user[1], NULL, 10);
if (uid_l < 0 || errno) { if (uid_l < 0 || errno) {
errno = errno ? errno : EINVAL; errno = errno ? errno : EINVAL;
return NULL; return NULL;
} }
getpwuid_r((uid_t)uid_l, &(info->d.pw), info->str, STR_MAX, &pw); pw = getpwuid((uid_t)uid_l);
}
if (!pw) {
if (!errno)
errno = EINVAL;
return NULL;
} }
if (!pw && !errno)
errno = EINVAL;
return pw; return pw;
} }
static struct group* group_to_grp(const char *group, struct user_info *info) static struct group* group_to_grp (const char *group)
{ {
if (!group) { if (!group) {
errno = EINVAL; errno = EINVAL;
return NULL; return NULL;
} }
struct group *gr; struct group* gr;
long gid_l; long gid_l;
errno = 0; errno = 0;
if (group[0] != '#') { if (group[0] != '#') {
getgrnam_r(group, &(info->d.gr), info->str, STR_MAX, &gr); gr = getgrnam(group);
} else { } else {
gid_l = strtol(&group[1], NULL, 10); gid_l = strtol(&group[1], NULL, 10);
if (gid_l < 0 || errno) { if (gid_l < 0 || errno) {
errno = errno ? errno : EINVAL; errno = errno ? errno : EINVAL;
return NULL; return NULL;
} }
getgrgid_r((gid_t)gid_l, &(info->d.gr), info->str, STR_MAX, &gr); gr = getgrgid((gid_t)gid_l);
}
if (!gr && !errno)
errno = EINVAL;
return gr;
}
/* die() emalloc() estrdup() and erealloc() are all helper functions, wrappers
* to respective functions that use die() to give a message and then fail */
void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
if (fmt[0] && fmt[strlen(fmt) - 1] == ':') {
fputc(' ', stderr);
perror(NULL);
} else {
fputc('\n', stderr);
} }
va_end(ap); if (!gr) {
exit(errno ? errno : EXIT_FAILURE); if (!errno)
} errno = EINVAL;
return NULL;
void *emalloc(size_t s)
{
if (!s || s == (size_t)-1)
die("bad malloc: invalid size");
void *p = calloc(1, s);
if (!p)
die("bad malloc:");
return p;
}
void *erealloc(void *p, size_t s)
{
if (!s || s == (size_t)-1)
die("bad realloc: invalid size");
void *r = realloc(p, s);
if (!r)
die("bad realloc:");
return r;
}
char *estrdup(const char *s)
{
if (!s)
die("bad strdup: cannot duplicate NULL pointer");
char *r = strdup(s);
if (!r)
die("bad strdup:");
return r;
}
/* Parses the config file and stores the result in a config vector pointed
* by conf of size num */
static int get_config(struct config **conf, int *num)
{
if (!conf || !num)
return -1;
FILE *fp = fopen(config_file, "r");
if (!fp)
die("fopen:");
struct stat st;
if (fstat(fileno(fp), &st) == -1)
die("fstat:");
if (st.st_uid != 0 || st.st_gid != 0)
die("config file must be owned by root:root");
if (!S_ISREG(st.st_mode))
die("config file must be a regular file");
if (st.st_mode & S_IRWXO || st.st_mode & S_IROTH ||
st.st_mode & S_IWOTH || st.st_mode & S_IXOTH)
die("others may not modify, read or execute config file\n"
"suggested permissions for the config file: 660");
char line[CONF_LINE_MAX];
*num = 0;
*conf = NULL;
for (int i = 0; fgets(line, CONF_LINE_MAX, fp); i++) {
char *s, *t, *sv;
int n = 0;
for (int ll = strlen(line)-1; isspace(line[ll]); line[ll--] = '\0');
struct config c = {0};
for (s = line;; s = NULL, n++) {
int getflags = 0;
if (!(t = strtok_r(s, " \t", &sv)))
break;
if (*t == '#')
break;
switch (n) {
case 0:
if (!strcmp(t, "permit"))
c.type = 1;
else if (!strcmp(t, "deny"))
c.type = 0;
else
die("non valid config line %d", i);
break;
case 1:
c.who = estrdup(t);
break;
case 2:
if (strcmp(t, "as"))
die("non valid config line %d", i);
break;
case 3:
c.as = estrdup(t);
break;
default:
getflags = 1;
break;
}
if (!getflags)
continue;
if (isupper(*t)) {
char *e, *se, *et;
for (e = t;; e = NULL) {
if (!(et = strtok_r(e, ",", &se)))
break;
char *sep;
int x;
if (!(sep = strchr(et, ',')))
die("invalid env at %d", i);
c.env = erealloc(c.env, (c.env_n+1)*sizeof(struct env_elem));
x = c.env_n;
*sep = '\0';
for (char *p = et; *p; p++)
if (!isupper(*p) && *p != '-'
&& *p != '_')
die("non valid"
"env at %d", i);
c.env[x].name = estrdup(et);
c.env[x].value = estrdup(sep+1);
c.env_n++;
}
} else if (!strcmp(t, "persist")) {
c.flags |= FLAG_PERSIST;
} else if (!strcmp(t, "nopass")) {
c.flags |= FLAG_NOPASS;
} else if (!strcmp(t, "nolog")) {
c.flags |= FLAG_NOLOG;
} else {
die("flag %s not recognized at %d", t, i);
}
}
if (n < 3)
die("non valid config line %d", i);
*conf = erealloc(*conf, ((*num)+1)*sizeof(struct config));
(*conf)[(*num)] = c;
*num += 1;
} }
fclose(fp); return gr;
return *num;
} }

@ -1,105 +0,0 @@
.TH US.CONF 5 "JULY 2021" "Alessandro Mauri"
.SH NAME
us.conf \- us configuration file
.SH DESCRIPTION
.PP
The
.BR us(1)
utility executes commands as another identity according to the rules in the
.BR us.conf
configuration file.
.PP
The rules have the following format:
.IP
.BR "permit|deny"
.BR user
as
.BR target
.OP options
.OP ENV
.SS Options
Possible options are:
.IP nopass
The user is not required to enter a password.
.IP persist
Once entering the password for the first time, a timer for five minutes is
started, during those five minutes the user is not required to re-enter
the password for that session. Re-invoking us resets that timer.
.IP nolog
Do not log to
.BR syslog(2)
command outcome
.PP
The sum of matching rules determines the action taken, if no rules match
the action is denied.
.PP
Comments are made by having the first non-blank character of a line be an hash
mark ('#'), comments take up the whole line and cannot be embedded in the
middle of a line.
.PP
A valid user or target is an alphanumeric string containing the name of the
target. If the target is a user, the string begins with [0-9A-z]; if the
target is a group then the has to begin with ':'. Instead of the name of the
user/group it's number can be used, in that case the part of the string that
would contain the name must begin with '#' (so after a possible ':').
.PP
As options a comma separated list of environment variables can be specified,
these will be added or will override existing environment variables during
execution of the command. A valid environment variable list starts with an
uppercase letter and ends at the next space.
.PP
A valid config line must be owned by root:root and should not be readable,
writable or executable for any other user or group, in other words the best
file permissions for the config file are
.BR 660
if the config file fails to meet this requirements it will get rejected and
invocation will fail.
.SH FILES
.IP /etc/us.conf
us(1) configuration file
.SH EXAMPLES
.PP
The following example will allow root to execute commands as itself without
requiring a password and without logging:
.PP
.EX
permit root as root nopass nolog
.EE
.PP
This next example allows users in the wheel group to execute commands as
root including a new environment variable IS_WHEEL set to 'yes' and the variable
EDITOR will be set to ed, the standard unix editor:
.PP
.EX
permit :wheel as root IS_WHEEL=yes,EDITOR=ed
.EE
.PP
In this example the user maria is allowed to execute commands as a member of
the group wheel and the session is remembered so that in the next five
minutes the password won't be needed:
.PP
.EX
permit maria as :wheel persist
.EE
.PP
This time the user joe is denied to execute commands as anyone who's member of
the group 'coolppl' because joe is uncool
.PP
.EX
deny joe as :coolppl
.EE
.SH LIMITATIONS
.PP
Due to the way the environment is parsed, neither the name nor the value can
contain commas.
.SH "SEE ALSO"
.BR us(1)
.SH AUTHOR
Alessandro Mauri <alemauri001@tuta.io>
Loading…
Cancel
Save