-1

I'm considering a possible denial of service attack scenario, where a script cause a system resource outage by recursively invoking itself as interpreter.

The principle is as follow:

The script specifies at its first line, in the form of a #! shabang, the absolute path of itself, as its own interpreter.

The system kernel will, depend on its support, automatically invoke the interpreter during the execve system call, prepending the interpreter, to the vector of arguments.

Such invocation will exhaust the limit on the size of program arguments ({ARG_MAX}) set in the system, thus causing a (possibly isolated) failure.

Experiment

I've created 2 different set of attack vectors,

  • The first one, invoking itself

    #!/usr/local/bin/recurse
    
  • The second one, invoking each other.

    #!/usr/local/bin/recurse-1
    
    #!/usr/local/bin/recurse-2
    

I've tested these 2 attack vectors on macOS Big Sur 11.5.2. And when I check the exit status using echo $?, it shows 0, which means the processes completed successfully.

Question.

Had modern operating systems been patched against such attack? Are there research papers on this?

muru
  • 72,889
DannyNiu
  • 620
  • 5
  • 19
  • 1
    I'm not sure that I see where the DoS is here. You've set out in the question already why the recursion would quickly end as the command line got too long, even with no other mitigation. Other than the brief memory spike and some wasted PIDs, what's the external cost you are envisaging? – Michael Homer Aug 22 '21 at 03:36
  • @MichaelHomer I didn't do an estimate of "external cost", I categorized this as DoS solely because it doesn't fit in other categories. I was suprised that this resulted in the exit status of 0, which is the main reason I asked it here. – DannyNiu Aug 22 '21 at 03:44
  • Yes, but the only service that’s denied is the inherently broken script itself - is that what you’re thinking of? – Michael Homer Aug 22 '21 at 04:00
  • @MichaelHomer pretty much, and I wonder why it didn't give a failure exit status. – DannyNiu Aug 22 '21 at 04:01
  • 2
    The exit status is not zero when run with exec(3) (“Exec format error”), so I think you’re seeing bash run it as a shell script containing only a comment. – Michael Homer Aug 22 '21 at 04:02
  • @MichaelHomer I did a bit of further experiment based on your input, I'll make it into an answer hope you don't mind. – DannyNiu Aug 22 '21 at 04:07
  • @MichaelHomer Your comment is probably related to Which shell interpreter runs a script with no shebang? right? – Kusalananda Aug 22 '21 at 07:14
  • @Kusalananda Well, partly, though in this case zsh actually errors out rather than treating it as a script, and even bash doesn't do it on Linux, so it's not like the case of a pure executable text file. – Michael Homer Aug 22 '21 at 07:17

2 Answers2

2

On Linux, I get the following:

A script with a nonexisting interpreter on the hashbang line (execve() gives ENOEXEC):

$ cat brokenhashbang.sh
#!/bin/nonexisting
echo hello
$ ./brokenhashbang.sh
bash: ./brokenhashbang.sh: /bin/nonexisting: bad interpreter: No such file or directory

A script with recursive hashbang (ELOOP):

$ cat /tmp/recursivehashbang.sh 
#!/tmp/recursivehashbang.sh
echo hello
$ /tmp/recursivehashbang.sh 
bash: /tmp/recursivehashbang.sh: /tmp/recursivehashbang.sh: bad interpreter: Too many levels of symbolic links

A script with an existing but non-executable interpreter (EACCESS):

$ cat noexechashbang.sh 
#!/etc/passwd
echo "hello?"
$ ./noexechashbang.sh 
bash: ./noexechashbang.sh: /etc/passwd: bad interpreter: Permission denied

It's not just Bash: Dash, ksh and zsh give similar errors.

If the script doesn't have a hashbang, or the hashbang points to an otherwise non-executable file (which needs to have +x, you get ENOEXEC), then the behavior differs a bit. Bash runs the file itself as a shell script, while zsh seems to look inside to see if there's a hashbang line, and then either tries to start that interpreter, or runs it with /bin/sh. Running the script via the shell on ENOEXEC is the POSIX-specified behaviour for the shell and for the execlp()/execvp() functions.

