Thread: a utility for preventing exec() calls
Brought to you by:
xystrus
From: Nick C. <ni...@cl...> - 2019-02-07 14:05:24
|
I made this, it might be interesting to anyone looking to implement something like rssh, or to un-retire rssh itself: it allows you to execute a program but trap any calls to libc exec* syscall wrappers that the program might make: https://github.com/ncleaton/libcallfilt It provides a second line of defense if you've tried to block all of the options that could exec arbitrary things but you may have missed something. |
From: Russ A. <ea...@ey...> - 2019-02-07 19:19:37
|
Nick Cleaton <ni...@cl...> writes: > I made this, it might be interesting to anyone looking to implement > something like rssh, or to un-retire rssh itself: it allows you to > execute a program but trap any calls to libc exec* syscall wrappers that > the program might make: > https://github.com/ncleaton/libcallfilt > It provides a second line of defense if you've tried to block all of the > options that could exec arbitrary things but you may have missed > something. Thank you -- that's an interesting option! BTW, you probably want to add posix_spawn, posix_spawnp, and execveat. (Although a nice property of this approach is that it doesn't rely on finding every possible system call, only covering the ones that the legitimate program you're trying to spawn might use.) -- Russ Allbery (ea...@ey...) <http://www.eyrie.org/~eagle/> |
From: Nick C. <ni...@cl...> - 2019-02-08 08:29:12
|
On Thu, 7 Feb 2019 at 19:19, Russ Allbery <ea...@ey...> wrote: > > BTW, you probably want to add posix_spawn, posix_spawnp, and execveat. > Thanks, I'll add those. |
From: Derek M. <co...@pi...> - 2019-02-08 01:38:33
|
Hey... On Thu, Feb 07, 2019 at 11:19:26AM -0800, Russ Allbery wrote: > Nick Cleaton <ni...@cl...> writes: > > https://github.com/ncleaton/libcallfilt > > > It provides a second line of defense if you've tried to block all of the > > options that could exec arbitrary things but you may have missed > > something. > > Thank you -- that's an interesting option! > > BTW, you probably want to add posix_spawn, posix_spawnp, and execveat. > (Although a nice property of this approach is that it doesn't rely on > finding every possible system call, only covering the ones that the > legitimate program you're trying to spawn might use.) ...assuming the program is legitimate, and hasn't been replaced somehow by a more nefarious version... This seems potentially useful, but I'm concerned this is just another example of something that seems like a good idea in principle (like rssh), but perhaps depending on the exact use cases, is hard to do in practice, and Russ hints at some of the why. If someone is serious about bypassing your access restrictions, they're not going to stick to doing what a well-behaved program might do. To get this right, in the general case, you need to know all of the possible ways to execute a program from system code on all of your target platforms. And you probably don't. And there are probably platform-dependent ways that it could be done using inline assembly that would be hard or impossible for you to block even if you did... As a quick example, in Linux most system calls can be called indirectly via the syscall() system call, which AFAICT would bypass your filter. I'm not sure it's practical to block syscall() as glibc probably uses it directly, and while it would be uncommon, some system utilities you might want to use may use it also (I personally have written code that calls it). There's probably something equivalent on some other platforms, and it's probably called something other than syscall()... You also have to think very hard about what is being executed, and whether it provides any means for an attacker to upload/execute arbitrary program code. Having been through that, I can tell you there are some pretty obscure features of SSH, and of the underlying OS and its utilities, that make that task very difficult. And new ones get added over time. So no matter how simple the task might seem when you start, it's not, and it's never finished. As for your code... [Some of this will be nit-picky, some will definitely not be. I think being nit-picky in security contexts is REQUIRED.] At a quick glance, I note that you use EACCES, but FWIW I think the more appropriate errno here would be EPERM. EACCES is usually used for things like "you don't have appropriate ACL bits to do that" whereas EPERM is usually more like "Letting you do that could be dangerous, so I'm not gonna." I suppose it's a judgement call, and TBH we probably didn't need both to begin with. Also I note you don't check the return value of setenv(), which could result in complete bypass of the filter. You also do not check the return value from your snprintf() calls. It doesn't look exploitable, but if LD_PRELOAD was already set to something long, and you actually needed that, it would likely break. That'd be rare and unlikely to be an issue for most installations, but nevertheless it's an unnecessary limitation caused by a bug. And you don't cast the return value of malloc(). Not in and of itself a problem, but you should always compile security-sensitive programs with -Wall -Werror, so you don't miss anything stupid, and if you do that I believe this will fail to compile. I think the better approach would be: ... /* don't do expensive malloc unless we need to. Usually we don't need to. */ char buf[48]; // Use a macro... char *mem = buf; int size = snprintf(buf, 48, ...); // Use the macro, Luke! if (size > 48){ // Use the macro, Luke! mem = (char *)malloc(++size); int s = snprintf(mem, size, ...); /* impossible, but sometimes impossible things happen! */ if (s > size) { explode(); } } if (setenv(var, mem ...) == -1) explode(); // either don't bother with free(), or set some flag so you know you // need to. Since you're about to execvp(), it's superfluous. ... Obviously for brevity, I left out a few of the details you already included. Some bits of the above (e.g. ++size) might feel more clever than clear; if so rewrite accordingly. Hopefully it's clear that explode() is shorthand for "something went wrong; don't continue to do what the user asked for..." One last point: I generally recommend(ed) that people use rssh in a chroot, statically linked, unless they had a good reason not to. One reason for static linking in security-sensitive applications is to prevent LD_PRELOAD and similar from being an attack vector, but making use of that fact also eliminates the potential usefulness of a filter like this entirely. To that point: If your attackers had any sort of ability to control what was being executed--intentionally or not--they could simply execute a statically-linked version that bypasses your filter. Security is hard... :( -- Derek D. Martin http://www.pizzashack.org/ GPG Key ID: 0x81CFE75D |
From: Russ A. <ea...@ey...> - 2019-02-08 02:37:24
|
Derek Martin <co...@pi...> writes: > This seems potentially useful, but I'm concerned this is just another > example of something that seems like a good idea in principle (like > rssh), but perhaps depending on the exact use cases, is hard to do in > practice, and Russ hints at some of the why. If someone is serious > about bypassing your access restrictions, they're not going to stick to > doing what a well-behaved program might do. To get this right, in the > general case, you need to know all of the possible ways to execute a > program from system code on all of your target platforms. And you > probably don't. And there are probably platform-dependent ways that it > could be done using inline assembly that would be hard or impossible for > you to block even if you did... It may also be worth noting that this wouldn't have blocked the recent security vulnerability around SSH config handling, where scp on the server side could be tricked into reading an uploaded SSH config file that specifies an uploaded shared object to dynamically load and execute as a PKCS#11 provider. :( Even my idea presented earlier on the list of forcing a specific rsync command line on the server side (which could then be implemented with SSH force-command) *still* is exploitable if the home directory of the user is writable because of rsync popt support loading ~/.popt. One can block exec from there with something like this wrapper as well, but it still allows aliasing command line options and thus changing the effective rsync command being executed to some other command with different options, such as --daemon. It's harder to get to code execution with exec blocked, but spawning a daemon on the server listening on the rsync port is at least not great and may well have some other exploit path. sftp is probably closer to the "correct" solution to this problem. The Subsystem facility that it uses avoids passing things through shells, and it can be used with ChrootDirectory to enforce a file namespace. And it won't have "features" like loading popt configuration files. -- Russ Allbery (ea...@ey...) <http://www.eyrie.org/~eagle/> |
From: Nick C. <ni...@cl...> - 2019-02-08 09:22:38
|
On Fri, 8 Feb 2019 at 02:37, Russ Allbery <ea...@ey...> wrote: > Derek Martin <co...@pi...> writes: > > It may also be worth noting that this wouldn't have blocked the recent > security vulnerability around SSH config handling, where scp on the server > side could be tricked into reading an uploaded SSH config file that > specifies an uploaded shared object to dynamically load and execute as a > PKCS#11 provider. :( > Yes and no. If you were intentionally allowing scp in client mode to make outgoing connections from your server then you couldn't use libcallfilt with it in its current form anyway, as you'd have to allow scp to exec ssh to function. However if you only wanted to allow scp so that people can scp things to and from your server then libcallfilt would prevent it from exec-ing ssh even if you failed at restricting the options to enforce server mode. Even my idea presented earlier on the list of forcing a specific rsync > command line on the server side (which could then be implemented with SSH > force-command) *still* is exploitable if the home directory of the user is > writable because of rsync popt support loading ~/.popt. Writable $HOME seems pretty risky in general for command restriction, I suspect the ~/.popt thing is just one of many. sftp is probably closer to the "correct" solution to this problem. The > Subsystem facility that it uses avoids passing things through shells, and > it can be used with ChrootDirectory to enforce a file namespace. And it > won't have "features" like loading popt configuration files. > A restricted view of the filesystem is certainly good, if not using a command built into ssh it would make sense to nsjail/firejail/bubblewrap the executed command to get seccomp filters and filesystem limiting. If you really want the rsync protocol then a forced command of "${things such as nsjail and libcallfilt go here} rsync --server --daemon --config /etc/some-rsyncd.conf ." is probably about as solid as you can get it: rsync in daemon mode is designed to interact with an untrusted user, and you get to set which parts of the filesystem are readable and writable in /etc/some-rsyncd.conf. You do have to adapt the rsync client command though, to work in terms of modules defined in your rsyncd.conf rather than file paths: rsync -av -e ssh /my/thing us...@rs...::backups/ |
From: Nick C. <ni...@cl...> - 2019-02-08 09:42:57
|
On Fri, 8 Feb 2019 at 01:37, Derek Martin <co...@pi...> wrote: > > [...], you need to know all of the possible ways to execute > a program from system code on all of your target platforms. And you > probably don't. And there are probably platform-dependent ways that > it could be done using inline assembly that would be hard or > impossible for you to block even if you did... > If the attacker is able to execute arbitrary system code, then they're already past the wall that this thing is intended to strengthen. It's value is in preventing them from getting to that point in the first place via tricky options that lead commands to exec attacker-controlled things. Also, I have heard rumours of commands that helpfully fire up an editor on error, which is exactly what you don't want when trying to restrict a user. This would be mitigated by blocking exec()s, and maybe other insane behaviours that none of us yet know of would also be trapped. [snip code review] > Thanks for taking the time to do that, you make several good points which I'll incorporate. |
From: Russ A. <ea...@ey...> - 2019-02-08 17:50:48
|
Nick Cleaton <ni...@cl...> writes: > If you really want the rsync protocol then a forced command of "${things > such as nsjail and libcallfilt go here} rsync --server --daemon --config > /etc/some-rsyncd.conf ." is probably about as solid as you can get it: > rsync in daemon mode is designed to interact with an untrusted user, and > you get to set which parts of the filesystem are readable and writable > in /etc/some-rsyncd.conf. > You do have to adapt the rsync client command though, to work in terms of > modules defined in your rsyncd.conf rather than file paths: > rsync -av -e ssh /my/thing us...@rs...::backups/ Don't you lose SSH authentication this way? You're spawning a separate daemon that I think is now using the built-in rsync authentication, which is just password (or nothing), so an attacker can then just connect directly to the daemon that you've spawned. -- Russ Allbery (ea...@ey...) <http://www.eyrie.org/~eagle/> |
From: Nick C. <ni...@cl...> - 2019-02-08 21:24:01
|
On Fri, 8 Feb 2019 at 17:50, Russ Allbery <ea...@ey...> wrote: > Nick Cleaton <ni...@cl...> writes: > > > rsync -av -e ssh /my/thing us...@rs...::backups/ > > Don't you lose SSH authentication this way? You're spawning a separate > daemon that I think is now using the built-in rsync authentication, which > is just password (or nothing), so an attacker can then just connect > directly to the daemon that you've spawned. > No, with --server and --daemon (as opposed to just --daemon) you get an rsync daemon connection over an ssh transport, it doesn't listen on a tcp port. http://man7.org/linux/man-pages/man1/rsync.1.html#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION I was wrong about being able to use the user@server syntax though, apparently you have to use -e "ssh -l $username" instead. > -- > Russ Allbery (ea...@ey...) <http://www.eyrie.org/~eagle/> > > |
From: Russ A. <ea...@ey...> - 2019-02-08 21:37:52
|
Nick Cleaton <ni...@cl...> writes: > No, with --server and --daemon (as opposed to just --daemon) you get an > rsync daemon connection over an ssh transport, it doesn't listen on a > tcp port. Oh! I had no idea that was a thing. Thank you. -- Russ Allbery (ea...@ey...) <http://www.eyrie.org/~eagle/> |
From: Derek M. <co...@pi...> - 2019-02-12 22:13:31
|
On Fri, Feb 08, 2019 at 09:42:32AM +0000, Nick Cleaton wrote: > On Fri, 8 Feb 2019 at 01:37, Derek Martin <co...@pi...> wrote: > > > > > [...], you need to know all of the possible ways to execute > > a program from system code on all of your target platforms. And you > > probably don't. And there are probably platform-dependent ways that > > it could be done using inline assembly that would be hard or > > impossible for you to block even if you did... > > > > If the attacker is able to execute arbitrary system code, then they're > already past the wall that this thing is intended to strengthen. Sure, but what I'm trying to point out is that there may be a variety of ways that could happen that you're not thinking of, and that some folks may imagine uses for this that you haven't, but which won't actually be workable, but that won't stop them from blaming you (and expecting you to do something about it). How do you imagine this filter program will be executed? It clearly can't be the user's shell, since a) it requires arguments to do anything useful, yet there is no way to pass them to the the shell when a user logs in, and b) the necessarily needs to be able to execute other programs (or else it must implement everything the user should be able to do internally, in some fashion). So that means typically, the user's shell must be some other program (perhaps some fork of rssh, for example) which has been taught to use the filter. But there's no guarantee that program will use it correctly, or that some flaw in that program or changes to OpenSSH or some system feature won't allow the complete bypass of whatever that program is (as we've seen repeatedly with rssh), or that the user can find a way to insert some other preload library before yours comes into play, preventing it from coming into play (e.g. the PKCS11Provider we've been talking about in regard to rssh). Unless you can predict all the ways that can happen, now and forever, and can ensure that they will always be under your control--which I'm fairly sure you can't--you have created for yourself a very hard task. That is where rssh finds itself. I'm not trying to discourage you, or to suggest it's not worth trying, I'm just trying to give you some perspective on what you may be signing up for by authoring such software. -- Derek D. Martin http://www.pizzashack.org/ GPG Key ID: 0x81CFE75D |
From: Nick C. <ni...@cl...> - 2019-02-13 09:00:53
|
On Tue, 12 Feb 2019 at 22:13, Derek Martin <co...@pi...> wrote: > On Fri, Feb 08, 2019 at 09:42:32AM +0000, Nick Cleaton wrote: > > > If the attacker is able to execute arbitrary system code, then they're > > already past the wall that this thing is intended to strengthen. > > Sure, but what I'm trying to point out is that there may be a variety > of ways that could happen that you're not thinking of, and that some > folks may imagine uses for this that you haven't, but which won't > actually be workable, but that won't stop them from blaming you (and > expecting you to do something about it). > That's a very good point, I need to be more clear in the docs about what this thing isn't trying to be. How do you imagine this filter program will be executed? There are a few ways. For example, if you're setting up sudo to allow an unprivileged user to run the frob command as root, and you don't expect frob to execute anything else or write any files outside /var/spool/frob or read any files outside /usr, you might set up a jailed-frob script something like: #!/bin/sh exec nsjail -Mo -R /usr -W /var/spool/frob libcallfilt denyexec /usr/bin/frob -- "$@" ... and allow users to run that via the sudoers file. So now you're protected against frob being tricked into reading or writing the wrong parts of the filesystem, or being tricked into running other commands. You can (and should) also limit the system calls available to frob via nsjail's seccomp support, but blocking execve() at the system call level is problematic because nsjail itself needs to call execve() after setting up the seccomp filters. Something like rssh that gets installed as the user's shell or something that gets installed as an ssh forced command and parses SSH_ORIGINAL_COMMAND (such as rsync's rrsync script) could use it similarly. I don't think rssh in its current form can be used with it, because as far as I can see there's no way to configure the path to the scp/rsync/etc programs that rssh will invoke. |
From: Nick C. <ni...@cl...> - 2019-02-13 09:36:55
|
On Wed, 13 Feb 2019 at 09:00, Nick Cleaton <ni...@cl...> wrote: > > #!/bin/sh > exec nsjail -Mo -R /usr -W /var/spool/frob libcallfilt denyexec > /usr/bin/frob -- "$@" > Sorry, that should be: #!/bin/sh exec nsjail -Mo -R /usr -B /var/spool/frob -- libcallfilt denyexec /usr/bin/frob -- "$@" (nsjail takes -B not -W for a writable part of the filesystem, and I missed out a --) |