Serious Security – How ‘special case’ code blew a hole in OpenSMTPD
If there’s one open source project with an unashamedly clear focus on security, it’s the OpenBSD operating system.
In its own words, its efforts “emphasize portability, standardization, correctness, proactive security and integrated cryptography.”
Indeed, numerous sub-projects under the OpenBSD umbrella have become well-known cybersecurity names in their own right, notably OpenSSH – which ships with almost every Linux distribution and, since Windows 10, with Windows – and LibreSSL.
There’s also OpenSMTPD, a mail server that aims to allow “ordinary machines to exchange emails with other systems speaking the SMTP protocol”, for example to let you run a mail server of your own instead of relying on cloud services like Gmail or Outlook.com.
Well, if you do use OpenSMTPD, you need to make sure you’re not vulnerable to a recently-disclosed bug that could let a crook take over your server simply by sending an email containing evil commands.
Being security-conscious doesn’t stop the OpenBSD project from writing buggy code…
…but it has made the core team very quick at responding when bugs are reported, which is what happened in this case.
The bug itself brings back memories of the infamous Internet Worm from way back in 1988, when a programmer called Robert Morris – ironically, the son of a government cryptographic researcher called Robert Morris – unleashed an auto-spreading computer virus that quickly swamped the then-fledgling internet.
One of the self-spreading tricks used by Morris was to exploit a “feature” in the Sendmail software – one that was not supposed to be used in real life, only for debugging – that allowed him to embed system commands inside the text of an email.
When the email was received by the server, it would essentially be launched as a program, instead of processed and delivered as a message.
This new OpenSMTPD bug, denoted CVE-2020-7247, was found by cybersecurity company Qualys, and gives cybercriminals a similar sort of attack lever to Morris’s worm.
In fact, when Qualys coders developed and published a Proof of Concept (PoC) to demonstrate the exploitability of this bug, they admitted that they “drew inspiration from the Morris worm”.
How the bug works
OpenSMTPD allows you to specify a command that it will use to handle the mail that it receives, whether that’s email coming in from outside or messages that you’re queuing up for delivering to other servers.
Like many Unix programs, it uses the system’s command shell
/bin/sh to spawn your command of choice, passing along the email address details as parameters.
As you probably know, “shelling out” to user-specified commands is risky, because the shell treats some characters in its list of parameters in a special way.
You can try this for yourself, for example by sending the commands below to a Unix shell.
-c means “run what follows as a command” and the text
echo inside the command string is itself a command that means “print the message that follows”.)
/bin/sh -c 'echo email@example.com' /bin/sh -c 'echo firstname.lastname@example.org more text' /bin/sh -c 'echo email@example.com;echo more text'
You’d probably, and reasonably, expect to see the following output:
firstname.lastname@example.org email@example.com more text firstname.lastname@example.org;echo more text
But you don’t – instead, you see:
email@example.com firstname.lastname@example.org more text email@example.com more text
The reason is that the semicolon character (
;) in the last line tells the shell to split the line into two commands and run them one after the other.
So the shell doesn’t print out
;echo more text at the end of the third line.
Instead it acts as though you had done this…
/bin/sh -c 'echo firstname.lastname@example.org' /bin/sh -c 'echo email@example.com more text' /bin/sh -c 'echo firstname.lastname@example.org /bin/sh -c 'echo more text'
…which is not the same thing at all!
How the bug came about
OpenSMTPD does try to stop dangerous characters such as semicolons from leaking into the commands it generates, by checking both the username part (
duck in our example above) and the domain part (
example.com in our example) of any email address you specify as the sender or the receiver of any message.
In pseudocode, it’s something along these lines:
if the username is dodgy or the domain is dodgy then reject the message end
But things are never quite that simple, because usernames and domains that are totally blank obviously fail the dodginess test, but sometimes need to be allowed.
When issues like this come along, programmers often need to describe this sort of ‘special case’ logic in their code, and wherever there’s an exception, there’s a risk that a security bypass might be introduced.
The OpenSMTPD code actually ended up like this:
if the username is dodgy or the domain is dodgy then -- allow 'dodginess' if it's caused by the fact that the address -- is completely blank, because that's a special case if both the username and the domain are blank then allow it <-- WHY NOT CHECK THIS FIRST IF IT'S SPECIAL? end -- a missing username is useless, so don't allow that if just the username is missing then reject it end -- but a missing domain name is OK, because it means 'use the default' if just the domain is missing then use the default domain name allow it <-- OOPS! THE CODE CAN GET HERE *EVEN IF WE ALREADY KNOW THE USERNAME IS DODGY* end reject the message end
You can see the problem above, namely that two special cases for accepting dodgy data were handled inside the very “if” statement that was there to reject dodgy addresses.
The end result is that a blank domain name is enough to get a message accepted even though that message already failed the username safety check!
You’re supposed to address SMTP messages like this…
MAIL FROM:<email@example.com> RCPT TO:<firstname.lastname@example.org>
…but the Qualys researchers figured out that they could trick the software into running commands of their own by saying something like this…
MAIL FROM<;command line of their choice;> RCPT TO<;another unexpected command;>
The “usernames” above are
;command line of their choice; and
;another unexpected command;, both of which are clearly both dodgy and dangerous.
Ironically, even though OpenSMTPD correctly detects those text strings as dangerous, the rogue data gets allowed through to the command shell anyway because it’s not followed by a domain name.
How it was fixed
The new code is much easier to follow, and gets the special cases out of the way first, so the “if” statements that deal with rejecting messages don’t have sub-clauses that revoke that rejection:
if both the username and the domain are missing then -- a very specific special case tested first allow it end if the username is missing or dodgy then -- blank or dodgy usernames *must* fail up front reject it end if the domain is missing then --if we get here, the username is OK so we -- use the default domain name end if the domain name is dodgy -- if we get here, we do have a domain because -- it's either the one specified or the default, -- so now we can check if it's dodgy reject it end -- and at this point, we have: -- a valid, non-empty username -- a valid, non-empty domain (perhaps the default) -- so we can... accept it
What to do?
This bug is dangerous because, by default, OpenSMTPD listens for local mail that’s being sent out.
When mail is received locally, the server uses the root (superuser account) to deal with it, so anyone who’s already logged in can use this bug to “promote” themselves to root.
That’s an elevation of privilege (EoP) vulnerability.
But if you are using OpenSMTPD to accept mail from outsiders, then the bug is worse because users who don’t even have accounts on your system, let alone who aren’t logged in, can run commands on your server just by transmitting a sneakily-formatted email.
That’s a remote code execution (RCE) vulnerability.
- If you have a vulnerable version of OpenSMTPD, patch it now. The fix was delivered rapidly, so do yourself the favour of applying it rapidly, too. The patch arrived in OpenSMTPD 6.6.2 (6.6.2p1 if you are using the so-called Portable source code intended for use on operating systems other that OpenBSD itself).
- Watch out when programming for special cases. If you are a coder, don’t be in too much of a hurry to “fix” problems handling unusual or unexpected data. Try to get the special cases out of the way first so you don’t end up with code that’s supposed to block errors but has numerous exceptions that cause the error to be ignored. The clearer your code, the easier it is to review and the more likely it is to be correct.
- Minimise your use of the root account. Sometimes, you can’t avoid it, for example if you need to access system files or reconfigure privileged services. But always be even more cautious than usual when preapring data to be handed up to a program that will run as root.
- Avoid running sub-programs via the shell if you can. Sometimes, for reasons of flexibility – like here, when you might want to hand off from OpenSMTPD to a script of your own – you have little choice. But always be even more cautious that usual when preparing data to be passed into a shell script, because of those dangerous “special characters”.