7
\$\begingroup\$

I have here a script called rwog (run without groups) that allow a user to run a shell without particular group memberships. rwog, in more detail...

  • Is primarily for a support staffer to pretend to be an ordinary user without su-ing to anybody.
  • Must be run as root, but those allowed to run it will be given an entry in /etc/sudoers.
  • Should be executed as sudo rwog groups to drop.
  • Will have a uid of root, a gid of the_support_team, and permissions 0550 (i.e. user and group can read and execute, world can do nothing, and no setgid/setuid bits).
  • Cannot change your gid, change your uid, or add yourself to supplementary groups.
  • Revokes pretty much all capabilities, though this may be a tad zealous.
  • Does not modify /etc/group (so id will be changed, but id $USER will not).
  • Is primarily (but not exclusively) intended to run on a Centos 7 environment..

My main concern: Can this script be exploited to gain escalated privileges? I won't ignore suggestions not related to security.

The Script

#!/bin/bash

function help(){
    echo "rwog - run without groups";
    echo "Runs a shell as if you weren't in certain supplementary groups."
    echo "Good for pretending that you're not a support user."
    echo "Usage example:";
    echo -e "\trwog [(-h|--help)] [group ...]";
    echo "Options:";
    echo -e "\t-h or --help: Displays this information.";
    exit 1;
}

# Declare vars. Flags initalizing to 0.

# Execute getopt
ARGS=$(getopt -o "h" -l "help" -n "rwog" -- "$@");

#Bad arguments
if [ $? -ne 0 ];
then
    help;
fi

eval set -- "$ARGS";

while true; do
    case "$1" in
        -h|--help)
            shift;
            help;
            ;;

        --)
            shift;
            break;
            ;;
    esac
done


if [[ $(id -u) != 0 ]]; then
    # If you're not root...
    echo "You must be root to use this script. Run it with sudo."
    exit 1
fi

for group in $@; do
    # For each group we want to drop...

    if [[ "$group" == "$SUDO_USER" ]]; then
        # If you're trying to drop your own gid...
        echo "You cannot drop your own gid."
        exit 1
    elif ! getent group "$group" &>/dev/null ; then
        #If this group doesn't exist...
        echo "$group is not a valid group."
        exit 1
    elif  ! groups "$SUDO_USER" | grep -E &>/dev/null "\b$group\b"; then
        # Else if you're not actually a member of this group...
        echo "Not a member of $group, cannot drop it"
        exit 1
    fi
done

my_groups=$(id -Gn "$SUDO_USER" | xargs -n1 | sort -u)
groups_to_drop=$(echo $@ | xargs -n1 | sort -u)
# Put the groups that you want to drop on multiple lines, then sort them

if [[ -z "$groups_to_drop" ]]; then
    # If you didn't pick a group to drop...
    echo "Please specify at least one supplementary group to drop."
    help
fi

reduced_groups=$(comm -13 <(echo "$groups_to_drop") <(echo "$my_groups") | paste -s -d,)
# Subtract the groups we want to drop from the groups we're in, then merge them onto one line

export USERNAME="$SUDO_USER"
export USER="$SUDO_USER"
export LOGNAME="$SUDO_USER"
export HOME=$( getent passwd "$SUDO_USER" | cut -d: -f6 )

dropped_capabilities=$(capsh --print | grep -E "Bounding set =(.+)" | sed "s/Bounding set =//g")

capsh --secbits=0xf --drop="$dropped_capabilities" --groups="$reduced_groups" --gid="$SUDO_GID" --uid="$SUDO_UID" -- --login
\$\endgroup\$
  • \$\begingroup\$ I was surprised to find that this script is necessary, and that sg and newgrp don't already have support for changing supplementary groups. \$\endgroup\$ – Toby Speight Mar 6 '18 at 9:31
8
\$\begingroup\$

It's not an error to ask for help

When -h is passed, the help() function shouldn't exit with non-zero status, because it's done what was asked for.

Conversely, when we pass no arguments, we should exit with non-zero, and we should write the message to the standard error stream.

I'd write that as

help() {
    local tab=$'\t'
    cat <<END
$0 - run without groups
Runs a shell as if you weren't in certain supplementary groups.
Good for pretending that you're not a support user.
Usage example:
${tab}$0 [(-h|--help)] [group ...]
Options:
${tab}-h or --help: Displays this information.
END
}

And then use it as

# Just use the command directly instead of testing $?
if ! ARGS=$(getopt -o "h" -l "help" -n "rwog" -- "$@");
then
    help >&2
    exit 1
fi

and

    case "$1" in
        -h|--help)
            help
            exit 0
            ;;

Elsewhere in the code, diagnostic messages are printed to standard output; these should all be redirected to &2.

Checking more exit statuses

We depend on tools such as xargs and sort that might not be present. Consider letting Bash check some statuses, but make sure you know which commands this will not check:

set -e

Also consider getting Bash to check that you're not using unset variables:

set -u

You'll want to use expansions such as ${foo:-} when you need to avoid this checking.

Quote your variable expansions

Just because you've checked that this is being run by root (perhaps overkill - is it sufficient to be any user that can run capsh?), doesn't mean that they can't do something unexpected. So write "$@", not $@:

for group in "$@"; do

Don't compare a group against a user

if [[ "$group" == "$SUDO_USER" ]]; then

Did you mean "$SUDO_GID" there? Also, there's little point using both [[...]] and quotes - I'd stick to the portable [...] here.

If you did mean to compare against the user name (e.g. you know your configuration always creates a per-user group sharing the same name), then it's a good idea to add a comment documenting that assumption, as it might not be valid on another system you copy this to.

No need to grep and sed in a pipeline

Sed is perfectly capable of selecting lines:

dropped_capabilities=$(capsh --print | sed -n -e 's/Bounding set =//gp')

Take care with $PATH

Even though your site's sudo policy should constrain PATH, it may be a good idea to set a specific PATH at the start of the script and/or use fully-qualified pathnames for executables.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Actually, I did mean to compare $group with $SUDO_USER, because most Linux systems put users in a singleton group that shares their name and ID number. \$\endgroup\$ – JesseTG Mar 5 '18 at 17:08
  • 1
    \$\begingroup\$ In that case, it's probably worth documenting the assumption in a comment - I'll add a note to the review. \$\endgroup\$ – Toby Speight Mar 5 '18 at 17:10
  • \$\begingroup\$ So which of these are potential security risks? $PATH and variable expansions I understand. Anything else? \$\endgroup\$ – JesseTG Mar 5 '18 at 19:31
  • \$\begingroup\$ Failing to check whether commands exist may fall into that category, though I don't see immediately how to exploit it. There's certainly a problem if users are able to modify any directories in the search path ahead of the intended executables. I'd recommend specifying all programs using absolute paths. \$\endgroup\$ – Toby Speight Mar 6 '18 at 9:16
  • 1
    \$\begingroup\$ Only if they can overwrite the program or they can somehow otherwise persuade sudo to run it for them, in which case all bets were off to start with. \$\endgroup\$ – Toby Speight Mar 9 '18 at 16:02
2
\$\begingroup\$

Some minor points on top of Toby's excellent review.

Input validation

This check should be moved higher up in the code:

if [[ -z "$groups_to_drop" ]]; then
    # If you didn't pick a group to drop...
    echo "Please specify at least one supplementary group to drop."
    help
fi

This happens after for group in $@, after my_groups is created, and after groups_to_drop is created from $@. The check on whether $@ is empty should come before all those operations.

A tip: instead of for group in "$@"; do, you could write for group; do to the same effect.

Style

In the first half of the script, many lines end with a redundant ;. Remove those.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.