Compare commits

...

23 Commits

Author SHA1 Message Date
Linux User 9f0238d788 pause for some time on wrong password 3 years ago
Linux User b97be63e1a reset terminal state on termination 3 years ago
Linux User fedeed40ec changed config, + became permit and - deny 3 years ago
Alessandro Mauri 874ee585b7 updated readme 3 years ago
Alessandro Mauri 720f956267 suggest config file permissions 3 years ago
Alessandro Mauri 9c6f7703dc fix typos 3 years ago
Alessandro Mauri e2284c301b added manual pages 3 years ago
Alessandro Mauri 990136a6a7 bug fixes 3 years ago
Alessandro Mauri 37967a87c0 added help message 3 years ago
Alessandro Mauri 475aa341c5 implemented "nolog" option 3 years ago
Alessandro Mauri f489885e3e implemented config persist 3 years ago
Alessandro Mauri 391353cea6 more comment and bug fix 3 years ago
Alessandro Mauri 86a6af5073 less free()s 3 years ago
Alessandro Mauri 3f1bf8c5be I know what to do 3 years ago
Alessandro Mauri e63109a763 improvements and fixes 3 years ago
Alessandro Mauri d6c13b3d39 first working config file 3 years ago
Alessandro Mauri c1050a27f9 added erroring functions 3 years ago
Alessandro Mauri 01c4b00352 only openbsd doesn't use -lcrypt 3 years ago
Alessandro Mauri 1594ec867b fix nasty expired pointer 3 years ago
Alessandro Mauri 2e181319ed make: resolve LDFLAGS based on system 3 years ago
Alessandro Mauri 9494b18325 compiles on openbsd 3 years ago
Alessandro Mauri fb6c872dc7 authenticate using /etc/shadow 3 years ago
Alessandro Mauri 074ebfec6c updated todo 3 years ago
  1. 1
      .gitignore
  2. 18
      README.md
  3. 16
      TODO
  4. 96
      config.template
  5. 23
      makefile
  6. 77
      us.1
  7. 762
      us.c
  8. 105
      us.conf.5

1
.gitignore vendored

@ -0,0 +1 @@
us

@ -3,3 +3,21 @@
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
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

@ -1,16 +0,0 @@
- 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

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

77
us.1

@ -0,0 +1,77 @@
.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>

762
us.c