(For the case with a nonexisting interpreter, Dash just gives the confusing dash: 1: ./brokenhashbang.sh: not found, as if the script itself didn't exist. But I think it's the same error from the underlying system call and Dash is just too simple to check which file is missing.)

In any case, I can't see what the attack here would be, since if they're running a command of your choosing, you can already do whatever you like. Not by having a nonexisting interpreter for the script, but a working one.

ilkkachu
  • 138,973
  • There's a fourth case: interpreter is +x non-text non-executable, which does produce ENOEXEC in Linux and Bash does run the script as a shell script (but zsh still doesn't, even in sh emulation). – Michael Homer Aug 22 '21 at 08:54
  • @MichaelHomer, right, Bash runs it itself if it gets ENOEXEC (I think regardless of if the error comes from the script itself, or the interpreter). But as far as I can tell, zsh then looks inside to see if there's a hashbang, and tries to run the interpreter itself (getting ENOEXEC again if the interpreter doesn't work). And if there's no hashbang, it runs the script with /bin/sh. – ilkkachu Aug 22 '21 at 09:58
  • Note that you also get the ELOOP error for echo '#!a' > b; echo '#!b' > a; chmod +x a b; ./a – Stéphane Chazelas Aug 22 '21 at 10:56
  • You do get an endless loop with a echo '#! /usr/bin/env' > a; chmod +x a; ./a – Stéphane Chazelas Aug 22 '21 at 10:58
  • @StéphaneChazelas, with help from env specifically exec'ing the file again, yes. Not too bad a DoS, it's just the same process calling execve and some other syscalls over and over again. And yeah, I think most implementations just detect loops by counting redirections, so it doesn't matter how long the loop is (or if it's just a really long non-looping chain) – ilkkachu Aug 22 '21 at 11:21
  • @StéphaneChazelas maybe I'm too dense, but nobody has yet demonstrated anything more interesting than printf 'echo BOOBS; "$0"' > a; chmod +x a; ./a ;-) –  Aug 22 '21 at 13:29
  • @MichaelHomer same will happen with any program which is using execvp(3), including but not limited to find, xargs, perl, etc: printf '\xff' > interp; printf '#!./interp\necho yeah\n' > script; chmod +x ./interp ./script; echo 1 | xargs ./script. Again, I don't see anything interesting about it -- everything works as documented. –  Aug 22 '21 at 13:47
  • @CocaineMitch, yes that one would also fork extra processes at each iteration, but I was the impression that the OP was looking to exploit the shebang mechanism specifically. – Stéphane Chazelas Aug 22 '21 at 13:49
  • @CocaineMitch, ah, good point that execvp() does it too. – ilkkachu Aug 22 '21 at 15:13
0

NOTE The following is tested on macOS Big Sur 11.5.2.

According to the standard (as well as existing practice), the shell will attempt to interpret the file and execute the commands within if the exec function call returns with failure.

Here's a 3rd attack vector:

#!/usr/local/bin/recurse

eval 'printf "%s\n" xxx'

After naming this file as /usr/local/bin/recurse and executing it, the string xxx was printed on standard output, despite the interpreter loop is supposed to prevent the command from being evaluated.

If using a C program to invoke the script with the execv function, the errno value set by the function call is ENOEXEC.

DannyNiu
  • 620
  • 5
  • 19
  • Cannot reproduce with .printf '#! /tmp/rec\necho yeah' > /tmp/rec; chmod 755 /tmp/rec; /tmp/rec -- it will fail with ELOOP as expected. Maybe /usr/local/bin is special on your system. Please show something reproducible. –  Aug 22 '21 at 12:48
  • 1
    @CocaineMitch This answer gives a correctly reproducible example on macOS, as stated in the question, which produces ENOEXEC in this case. – Michael Homer Aug 22 '21 at 18:31
  • @MichaelHomer Indeed, my bad and sorry. It seems that that's the case on any BSD and Solaris, the ELOOP errno in that case being Linux-specific. –  Aug 22 '21 at 19:27
  • And since execve fails it ENOEXEC in that case, it triggers the "pass the file as a script to the shell interpreter" fallback of the shell or execvp / execlp functions. –  Aug 22 '21 at 19:34