├── example_whitelist.conf ├── README.md └── bdsh.py /example_whitelist.conf: -------------------------------------------------------------------------------- 1 | ls 2 | whoami 3 | pwd 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bdsh - Whitelist Restricted Shell 2 | 3 | A shell where you whitelist commands and only those commands can be executed. Either via ssh, as an interactive shell or launched with commands. Logs everything and escapes "dangerous" characters. 4 | 5 | bdsh stands for Boa Diminish SHell. You can probably guess why. It might have to to with snakes and restricting users, no pun intended. 6 | 7 | ## What is the reason you wrote bdsh? 8 | Sometimes sysadmins are forced to work with insecure systems or badly written applications. Critical systems working with a push model instead of a pull model, things that use ssh and break when stuff changes or users that need to be audit trailed. 9 | 10 | I couldn't find an easy way to set a shell for a user that both logs and is configurable via a whitelist. I tried scripts in `auhorized_keys` but then things `scp` and `sftp` break. bdsh tries to solve that and is fairly successfull in that. 11 | 12 | ## Requirements 13 | 14 | - Python 2.6+ 15 | - Python 3 is supported! 16 | 17 | bdsh is tested on Ubuntu, CentOS, OpenSUSE and Debian all with Python 2.7 or above. 18 | 19 | ## Installation 20 | 21 | - Clone the git repo && cd in 22 | - Install bdsh sytemwide 23 | - `sudo cp bdsh.py /usr/bin/bdsh` 24 | - `sudo chmod +x /usr/bin/bdsh` 25 | - Edit and place whitelist 26 | - `vi example_whitelist.conf && sudo cp example_whitelist.conf /etc/bdsh_whitelist.conf` 27 | - Edit `/etc/shells` and add `/usr/bin/bdsh` 28 | - Set the shell for the user, either via: 29 | - `sudo chsh -s /usr/bin/bdsh $USERNAME` 30 | - or 31 | - `vi /etc/passwd` 32 | 33 | ## Tips 34 | 35 | ### Enable SFTP/SCP 36 | 37 | Put these two lines in the whitelist file: 38 | 39 | scp 40 | /usr/lib/openssh/sftp-server 41 | 42 | Note that you might have to change `scp` to `/usr/bin/scp`. 43 | 44 | ## Important 45 | 46 | bdsh only checks if the command is whitelisted, not the arguments. So if you allow `ls`, you also allow `ls -la`, and `ls -d` and such. 47 | 48 | bdsh is not 100% safe, but it does provide a layer of security. 49 | 50 | Read this article about restriced shells: http://pen-testing.sans.org/blog/2012/06/06/escaping-restricted-linux-shells 51 | 52 | Don't change the "Dangerous Characters" array, if you for example remove the `&` then you can do something like this: `ssh user@host "ls && perl -e 'exec "/bin/bash"'"`. 53 | 54 | It does try its best to catch restriction-escaping: 55 | 56 | Jun 29 17:41:07 localhost bdsh: [RESTRICTED SHELL]: user "testshell" executed vim 57 | Jun 29 17:41:11 localhost bdsh: [RESTRICTED SHELL]: user "testshell" NOT allowed for /usr/bin/bdsh -c bash 58 | 59 | ## See Also 60 | 61 | Another way to restrict ssh, written by me: https://github.com/RaymiiOrg/restrict_ssh 62 | 63 | -------------------------------------------------------------------------------- /bdsh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (C) 2013 - Remy van Elst 3 | 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # This script can act as a shell for a user, allowing specific commands only. 18 | # It tries its best to only allow those comamnds and strip possibly dangerous 19 | # things like ; or >. But it won't protect you if you allow the vim command 20 | # and the user executes !bash via vim (and such). It also logs everything to 21 | # syslog for audit trailing purposes. 22 | 23 | # It currently only checks commands, no parameters. This is on purpose. 24 | 25 | import getpass, os, re, sys, syslog, signal, socket, readline 26 | 27 | # format of whitelist: one command or regex per line 28 | command_whitelist = "/etc/bdsh_whitelist.conf" 29 | username = getpass.getuser() 30 | hostname = socket.gethostname() 31 | 32 | def log_command(command, status): 33 | """Log a command to syslog, either successfull or failed. """ 34 | global username 35 | logline_failed = "[RESTRICTED SHELL]: user \"" + username + "\" NOT allowed for " + command 36 | logline_danger = "[RESTRICTED SHELL]: user \"" + username + "\" dangerous characters in " + command 37 | logline_success = "[RESTRICTED SHELL]: user \"" + username + "\" executed " + command 38 | if status == "success": 39 | syslog.syslog(logline_success) 40 | elif status == "failed": 41 | syslog.syslog(logline_failed) 42 | elif status == "danger": 43 | syslog.syslog(logline_danger) 44 | 45 | def dangerous_characters_in_command(command): 46 | # via http://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html 47 | danger = [';', '&', '|', '>', '<', '*', 48 | '?', '`', '$', '(', ')', '{', 49 | '}', '[', ']', '!', '#'] 50 | for dangerous_char in danger: 51 | for command_char in command: 52 | if command_char == dangerous_char: 53 | return True 54 | 55 | def entire_command_scanner(command): 56 | danger = ["&&"] 57 | for dangerous_char in danger: 58 | if re.findall(dangerous_char, command): 59 | return True 60 | 61 | def execute_command(command): 62 | """First log, then execute a command""" 63 | log_command(command, "success") 64 | # try: 65 | # subprocess.call(command, shell=False) 66 | # except OSError: 67 | # pass 68 | os.system(command) 69 | 70 | def command_allowed(command, whitelist_file=command_whitelist): 71 | """Check if a command is allowed on the whitelist.""" 72 | try: 73 | with open(whitelist_file, mode="r") as whitelist: 74 | for line in whitelist: 75 | # We are reading commands from a file, therefore we also read the \n. 76 | if command + "\n" == line: 77 | return True 78 | else: 79 | continue 80 | 81 | except IOError as e: 82 | sys.exit("Error: %s" % e) 83 | 84 | def interactive_shell(): 85 | global username 86 | global hostname 87 | while True: 88 | prompt = username + "@" + hostname + ":" + os.getcwd() + " $ " 89 | try: 90 | if sys.version_info[0] == 2: 91 | command = raw_input(prompt) 92 | else: 93 | command = input(prompt) 94 | # Catch CRTL+D 95 | except EOFError: 96 | print("") 97 | sys.exit() 98 | if command == "exit" or command == "quit": 99 | sys.exit() 100 | elif command: 101 | if not entire_command_scanner(command): 102 | if command_allowed(command.split(" ", 1)[0]): 103 | for chars in command: 104 | if dangerous_characters_in_command(chars): 105 | log_command(command, "danger") 106 | # Don't let the user know via an interactive shell and don't exit 107 | command="" 108 | execute_command(command) 109 | 110 | if __name__ == "__main__": 111 | ## Catch CTRL+C / SIGINT. 112 | s = signal.signal(signal.SIGINT, signal.SIG_IGN) 113 | 114 | arguments = "" 115 | 116 | for args in sys.argv: 117 | if dangerous_characters_in_command(args): 118 | log_command(args, "danger") 119 | sys.exit() 120 | 121 | ## No Arguments? Then we start an interactive shell. 122 | if len(sys.argv) < 2: 123 | interactive_shell() 124 | else: 125 | ## Check if we are not launched via the local shell with a command (./shell.py ls) 126 | if sys.argv[1] and sys.argv[1] != "-c" and command_allowed(sys.argv[1].split(" ", 1)[0]) and not entire_command_scanner(sys.argv[1]): 127 | for arg in sys.argv[1:]: 128 | arguments += arg 129 | arguments += " " 130 | execute_command(arguments) 131 | ## Check if we are launched via the local shell and the command is not allowed 132 | elif len(sys.argv) < 3: 133 | for arg in sys.argv: 134 | arguments += arg 135 | arguments += " " 136 | log_command(arguments, "failed") 137 | elif sys.argv[2] and command_allowed(sys.argv[2].split(" ", 1)[0]) and not entire_command_scanner(sys.argv[2]): 138 | for arg in sys.argv[2:]: 139 | arguments += arg 140 | arguments += " " 141 | execute_command(arguments) 142 | else: 143 | for arg in sys.argv: 144 | arguments += arg 145 | arguments += " " 146 | log_command(arguments, "failed") 147 | # Debug use 148 | # print("\"" + arguments + "\"") 149 | ## Give back the CTRL+C / SIGINT 150 | signal.signal(signal.SIGINT, s) --------------------------------------------------------------------------------