24

There's a similar question that deals with the 'wrapping' scenario, where you want to replace for example cd with a command that calls the builtin cd.

However, in light of shellshock et al and knowing that bash imports functions from the environment, I've done a few tests and I can't find a way to safely call the builtin cd from my script.

Consider this

cd() { echo "muahaha"; }
export -f cd

Any scripts called in this environment using cd will break (consider the effects of something like cd dir && rm -rf .).

There are commands to check the type of a command (conveniently called type) and commands for executing the builtin version rather than a function (builtin and command). But, lo and behold, these can be overridden using functions as well

builtin() { "$@"; }
command() { "$@"; }
type() { echo "$1 is a shell builtin"; }

Will yield the following:

$ type cd
cd is a shell builtin
$ cd x
muahaha
$ builtin cd x
muahaha
$ command cd x
muahaha

Is there any way to safely force bash to use the builtin command, or at least detect that a command isn't a builtin, without clearing the entire environment?

I realize if someone controls your environment you're probably screwed anyway, but at least for aliases you've got the option to not call the alias by inserting a \ before it.

falstro
  • 433
  • you can run your script using env command before, like this: env -i <SCRIPT.sh> – Arkadiusz Drabczyk Mar 05 '15 at 12:58
  • Only if env isn't redefined as a function as well. This is terrifying. I first thought that special characters would help -- calling with full path that includes /, using . to source, and so on. But those can also be used for function names! You can redefine any function you want, but it's hard to get back to calling the original command. – orion Mar 05 '15 at 13:03
  • 1
    True, but if you're going to run any script on a compromised machine you're screwed anyway. Alternatively, write your script in #/bin/sh if this is not default interactive shell. – Arkadiusz Drabczyk Mar 05 '15 at 13:07

3 Answers3

20