@ -17,9 +17,16 @@
*/
#define _POSIX_C_SOURCE 200809L
#ifdef __linux__
#define _DEFAULT_SOURCE
#else
#define _BSD_SOURCE
#endif
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -28,34 +35,111 @@
#include <unistd.h>
#include <limits.h>
#include <grp.h>
#include <security/pam_appl.h>
#include <security/pam_misc.h>
static void usage (void);
static int perm_set (struct passwd *, struct group *);
static int authenticate (const char *);
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 *);
// FIXME: misc_conv is a separate library, should stick to plain PAM or make
// our own pam module
static struct pam_conv conv = {misc_conv, NULL};
#include <termios.h>
#include <stdarg.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;
char *config_file = "/etc/us.conf";
int tty_fd = STDOUT_FILENO;
struct termios tio_before = {0};
int main (int argc, char *argv[])
void int_handler(int signum)
{
(void)signum;
if (tio_before.c_iflag || tio_before.c_oflag || tio_before.c_iflag)
tcsetattr(tty_fd, TCSANOW, &tio_before);
putchar('\n');
exit(signum);
}
int main(int argc, char *argv[])
{
char *t_usr = "root", *t_grp = NULL;
struct passwd *t_pw;
struct group *t_gr;
struct user_info t_gr_info = {0}, t_pw_info = {0};
int opt, err;
int shellflag = 0, envflag = 0;
while ((opt = getopt(argc, argv, "A:u:g:C:se")) != -1) {
int shellflag = 0, envflag = 0, askpass = 0;
/* 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) {
case 'A':
printf("-A is not yet implemented\n");
exit(EXIT_FAILURE);
askpass = 1;
break;
case 'u':
t_usr = optarg;
@ -64,8 +148,7 @@ int main (int argc, char *argv[])
t_grp = optarg;
break;
case 'C':
printf("-C is not yet implemented\n");
exit(EXIT_FAILURE);
config_file = optarg;
break;
case 's':
shellflag = 1;
@ -73,36 +156,146 @@ int main (int argc, char *argv[])
case 'e':
envflag = 1;
break;
case 'h':
usage(1);
exit(EXIT_SUCCESS);
break;
case '?':
usage();
usage(0);
exit(EINVAL);
break;
}
}
/* Get user info */
const char *uname;
char *shell;
uid_t ruid = getuid();
struct passwd *my_pw = getpwuid(ruid);
struct passwd *my_pw = NULL;
struct user_info my_info = {0};
getpwuid_r(ruid, &my_info.d.pw, my_info.str, STR_MAX, &my_pw);
if (!my_pw) {
fprintf(stderr, "getpwid: %s\n", strerror(errno));
return errno;
}
uname = my_pw->pw_name;
char *my_name = 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:");
/* Authenticate */
if (authenticate(uname) != PAM_SUCCESS)
exit(EXIT_FAILURE);
/* Get target user and group info */
t_pw = user_to_passwd(t_usr, &t_pw_info);
if (!t_pw)
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;
}
t_gr = group_to_grp(t_grp);
/* We don't need conf anymore */
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 */
if (!shellflag)
@ -114,40 +307,18 @@ int main (int argc, char *argv[])
/* Set argc and argv */
int c_argc = argc - optind;
char **c_argv;
char **c_argv = NULL;
if (c_argc) {
c_argv = malloc(sizeof(char *) * (c_argc + 1));
if (!c_argv) {
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);
}
}
c_argv = emalloc(sizeof(char *) * (c_argc + 1));
for (int i = 0; optind < argc; optind++, i++)
c_argv[i] = estrdup(argv[optind]);
} else {
c_argc = 1;
c_argv = malloc(sizeof(char *) * (c_argc + 1));
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 = emalloc(sizeof(char *) * (c_argc + 1));
c_argv[0] = estrdup(shell);
}
c_argv[c_argc] = NULL;
struct env_elem {
char *name;
char *value;
};
struct env_elem env_keep[] = {
{"PATH", NULL},
{"TERM", NULL},
@ -166,10 +337,12 @@ int main (int argc, char *argv[])
{NULL, NULL}
};
if (envflag) { /* clear env */
/* Copy what has to be copied and then clear the environment, we'll
* make a fresh one later */
if (envflag) {
for (int i = 0; env_keep[i].name; i++)
env_keep[i].value = strdup(getenv(env_keep[i].name));
environ = NULL; // in place of clearenv
env_keep[i].value = estrdup(getenv(env_keep[i].name));
environ = NULL;
}
for (int i = 0; env_mod[i].name; i++) {
@ -177,22 +350,33 @@ int main (int argc, char *argv[])
if (err == -1) {
fprintf(stderr, "setenv: %s\n", strerror(errno));
goto fail_end;
}
}
}
if (envflag) {
for (int i = 0; env_keep[i].name; i++) {
if (env_keep[i].value) {
err = setenv(env_keep[i].name, env_keep[i].value, 1);
if (err == -1) {
fprintf(stderr, "setenv: %s\n", strerror(errno));
goto fail_end;
}
}
for (int i = 0; envflag &&env_keep[i].name; i++) {
if (env_keep[i].value) {
err = setenv(env_keep[i].name, env_keep[i].value, 1);
if (err == -1) {
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'
err = setenv("US_USER", my_pw->pw_name, 0);
/* Do not override, we might be under more levels of 'us' */
err = setenv("US_USER", my_name, 0);
errno = 0;
/* Set permissions */
@ -201,10 +385,19 @@ int main (int argc, char *argv[])
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 */
err = execvp(c_argv[0], c_argv);
if (err == -1)
fprintf(stderr, "execl: %s\n", strerror(errno));
fprintf(stderr, "execvp: %s\n", strerror(errno));
/* Cleanup and return */
fail_end:
@ -215,20 +408,21 @@ int main (int argc, char *argv[])
return errno;
}
static inline void usage (void)
static inline void usage(int complete)
{
// TODO: planned options
// -a [program]: like sudo's askpass
// -u [user]: change the default user from root to user
// -g [group]: change the primary group to [gorup]
// both -a and -g will accept numbers with #[num] like sudo
// -c [file]: manually select config file
// something about environment
// something about non interactiveness
printf("usage: us [-se] [-u user] [-g group] command [args]\n");
printf("usage: us [-hseA] [-u user] [-g group] [-C config] command [args]\n");
if (!complete)
return;
printf("-h print this message\n"
"-s use the user's shell instead of /bin/sh\n"
"-e keep the user's entire environment\n"
"-A use the command in US_ASKPASS as askpass helper\n"
"-u user set new user to 'user' instead of root\n"
"-g group set new group to 'group'\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) {
errno = EINVAL;
@ -259,7 +453,7 @@ static int perm_set (struct passwd *pw, struct group *gr)
printf("setregid failed\n");
return -1;
}
if (setreuid(uid, uid) == -1) {
printf("setreuid failed\n");
return -1;
@ -268,98 +462,394 @@ static int perm_set (struct passwd *pw, struct group *gr)
return 0;
}
static int authenticate (const char *uname)
static int authenticate(uid_t uid, int ask, int persist)
{
pam_handle_t *pamh;
int pam_err, count = 0;
do {
pam_err = pam_start("User Switcher", uname, &conv, &pamh);
if (pam_err != PAM_SUCCESS) {
fprintf(stderr, "pam_start: %s\n", pam_strerror(pamh, pam_err));
return pam_err;
// TODO: implement u2f compat
// TODO: check root access, maybe
/* try to check if a valid saved session exists */
char tmp_file[512] = {0};
if (persist) {
pid_t sid = getsid(0);
if (sid == (pid_t)-1)
die("getsid:");
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;
}
pam_err = pam_authenticate(pamh, 0);
if (pam_err == PAM_SUCCESS) {
pam_err = pam_acct_mgmt(pamh, 0);
if (valid_session && session_file != -1) {
if (clock_gettime(CLOCK_MONOTONIC, &(now_t[0])) == -1)
die("clock_gettime:");
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 (pam_err != PAM_SUCCESS) {
printf("Auth failed: %s\n", pam_strerror(pamh, pam_err));
pam_end(pamh, pam_err);
if (valid_session)
return 0;
}
/* get the encrypted password */
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;
}
count++;
} while (pam_err != PAM_SUCCESS && count < 3);
if (pam_err != PAM_SUCCESS) {
fprintf(stderr, "better luck next time\n");
return pam_err;
} 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:");
}
// 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);
char *enc = crypt(pass, hash);
/* Remove password from memory, just to be sure */
memset(pass, 0, PASS_MAX);
if (strncmp(hash, enc, PASS_MAX)) {
sleep(FAIL_PAUSE);
printf("Authentication failure\n");
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;
}
static struct passwd* user_to_passwd (const char *user)
/* user_to_passwd() and group_to_passwd() both receive the user/group name
* 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) {
errno = EINVAL;
return NULL;
}
struct passwd* pw;
struct passwd *pw;
long uid_l;
errno = 0;
if (user[0] != '#') {
pw = getpwnam(user);
getpwnam_r(user, &(info->d.pw), info->str, STR_MAX, &pw);
} else {
uid_l = strtol(&user[1], NULL, 10);
if (uid_l < 0 || errno) {
errno = errno ? errno : EINVAL;
return NULL;
}
pw = getpwuid((uid_t)uid_l);
}
if (!pw) {
if (!errno)
errno = EINVAL;
return NULL;
getpwuid_r((uid_t)uid_l, &(info->d.pw), info->str, STR_MAX, &pw);
}
if (!pw && !errno)
errno = EINVAL;
return pw;
}
static struct group* group_to_grp (const char *group)
static struct group* group_to_grp(const char *group, struct user_info *info)
{
if (!group) {
errno = EINVAL;
return NULL;
}
struct group* gr;
struct group *gr;
long gid_l;
errno = 0;
if (group[0] != '#') {
gr = getgrnam(group);
getgrnam_r(group, &(info->d.gr), info->str, STR_MAX, &gr);
} else {
gid_l = strtol(&group[1], NULL, 10);
if (gid_l < 0 || errno) {
errno = errno ? errno : EINVAL;
return NULL;
}
gr = getgrgid((gid_t)gid_l);
getgrgid_r((gid_t)gid_l, &(info->d.gr), info->str, STR_MAX, &gr);
}
if (!gr) {
if (!errno)
errno = EINVAL;
return NULL;
}
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);
exit(errno ? errno : EXIT_FAILURE);
}
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 *num;
}

@ -0,0 +1,105 @@
.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