rce-regex: a case of command injection

TL;DR

I discovered and fixed an RCE (Remote Code Execution) vulnerability worth $20k in bounty due to a combination of naive shell script interpolation and a mistake on regex validation. The endpoint was for internal use only and correctly required authentication, but was also exposed to the wide internet, making it a risk. I was featured on an internal post and on the company's security training, presenting the contents of this writeup.

Vulnerability

Soon after joining the company, I was reviewing a patch by a colleague when our internal tooling flagged an unrelated line of code for possible command injection. The code was interpolating a string containing shell script in order to run a command. The authors knew it was dangerous, so they validated the input with a strict regex. The code looked like this:

if variable =~ /^[a-z]+$/
  system("/usr/bin/someprogram #{variable}")
end
      

However, I was successful in bypassing their validation and running commands on the host with a string like the this:

"followthewhiterabbit\n/usr/bin/hacktheworld"
      

This mistake is documented on the Ruby on Rails guide. It happens because ^ and $ match beginning and end of line, not input. The correct way to express this intent would be to use the \A and \z anchors instead.

But would that be enough?

Solution

The author just wanted to run a program and resorted to interpolating shell script to build the command line, which both makes total sense and is extremely dangerous. However, Ruby has a safer API for that (which I appreciate is not obvious at all unless you read the docs):

# note that it takes two arguments instead of one
system("/usr/bin/someprogram", variable)
      

Calling system() with a single string makes it feed the input to the default shell, allowing exploitation. However, calling it with a list of strings makes it spawn the process directly with exec(), bypassing the shell. If there's no shell, there's no injection.

This type of vulnerability can happen in virtually every programming language, including Ruby, JavaScript, HTML and SQL (see Exploits of a Mom). Essentially it can happen whenever metaprogramming is mixed with user input.

Conclusion

The vulnerability was in an internal endpoint that correctly required authentication, so it wasn't easily exploitable from the outside. However, because of reasons, it was also exposed to the wider internet, making it a risk of privilege escalation if an attacker could get around the authentication.

The Security team estimated that it would result in a $20k bounty payout if reported by an external researcher. They released an internal post delivering my explanation of the vulnerability to the company. Following the post, I was featured on the company's secure code review onboarding training presenting the contents of this article.