Olivier D is almost correct, but you must set POSIXLY_CORRECT before running unset. POSIX has a notion of Special Built-ins, and bash supports this. unset is one such builtin. Search for SPECIAL_BUILTIN in builtins/*.c in the bash source for a list, it includes set, unset, export, eval and source.

$ unset() { echo muahaha-unset; }
$ unset unset
muahaha-unset
$ POSIXLY_CORRECT=1
$ unset unset

The rogue unset has now been removed from the environment, if you unset command, type, builtin then you should be able to proceed, but unset POSIXLY_CORRECT if you are relying on non-POSIX behaviour or advanced bash features later.

This does not address aliases though, so you must use \unset (or use some form of quoting/escaping on that word) to be sure it works in interactive shell (or always, in case expand_aliases is in effect).

For the paranoid, this should fix everything, I think:

POSIXLY_CORRECT=1
\unset -f help read unset
\unset POSIXLY_CORRECT
re='^([a-z:.\[]+):' # =~ is troublesome to escape
while \read cmd; do 
    [[ "$cmd" =~ $re ]] && \unset -f ${BASH_REMATCH[1]}; 
done < <( \help -s "*" )

(while, do, done and [[ are reserved words and don't need precautions. You can't use = in an alias or function name, so no precautions needed for setting POSIXLY_CORRECT.) Note we are using unset -f to be sure to unset functions, although variables and functions share the same namespace it's possible for both to exist simultaneously (thanks to Etan Reisner) in which case unset-ing twice would also do the trick. You can mark a function readonly, bash does not prevent you unsetting a readonly function up to and including bash-4.2, bash-4.3 does prevent you but it still honours the special builtins when POSIXLY_CORRECT is set.

A readonly POSIXLY_CORRECT is not a real problem, this is not a boolean or flag its presence enables POSIX mode, so if it exists as a readonly you can rely on POSIX features, even if the value is empty or 0. You'll simply need to unset problematic functions a different way than above, perhaps with some cut-and-paste:

\help -s "*" | while IFS=": " read cmd junk; do echo \\unset -f $cmd; done

(and ignore any errors) or engage in some other scriptobatics.


Other notes:

  • function is a reserved word, it can be aliased but not overridden with a function. (Aliasing function is mildly troublesome because \function is not acceptable as a way of bypassing it)
  • [[, ]] are reserved words, they can be aliased (which will be ignored) but not overridden with a function (though functions can be so named)
  • (( is not a valid name for a function, nor an alias

Thanks to commenters below for the extra suggestions.

mr.spuratic
  • 9,901
  • Interesting, thanks! I seems weird to me that bash would default to not protecting unset et al from being overwritten. – falstro Mar 05 '15 at 13:40
  • This needs the -f argument to unset doesn't it? Otherwise if both a variable and function named the same as a builtin are defined then unset will pick the variable to unset first. Also this fails if any of the obscuring functions is set readonly doesn't it? (Though that's detectable and can be made a fatal error.) Also this misses the [ builtin it seems. – Etan Reisner Dec 03 '15 at 18:57
  • So no, looks like readonly isn't a problem... once this uses \unset -f at least. – Etan Reisner Dec 03 '15 at 19:13
  • 1
    I don't think \unset -f unset makes sense, given it will be done in the read loop. If unset is a function your \unset would normally resolve to it instead of the builtin, but you can use \unset safely so long as POSIX mode is in effect. Explicitly calling \unset -f on [, ., and : might be a good idea though, as your regex excludes them. I would also add -f to the \unset in the loop, and would \unset POSIXLY_CORRECT after the loop, instead of before. \unalias -a (after \unset -f unalias) also allows one to safely forego the escape on subsequent commands. – Adrian Günter Apr 26 '17 at 04:20
  • Also, if you wish to securely test for POSIX mode before proceeding and trusting set, unset, or other builtins/commands, while [[ ! "${POSIXLY_CORRECT+X}" ]]; do >/dev/null; done works for me in bash-4.4. While far from ideal due to the freewheeling loop, I couldn't find a way to reliably exit using only reserved words and other non-command constructs. That said, it's preferable to blindly continuing interpretation IMO, as you can't trust \unset if POSIXLY_CORRECT has been unset and made readonly, causing POSIXLY_CORRECT= assignment to fail. – Adrian Günter Apr 26 '17 at 04:35
  • POSIXLY_CORRECT is unset before the loop because I'm lazy and the loop uses non-POSIX features, I'm sure a (more verbose) POSIX compatible alternative would work too. The seemingly superfluous unset unset for the same reason. I have amended the regex to include . : and [ too, thanks. I don't have a failsafe exit that would work either... – mr.spuratic May 03 '17 at 17:45
  • 1
    @AdrianGünter Try: [[ ${POSIXLY_CORRECT?non-POSIX mode shell is untrusted}x ]], this will cause a non-interactive script to exit with the error indicated if the variable is unset, or continue if it is set (empty, or any value). – mr.spuratic Jul 26 '18 at 09:31
  • 1
    "you must use \unset" ... It should be noted that quoting of any character(s) would work to avoid the alias expansion, not just the first. un"se"t would work just as well, abeit less readable. "The first word of each simple command, if unquoted, is checked to see if it has an alias." –Bash Ref – Robin A. Meade Jan 31 '19 at 18:55
7

I realize if someone controls your environment you're probably screwed anyway

Yes, that. If you run a script in an unknown environment, all manner of things can go wrong, starting with LD_PRELOAD causing the shell process to execute arbitrary code before it even reads your script. Attempting to protect against a hostile environment from inside the script is futile.

Sudo has been sanitizing the environment by removing anything that looks like a bash function definition for over a decade. Since Shellshock, other environments that run shell script in a not-fully-trusted environment have followed suit.

You cannot safely run a script in an environment that has been set by an untrusted entity. So worrying about function definitions is not productive. Sanitize your environment, and in doing so variables that bash would interpret as function definitions.

  • 1
    I disagree with this entirely. Security is not a binary proposition centered around "sanitized" or "unsanitized". It's about removing opportunities for exploitation. And unfortunately, the complexity of modern systems means there are many exploitation opportunities that need to be locked down properly. Many exploitation opportunities center around innocuous-seeming upstream attacks, such as changing the PATH variable, redefining built-in functions, etc. Offer a single person or program the opportunity to do either, and your entire system is vulnerable. – Dejay Clayton Jun 18 '15 at 15:07
  • @DejayClayton I fully agree with your comment, except for the first sentence, because I don't see how it contradicts my answer in any way. Removing an exploitation opportunity helps, but not removing only half of it where a trivial tweak in the attack allows it to keep working. – Gilles 'SO- stop being evil' Jun 18 '15 at 15:31
  • My point is that you can't just "sanitize your environment" and then stop worrying about various attack vectors. Sometimes it pays to "protect against a hostile environment from inside the script." Even if you solve the "function definition" problem, you still have to worry about things such as having a PATH where someone could put a custom script named "cat" or "ls" into a directory, and then any script that invokes "cat" or "ls" instead of "/bin/cat" and "/bin/ls" is effectively executing an exploit. – Dejay Clayton Jun 18 '15 at 21:40
  • @DejayClayton Obviously sanitizing the environment doesn't help with attack vectors that are unrelated to the environment. (For example a script that calls /bin/cat in a chroot could be calling anything.) Sanitizing the environment does give you a sane PATH (assuming you're doing it right of course). I'm still not seeing your point. – Gilles 'SO- stop being evil' Jun 18 '15 at 21:42
  • Considering that PATH is one of the biggest opportunities for exploits, and that many questionable PATH practices are documented as recommended advice even in security blogs, and also considering that sometimes installed applications reorder PATH to ensure that such apps work, I can't see how defensive coding within a script would be a bad idea. What you consider a sane PATH may in fact be vulnerable to exploits you haven't encountered. – Dejay Clayton Jun 18 '15 at 21:54
1

You could use the command unset -f to remove the functions builtin, command, and type.

odc
  • 313
  • 1
  • 5