0xdevalias / devalias.net

Source for devalias.net
http://www.devalias.net/
49 stars 10 forks source link

[Blog] Ruby Security + Dynamic Method Attack Chain Example #9

Open 0xdevalias opened 5 years ago

0xdevalias commented 5 years ago

While auditing some Ruby source code on a bug bounty program the other day I came across an interesting pattern, that looked like it should be vulnerable. I really hoped I could turn this into a cool attack chain.. but it seemed there were just enough annoying constraints in place to make it unlikely, if not downright impossible :'(

So instead, have a bit of a walkthrough of what's bits are here, and how my mind would tend to approach attempting to break something like this when wearing my hacker/researcher hat. Often the thing that makes a cool find is just understanding the context in which the potential bug exists, and knowing what elements you can use to 'live off the land' and escalate it into something worth that sweet Bug Bounty $$.

The main interesting chunk of the vulnerable code looks like this:

description_method = activity.key.tr('.', '_')
descriptor = private_methods.find { |i| i[description_method] }
result.merge!(method(descriptor).call || {}) if descriptor

Since we can't do a super cool attack chain with that as is, let's simplify it down the bare minimum for demonstration purposes:

attack = ''
descriptor = private_methods.find { |i| i[attack] }
method(descriptor).call if descriptor

private_methods.find { |i| i[attack] } is basically referencing the [] method on a symbol:

This is obviously a bug (it should be something more like private_methods.find { |i| i == attack.to_sym }, but interestingly, thanks to Ruby's helper methods and attempts to make our lives super easy, we gain an interesting property that allows us to essentially bypass this check.

image

Since the value returned is the matching substring, and Ruby treats a string value as 'truthy', any partial match would result in a valid value being returned from find. And because it is using our block as a test (and not to map the values), it will use the full value that caused the match (not just the substring in our block). Convenient!

Based on these 2 qualities, so long as our attack matches a partial string in private_methods, it will be returned. Since find will only return the first found match, we would need to make sure our attack string uniquely hit our desired payload (or at least the order of private_methods changed enough that we could eventually hit it)

So from what we know here, we could simplify our example again:

attack = ''
method(attack.to_sym).call

To see what potential gadgets we would have available to .call with no params, we could do something like the following (note: this doesn't seem to consider if the methods require a block to be passed to them):

private_methods.select {|i| method(i).arity == 0 }

=> [:getwd, :pwd, :rename_cannot_overwrite_file?, :fu_have_symlink?, :fu_windows?, :fu_default_blksize, :default_src_encoding, :global_variables, :__method__, :__dir__, :iterator?, :block_given?, :loop, :__callee__, :enable_warnings, :at_exit, :fork, :not_implemented, :syscall, :proc, :lambda, :silence_warnings, :binding, :local_variables, :initialize]

Now, since there isn't a lot I know of off hand that we could do with the .call with no params.. let's contrive a situation similar to the above, that would let us do something interesting:

attack = 'eval'
params = ['puts "h4x0r3d!"']
method(attack.to_sym).call(*params)
# h4x0r3d!

When simplified and broken down to this level, it seems obvious why this would be vulnerable, and I doubt we would ever intentionally commit code that looks like this. But I guess this goes to demonstrate how a few basic levels of abstraction + leveraging language features can make a seemingly quite obvious security issue far less simple/easy to spot. In the real world, that's often where the cool vulnerabilities/attack chains come through: a few seemingly innocuous elements, that when combined together in the right way lead to something as blatantly bad as the above contrived example.

Based off the ruby-security guide (forked+cleaned up), there appear to be a number of other potentially interesting methods that could be abused for similar purposes (depending on the context/situation they exist within). A non-exhaustive list includes:

And a whole bunch more..

There also seem to be some interesting language-level tools for marking data as untrusted, which could be useful in designing some level of 'defense in depth' around this: