/* bashwrap.c - clean up the environment before bash ever sees it
* specifically, check for shellshock and valid terminal
* also, allow whitelisting of environment variables
*
* hacked together by Pegasus Epsilon <pegasus@pimpninjas.org>
* (C) 2014 Distribute Unmodified
* http://pegasus.pimpninjas.org/license
*
* Audit the code below, modify as you like, and then:
$ cc -Wall -pedantic bashwrap.c -o bashwrap
* Optionally strip unneeded sections. My test machine doesn't need these, but
* still adds them. Removing them all nearly cuts the binary size in half.
$ strip -R .note.ABI-tag -R .note.gnu.build-id -R .gnu.hash -R .gnu.version \
-R .rela.dyn -R .fini -R .eh_frame_hdr -R .eh_frame -R .fini_array -R .jcr \
-R .got -R .data -R .bss -R .comment bashwrap
* And then install as root:
# cp /bin/bash /bin/bash.orig
# cp bashwrap /bin
# ln -sf /bin/bashwrap /bin/bash
* Please let me know if you find anything wrong (bug/exploit/whatever)
*/
#include <stdio.h> /* perror() */
#include <stdlib.h> /* unsetenv() malloc() free() */
#include <unistd.h> /* access() F_OK execve() */
#include <string.h> /* strchr() strcspn() strlen() strcpy() stpncpy()
* strncmp() strstr() -- whew!
*/
/* unsets an environment variable
* takes a pointer to the environment entry (char*) in the environment
* returns void
*/
void unset (char *entp) {
unsigned len;
char *unset;
len = strcspn(entp, "=");
unset = malloc(len + 1);
/* POSIX.1-2008 whee */
*stpncpy(unset, entp, len) = 0;
/*printf("clearing environment variable %s...\n", unset);*/
unsetenv(unset);
free(unset);
}
/* check if terminfo file exists in path
* returns 1 if it exists, 0 if not, sets errno
*/
int termexists (char *path, char *filename) {
unsigned len;
char *filepath;
len = strlen(path);
filepath = strcpy(malloc(len + strlen(filename) + 3), path);
/* insert the first byte of filename (eg "x" for "xterm") into path
* turning "path/" into "path/x/"
*/
filepath[len] = *filename;
filepath[len + 1] = 0;
len = !access(strcat(strcat(filepath, "/"), filename), F_OK);
free(filepath);
return len;
}
/* checks if the given environment entry is in the whitelist
* returns 1 if whitelisted, 0 if not
* expand the whitelist as you see fit, this is all I need.
* don't forget the trailing = on each entry.
* TERM isn't neccessary here, that's covered in the TERM check below.
*/
int whitelisted (char *entp) {
char *whitelist[] = {
"LANG=", "USER=", "LOGNAME=", "HOME=", "PATH=", "MAIL=",
"SHELL=", "TZ=", "SSH_CLIENT=", "SSH_CONNECTION=", "SSH_TTY="
};
int i = sizeof(whitelist) / sizeof(whitelist[0]);
while (i--)
if (!strncmp(whitelist[i], entp, strlen(whitelist[i])))
return 1;
return 0;
}
/* checks if the given environment entry is a shellshock attempt
* returns 0 if it is, nonzero if it isn't.
*/
int clean (char *entp) {
/* this could be a #define
* but we may want to check other things one day.
*/
return !strstr(entp, "=()");
}
int main(int argc, char *argv[], char *envp[]) {
char **e, **r;
/* check environment */
r = e = envp;
do {
if (!strncmp("TERM=", *e, strlen("TERM="))) {
char *term;
term = strchr(*e, '=') + 1;
if (
/* check terminfo paths for valid TERM */
termexists("/usr/share/terminfo/", term) ||
termexists("/lib/terminfo/", term) ||
termexists("/etc/terminfo/", term)
) r = ++e; /* next entry */
else {
/* invalid TERM, unset it */
unset(*e);
e = r; /* start over */
}
/* straight whitelist other things */
} else if (whitelisted(*e) && clean(*e)) {
r = ++e; /* next entry */
} else {
/* not TERM, not whitelisted, or not clean, unset */
unset(*e);
e = r; /* start over */
}
} while (*e);
/*e = envp; while (*e) { printf("ENV: %s\n", *e++); }*/
if (execve("/bin/bash.orig", argv, envp))
perror("failed to start bash");
return -1;
}