Don't Pipe to your Shell
blog.existentialize.com
blog.existentialize.com
BTW: If you want ULTIMATE BRAVERY, you have to boot an arbitrary kernel over the internet: http://ipxe.org/ (scroll to the bottom, where it says `iPXE> chain http://boot.ipxe.org/demo/boot.php`)
It's not really absurd. In truth, this roughly equivalent to what people would do if you had them download the code, unpack it, and go to work.
What the author doesn't seem to know is that if the web server returns Content-Length (which the project really ought to ensure, and all the ones I've seen do), then wget will by default keep retrying until such time as the entire content stream has been successfully downloaded. If you Ctrl-C out of it, both wget and the shell process will terminate without processing any partially downloaded statements. The one liner is just automating the tedious manual steps that people would otherwise do, and arguably doing a better job of it than most.
You can and should do better (which is why package managers exist), but this is better than what a lot of people do if you don't provide the one liner. If you look at the commands in most one-liner shell scripts, there usually aren't a lot of destructive things like "rm" in them either.
Now, it'd still be better to do these one-liner shell scripts as shell archive whose last line extracts and runs the actual script, and you could use gzip encoding to provide some extra sanity checking, but for the most part one liner scripts are not the problem.
http://rvm.io suggests you run the command:
\curl -L https://get.rvm.io | bash -s stable
Which exhibits exactly the failure scenario outlined in the article; partway through the script RVM cleans up after itself by running "rm -rf ${rvm_src_path}"
Simple changing this to
"\curl -L https://get.rvm.io > /tmp/rvm.sh && bash /tmp/rvm.sh stable"
Would eliminate the potential for nasty failures, with a minimum of fuss.
Of course, doing it the way you describe creates a /tmp race condition, so there's that.
I'm also curious what you think a partial read of "rm -rf ${rvm_src_path}" might resolve to that would be dangerous.
For those whose security model looks like http://xkcd.com/1200 that's not a big difference; but depending on context it might be.
Perhaps the problem here is not that these project "developers" are suggesting that others pipe unread scripts into /bin/sh, it's that too often these "developers", or the others on the receiving end of their scripts, have a poor understanding of shell scripting.
I find in the majority of cases where someone wants me to run their shell script, I could do what needs to be done without using the script or rewrite the script myself, using a minimal POSIX-like shell instead of bash or ksh, and reducing script size by 90% or a similarly large percentage. Then there's the minority of cases where I actually admire the care that the script author took to make the script efficient and portable, or where I actually learn something new by reading the script. That happens all too rarely.
While I'm on the topic of shell scripting, I'll add that I do a lot of shell scripting for my own personal use. Since no one else ever has to read my scripts, I take the luxury of ignoring the Shift key and write all my scripts in lowercase. I have no need for uppercase variable names; they only slow me down.
I note that djb also writes his shell scripts in lowercase. For whatever it's worth, I consider him to be one of the world's best open source programmers, and a competent shell scripter.
One difference is that piping to a shell is a lot easier to inspect. So you could easily argue that piping to a shell is safer.
Heck, even downloading a source tarball and compiling it yourself isn't any safer unless you actually inspect that source. And who does that?
The issue isn't executing untrusted code, it's connections terminating early and causing your shell to execute incomplete code.
The article ends with the example code
TMP_DIR=`mktemp`
rm -rf $TMP_DIR
And the stream ending pre-maturely may cause the second line to end with rm -rf / and then execute it. While this wouldn't do anything anyway without --no-preserve-root added, it still brings up a good point about interrupted connections executing incomplete code which would otherwise be safe if the command was finished.wget -O - https://example.com/install.sh | sudo sh
Turns into this:
wget -O install.sh https://example.com/install.sh && sudo sh ./install.sh
Big deal.
EDIT: Changed protocol to https for FiloSottile.
EDIT: Thanks <3
Straw man.
No it doesnt turn into that, it turns into wget https://install.sh && ls && less install.sh && less lala2 && echo "ok looks good enough" && ./install.sh # no sudo, to user local install, its just a package or program ffs,
when you want packages to install for all users, or system wide, then you use the default package manager of your distribution.
Its only really bad if some does the following:
rm -rf /$TMP_DIR
The slash is important function install_my_software() {
...
}
install_my_software()
If the script is partially downloaded, it won't have any effect.It just seems to me that the main should worry about interfacing with the outside world, and the rest of your code should really just be written as a library.
I made a utility "shacat", which takes a sha1 checksum as an argument and then pipes input to output iff the sum of input matches the sum
e.g.
$ curl http://site.com/script | shacat 7ef39183 | sh
You don't even need https! You copy the command from the site including the sha sum, so it can't have been tampered withOf course, getting people to use shacat is the hard part
Why that? Have we never heard of defaced web pages?
Granted, using shacat is much better than piping into sh. But basic learning from security breaches is that nothing is safe, you only can find ways to do thing in a less catastrophic manner than others.
Well if you can't trust the website you're screwed anyway. If the website is compromised then absolutely any way they have of installing software is broken
rm -rf $TMP
and never: rm -rf /
Because that is not in the code.Also, both:
rm -rf $TM
and: rm -rf $TMP_
won't cause the issue (but yeah, potentially others).Statistically a cosmic ray flips a few bits in your RAM every year. Theoretically those bit flips could transform a benign program into a malicious one, and I'm sure that's happened to somebody somewhere sometime.
But it's very unlikely to happen to you tomorrow. You could use ECC RAM to prevent this from happening, but how many people do that?
> This might be unlikely, but the results of this happening, even once, could be catastrophic.
Also, you don't have to kill any process, that was a PoC by the author. The scenario is a connection drop.
I haven't.
> Bug: Update the nginx (?) error pages to be bash friendly
>
> root@marius ~]# curl -s https://lv.linode.com/nGiB | sudo bash
> bash: line 1: html: No such file or directory
> bash: line 2: syntax error near unexpected token `<'
> 'ash: line 2: `<head><title>502 Bad Gateway</title></head>
>
> This could be done by just dumping out echo statements since its already
> being parsed by the shell. Additional meta about the request (headers, etc)
> could be dumped into comments too…
>
> # 502 Bad Gateway
> echo
> echo "Uh oh: Something went sideways during the install!"
> echo "("
> echo "Re-running the installer should clear things up."
> echo
> echo " sudo !!"
> echoMany wouldn't.
> running arbitrary commands on your machine that could change based on user agent to trick you
Without even considering MitM, that running signed executables kind of mitigates.
Anyway, the point of the author is the failing mode.
Absolutely not. The author points out 1 way in which stupid inspecting could be beaten, however you can always do
$ curl https://coolscript.com/script > tmp.sh
$ cat tmp.sh
echo "I'm safe!"
$ ./tmp.shTo avoid users complaining about how dangerous it is.
This is why I distribute a tarball with an installer script in it. It's functionally almost the same, not that much harder, but it avoids all the backlash and makes my software more likely to be used.
It is like giving away all the security built everywhere else and yelling "YOLO".
(Because the authors should offer it over HTTPS, sign it, provide some checksum or ship it through some package manager that knows better, if they can't secure the process themselves)
Fix all the problems you can, of course, defense in depth is always a good idea. But hackers managing to alter a mirrored file without getting access to the website itself happens more often than you'd like to admit, and even hackers getting access to both a website and a tarball can be mitigated by signing tarballs instead of just listing sha sums.
If you trust your website won't be hacked to serve malicious code then you might do away with signing and cryptographic checksums, but then that only makes using TLS that much more important to avoid having your tarball zoinked by a MitM during transit.
Speaking for myself, this has never caused a problem for me, and I'll probably keep doing it because it's convenient and that convenience is more valuable weighed against the potential bad things that could happen. Most likely is the case that the package just doesn't execute. The probability that it ends up on rm or something destructive is probably very low, and if someone is actively trying to MITM you, they will find a way if you are smart enough not to run scripts from wget, most people aren't the target of this kind of very specific attack.
Like Apple's TouchID – it may not really be secure, but it's very convenient, and that will often be enough to make it mainstream.
I think we all understand this is not for the best but since it's normal we'd just go with it.
The number of times people might do this is probably well below 100 and there are much more risky day to day security faux pas than this.
One reason the bad is successful being the new normal is it came in smaller doses you don't even notice that or you think it's ok to ignore.
- A curl endpoint, piped to shell.
- 5 times out of 6, the result is echo "click"
- The other time, is something more sinister.Ah, those unexpected expectings.
foo() { ... }
1. Create a new library, rubygem, infrastructure thingie that was genuinely useful but had a non-trivial setup process.
2. To aid in that setup process, create a script that I recommend be piped into your shell.
3. Somewhere in the middle of that script, silently tar up everything in the users .ssh/ directory and send it somewhere I can see it.
Now putting something into ~/.ssh/...
I never do that; If I need a trial loop, I do the loop with all the relevant commands prepended by "echo" (and every ";", "&", "&&", and "||" properly escaped).
That's really about as well as you can do, because HTTP doesn't do a good job of reporting errors. You could try to get the content length in advance and then check against it after the download (which is basically what wget is doing), but that won't buy you much. Most servers won't do Content-MD5, so that's out. One smart thing to do would be to use "Accept-Encoding" to download a compressed version of the script and then do a decompression test before running. Alternatively, you can make the download script into a shell archive style script, such that it doesn't do anything until you get to the last byte, at which point it extracts out the real script and runs it (which wouldn't change what your install command is).
The whining about disabling the certificate check is also spurious. Most of the time these are scripts pointing to a non-https URL but which redirect to an HTTPS URL. You are already vulnerable when you do the HTTP request. On top of that, almost nobody is doing DNSSec, so you are already vulnerable at the DNS level. Even ignoring that, Salt offers it as a solution if you can't get the certificate check to work. The alternative would be to provide you with instructions on how to install a CA certificate, which someone is far more likely to screw up and unless you've established trust of the instructions themselves, could be just as vulnerable to a man-in-the-middle attack. Offering instructions on how to disable the check is a perfectly reasonable solution.
It is surprising to me that the shell will honor a command line without a newline[1], but it does:
$ (echo -n "echo rm -rf /tmp/mydir"; exit 1) | sh
OUTPUT: rm -rf /tmp/mydir
Obviously if tmpdir got truncated to /tmp, or even /, bad things would happen.
[1] Not so suprising when you consider sh -c "command" works without a trailing newline, I suppose.
Nope. By default wget will literally try forever (you can even shut down your network stack, wait for 10 minutes, and then turn it back on, and wget will proceed from there), and until it closes the stream, the shell interpreter will act as though data is still coming. Any normal way of telling wget to stop will also stop the shell interpreter. Worst case is you end up in some "in the middle" state of the script, but you won't be misinterpreting the script due to early termination of the stream.
That doesn't matter though because the problem that matters is NOT the buffering. Sure that can mean scripts execute partially and then exit, but they will never do something that wasn't intended. The problem the blog is talking about is when you get an EOF in the middle of a script.
The maintainer didn't check the commit and included it in develop, which consequently was downloaded and... ect.
I'm looking for it.
A work around I've been using for a while. I then :saveas and execute it myself once I verify it's not doing anything fishy.
Another favourite are those tiny installers that wrap a nice "curl http://yeah.not.ssl.com/boom.sh | sudo -s" inside the "installer". Great for building confidence in a project.
Maybe the best thing to do is to just distribute the shell scripts encrypted with the private key of the project, thereby forcing users to run it through gpg. Or use a gpg-archive -- both PGP and GPG has support for this, see gpg-zip(1).
That way you can at least establish a chain of trust that goes straight back to the author, and links the installer(s) directly with a gpg-key. It's not really possible to get anything better than that. And you avoid having a separate .sig-file.
Apparently there's also a GNU project for distributing signed archives -- but it doesn't appear to have wide support.
wget -O - http://example.ru/public.key | gpg --fast-import && sudo gpg --trust 0xABADIDEA
wget -O $URL > /tmp/f
if echo `md5sum /tmp/f` | grep "^8a17590b8e78f8f1cf4983e0e691b7ab"; then
sudo sh /tmp/f
fiAlso, barring that example I can't come up with many other horrible scenario. Unfortunate ones, sure. But not catastrophic. And as someone else said, adding random ppas would allow much worse things, and people do that all the time.
dir_user_old=/home/luser/.old_version
dir_user=/home/luser
rm -rf $dir_user_old
^
Truncate at the caret (^), and it turns into: dir_user_old=/home/luser/.old_version
dir_user=/home/luser
rm -rf $dir_user
which removes your home directory.Incidentally, I think the last example in the OP ("rm -rf /") is wrong. The "/" would never be transmitted, it is part of the variable $TMP_DIR which is expanded on the local system, not the remote server. But the idea and the other test with echo seem correct.
Considering that there is usually a sizable payload and the probability of a dropped connection is not evenly distributed and is probably very low, the scenario gets even less likely.
Yes, it's possible, but it's also possible to rm -rf / because you typed a path wrong and I bet the probability of human error is much higher than the probability of this shell trick screwing you over. People have rm -rf /'d their systems, but even this isn't a good reason to advocate for say, removing rm entirely or not allowing people to type into the shell :P
http://www.linuxbsdos.com/2012/04/29/what-will-rm-rf-actuall...