loading...

Dear Diary: Recording bash sessions as Github Gists

lbonanomi profile image lbonanomi ・5 min read

Does your employer use shared Linux accounts for managing aspects of the infrastructure or a particular product? I know many of my employers have. Does your employer discourage or outright prevent running sudo su - $SHARED_ACCOUNT? I know my employers never have.

Let's try and get a little accountability into this mosh pit.

We need functionality to:

  • Identify users accessing a shared account via sudo su or sudo bash.
  • Capture their shell session output.
  • Use a tamper-resistant store for their session output.

First, a few words about security

This project is intended to be an organizational tool for a cooperative team, not a security auditing system.

This article makes extensive use of authentication with ~/.netrc files. Your sysadmin/the corporate security crew/you may object to ~/.netrc files as potentially unsafe. Please at least use a dedicated Github token who's only scope is 'gist' and please look-into GPG encrypted .netrc files.

Finding su-ed users

Tell me who you used to be...
Slayer, Spirit in Black

Our first steps are determining if a user has sudo-ed into the current shell and what their original username was if they did. The easiest way to determine if a user is sudo-ed into a shell is to compare their Effective and Real UIDs, knowing that sudo only modifies the caller's effective UID value.

A standalone script could compare the results of getuid() and the ownership of /dev/stdin to determine if the real UID and effective UID matched, but we will stick to coreutils for the sake of portability.

whoami will reliably return the name associated with the effective UID.
who -m will parse login accounting to get the name of the user who authenticated to login(1).

So our simple test for a su-ed user can be:

[[ $(who -m | awk '{ print $1 }') == $(whoami) ]] || echo "You are su-ed to $(whoami)"

Capturing sessions

The obvious choice for recording user sessions is the script command, which records the content of a user's screen to a file. We'll use the '-q' flag to cut on-screen noise.

The obvious problem with capturing and publishing shell sessions is that this will publish command histories that use inline credentials. Even if you tweak the functions here for private storage it seems like an abuse of trust to record credentials in a shared space, so let's run a (very naive) sterilization.

function sterilizer() {
    cat $1 | strings | while read line
        do
            RAW="$line"
            echo "$RAW" | egrep -q "curl\b+.*(-u|--user).*:.*\b*" &&                RAW=$(echo "$RAW" | awk -F"http" '{ print "STERILIZED: curl http"$NF }')
            echo "$RAW" | egrep -q "curl.*(-H|--header).*(token|auth.*)\b+.*" &&    RAW=$(echo "$RAW" |awk -F"http" '{ print "STERILIZED: curl http"$NF }')
            echo "$RAW" | egrep -q "wget\b+.*--.*password\b+.*\b*" &&               RAW=$(echo "$RAW" |awk -F"http" '{ print "STERILIZED: wget http"$NF }')
            echo "$RAW" | egrep -q "https://(\S+?):\S+?@" &&                        RAW=$(echo "$RAW" |awk -F"@" '{ print "STERILIZED CALL TO: https://"$NF }')
            echo "$RAW"
        done > $1.dirty && mv $1.dirty $1
}

Nothing fancy (or comprehensive!), just a check for common idioms I've found in my own command history that leaked credentials. The pokey speed of this function will be forgiven because it will need to run only once for every session capture.

Storing sessions

To reduce overhead Github gists seems like the best way to store individual sessions:

  • They requires no specialist tooling.
  • They are discrete (every session by every user can be stored separately).
  • All changes are logged and timestamped.
  • Github.com is way more-available than anything I could run myself.

Gist files will need a little help from curl to get instanced...

$ curl -nsd '{"files":{"Filename":{"content":"Some content"}}}' https://api.github.com/gists 

but once they are created gists can be addressed like any other git repository.

$ git clone https://gist.github.com/dcf43b8ebb48d41090f3221f6bb4510d.git
Cloning into 'dcf43b8ebb48d41090f3221f6bb4510d'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.

Putting it all together:

Now we have a simple test to confirm that a user is su-ed into a shell, a command to record the current user's session and tools for creating and updating gist files on Github.com. Let's assemble these pieces into an addition to a shared account's ~/.bash_profile.

#/bin/bash

function sterilizer() {
    cat $1 | strings | while read line
        do
            RAW="$line"
            echo "$RAW" | egrep -q "curl\b+.*(-u|--user).*:.*\b*" &&                RAW=$(echo "$RAW" | awk -F"http" '{ print "STERILIZED: curl http"$NF }')
            echo "$RAW" | egrep -q "curl.*(-H|--header).*(token|auth.*)\b+.*" &&    RAW=$(echo "$RAW" |awk -F"http" '{ print "STERILIZED: curl http"$NF }')
            echo "$RAW" | egrep -q "wget\b+.*--.*password\b+.*\b*" &&               RAW=$(echo "$RAW" |awk -F"http" '{ print "STERILIZED: wget http"$NF }')
            echo "$RAW" | egrep -q "https://(\S+?):\S+?@" &&                        RAW=$(echo "$RAW" |awk -F"@" '{ print "STERILIZED CALL TO: https://"$NF }')
            echo "$RAW"
        done > $1.dirty && mv $1.dirty $1
}

function script_to_github() {
    SCRIPTFILE_NAME="$1 su-ed to $2 on $(hostname)"

    # API token sanity-checks
    #
    curl -Insw "%{http_code}" https://api.github.com/user | tail -1 | grep -q 200 || exit

    # Instance a gist and clone it
    #
    GISTID=$(curl -nsd '{"files":{"sfile":{"content":"scriptfile in-flight"}}}' https://api.github.com/gists | awk -F"\"" '$2 == "id" {print $4}' | head -1);
    git clone -q "https://gist.github.com/"$GISTID".git" ~/$GISTID &>/dev/null;

    # API and gist can use the same token but need different addresses
    # confirm pushing-back a complete gist
    #
    if (cd ~/$GISTID && git push -q &>/dev/null || exit)
    then
        script -qf ~/$GISTID/sfile && (cd ~/$GISTID && sterilizer sfile && git add * && git commit -qm "." && git push -q origin 2>/dev/null && rm -rf ~/$GISTID/.git ~/$GISTID/sfile && rmdir ~/$GISTID);
        curl -nks -X PATCH -d '{"files":{"sfile":{"filename":"'$SCRIPTFILE_NAME'"}}}' https://api.github.com/gists/$GISTID >/dev/null
        kill -1 $PPID
    else
        # Can't push back the gist. Clean-up and don't bother the user.
        rm -rf ~/$GISTID/.git ~/$GISTID/sfile && rmdir ~/$GISTID
    fi
}

# who -m shows the real user, whoami shows the effective user.
# If these don't match, its most-likely the user is `sudo su`-ed.
#
# This chunk should kick-off session recording as required:

[[ $(who -m | awk '{ print $1 }') == $(whoami) ]] || script_to_github $(who -m | awk '{ print $1 }') $(whoami)

And now we should have an unobtrusive record of who did what with a shared Linux account.

Posted on by:

lbonanomi profile

lbonanomi

@lbonanomi

Internet loudmouth since 1996

Discussion

markdown guide
 

As an afterthought: this is also a handy note-taking system for a single user, especially for reviewing how large software products got installed.