Ruby Class Pollution
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
This is a summary from the post https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html
Merge on Attributes
Example:
# 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)Explanation
Privilege Escalation: The
authorizemethod checks ifto_sreturns "Admin." By injecting a newto_sattribute through JSON, an attacker can make theto_smethod return "Admin," granting unauthorized privileges.Remote Code Execution: In
health_check,instance_evalexecutes methods listed inprotected_methods. If an attacker injects custom method names (like"puts 1"),instance_evalwill execute it, leading to remote code execution (RCE).This is only possible because there is a vulnerable
evalinstruction executing the string value of that attribute.
Impact Limitation: This vulnerability only affects individual instances, leaving other instances of
UserandAdminunaffected, thus limiting the scope of exploitation.
Real-World Cases
ActiveSupport’s deep_merge
deep_mergeThis isn't vulnerable by default but can be made vulnerable with something like:
Hashie’s deep_merge
deep_mergeHashie’s deep_merge method operates directly on object attributes rather than plain hashes. It prevents replacement of methods with attributes in a merge with some exceptions: attributes that end with _, !, or ? can still be merged into the object.
Some special case is the attribute _ on its own. Just _ is an attribute that usually returns a Mash object. And because it's part of the exceptions, it's possible to modify it.
Check the following example how passing {"_": "Admin"} one is able to bypass _.to_s == "Admin":
Poison the Classes
In the following example it's possible to find the class Person, and the the clases Admin and Regular which inherits from the Person class. It also has another class called KeySigner:
Poison Parent Class
With this payload:
It's possible to modify the value of the @@url attribute of the parent class Person.
Poisoning Other Classes
With this payload:
It's possible to brute-force the defined classes and at some point poison the class KeySigner modifying the value of signing_key by injected-signing-key.\
References
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
Last updated