Software, technology, sysadmin war stories, and more. Feed
Tuesday, April 10, 2018

Unauthenticated flag flipping yields remote code execution

I've worked for a few places and had friends at many more, and there are a lot of stories which keep coming up. Sometimes you might read one and think "this is about us". Well, it might be, but it's probably not. It's just that the same mistakes keep happening in different companies.

This is one of them.

One thing that seems to be pretty popular is to create a library that can handle all of the arguments you pass to your program. This is so you can run "my_server --feature1=true --color=blue" and it'll figure it all out. I'm not just talking about getopt() here -- that's old hat. These libraries will do magic things so you can define your flag in one place and have it magically plucked out of argv[] and wind up in a global variable. Then, any other part of the code (!) can access it.

So you end up with a bunch of these global variables floating around, and they get used to control a bunch of things. You get some interesting cross-unit dependencies this way.

This can be messy, but it's not the story I want to tell today.

Today's story has to do with what happens when companies take that flag management infrastructure and double down on it. The next thing you know, your programs have HTTP status servers, or proprietary RPC status methods... or both. Those status mechanisms let you read the values of those flags remotely. Want to know what server1's value for "color" is? Just query it. You'll get back "blue".

This goes on for a while, and then someone eventually gets the bright idea to allow changing the values through these interfaces. Maybe it's a HTTP GET request that just does "var/write?foo=bar" instead of the existing "/var/read?foo" that you've been doing. Maybe it's a new RPC method which accepts writes and changes the value.

The point is that now you can flip flags on and off, and change values at runtime. This might be very handy if something breaks particularly badly. It can also be used for great evil, since whatever flags you flip are not "sticky", and they will revert as soon as the program restarts. Your quick fix or feature rollout goes poof.

Even that's not the story I want to tell today.

No, the story I want to tell is when these runtime-modifiable flags are then used to drive calls to run other processes. Sometimes they happen via popen() or system(), at which point a shell gets involved. Other times, they use execve() or one of its cousins. Either way, the server ends up kicking off some other binary or script at a given path.

So let's set this up: you have a server process. It calls part of GhostScript to change plain text into a PDF. Maybe you start it up like this:

/path/to/your/server --ps2pdf_path=/usr/bin/ps2pdf

Simple enough, right? It does its thing and basically works.

Well then, tell me this: what happens the first time I come along and tell your program to "set" a new value for "ps2pdf_path"? What if I can make it something else, like "/tmp/give_me_a_shell"? What if the file at that path in /tmp is enough of a shell script or awk invocation (yes!) to start listening to a port and run arbitrary commands?

For some reason, people who tend to wire up their args/flags systems to the network also tend to forget to add authentication and authorization. Instead, anyone who comes along with a well-formatted HTTP or RPC request can start flipping flags.

I've seen this so many times now. It's just not funny any more.

There were systems where they had a "developer mode" backdoor which ignored authentication so the devs could test stuff "as that user" without getting credentials for that user. They said it was safe because it only worked "when the flag was on".

Guess what happened when someone flipped on the flag remotely. Oops.

There were systems like my "ps2pdf" example above, but they went even more into the weeds and ran as root for no good reason. They were not doing anything remotely interesting which needed root privileges even briefly, but they had them full-time anyway. Exploiting them saved you the trouble of finding a way to escalate from a regular user to root!

This isn't just on internal systems, either. Actual products have shipped into the world with flags that can be flipped this way. Some of them are really just a matter of a GET or POST to the right endpoint, and can be triggered by loading an evil web page that happens to call back to the host.

Did you catch that? Even if the machine in question can't see the outside world, if you on your browser can see that machine and the outside world at the same time, someone could try to make you fetch a resource from an internal URL which will load for you, and will create a huge mess. Great, right?

The best one I saw in terms of "meant well, really" goes to the system which glued together a flexible command-line flag parsing library, a HTTP or RPC status server with the ability to do writes, and a flexible logging library.

One of the things the logging library could do was "log to e-mail". That is, instead of (or in addition to) writing to stdout, stderr, or a log file, it could also invoke /usr/sbin/sendmail and send you the message instead.

How did it find sendmail? You guessed it: another flag. That means if you came along and set that flag to /tmp/my_evil_thing and then it would run it instead. Instant compromise!

But, you think, who logs to sendmail by default? That's true. Nobody really uses that feature, so it defaults to off. But hey, what controls whether it runs or not? That's right, yet another flag! So you set that flag on to enable mailing logs, then make sure the sendmail_path is pointed at the thing you control, and you're done!

The best part about this one is that it creates vulnerabilities in every single piece of software which uses this particular three-part cocktail. Even the programs which have been written by paranoid people like me who swear up and down that they aren't running subprocesses can be exploited by this. You can look through the app's source code all day long and never see a single subprocess invocation because it's not there.

It's in the logging library. The library they trusted.

They all meant well! It's just that gluing all of this together created a huge software monster.

Now's the point where you go audit your own stuff to see if this kind of hop, skip, and jump strategy will work. Be sure to bring a light, because it's dark in those places, and you may be eaten by a grue.

Problems like this give me a new respect for things like OpenBSD's pledge().

April 11, 2018: This post has an update.