Ruby Class Pollution

This is a summary from the post https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html

Merge on Attributes

Example:

# 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)

Explanation

  1. Privilege Escalation: The authorize method checks if to_s returns "Admin." By injecting a new to_s attribute through JSON, an attacker can make the to_s method return "Admin," granting unauthorized privileges.

  2. Remote Code Execution: In health_check, instance_eval executes methods listed in protected_methods. If an attacker injects custom method names (like "puts 1"), instance_eval will execute it, leading to remote code execution (RCE).

    1. This is only possible because there is a vulnerable eval instruction executing the string value of that attribute.

  3. Impact Limitation: This vulnerability only affects individual instances, leaving other instances of User and Admin unaffected, thus limiting the scope of exploitation.

Real-World Cases

ActiveSupport’s deep_merge

This isn't vulnerable by default but can be made vulnerable with something like:

Hashie’s deep_merge

Hashie’s deep_merge method operates directly on object attributes rather than plain hashes. It prevents replacement of methods with attributes in a merge with some exceptions: attributes that end with _, !, or ? can still be merged into the object.

Some special case is the attribute _ on its own. Just _ is an attribute that usually returns a Mash object. And because it's part of the exceptions, it's possible to modify it.

Check the following example how passing {"_": "Admin"} one is able to bypass _.to_s == "Admin":

Poison the Classes

In the following example it's possible to find the class Person, and the the clases Admin and Regular which inherits from the Person class. It also has another class called KeySigner:

Poison Parent Class

With this payload:

It's possible to modify the value of the @@url attribute of the parent class Person.

Poisoning Other Classes

With this payload:

It's possible to brute-force the defined classes and at some point poison the class KeySigner modifying the value of signing_key by injected-signing-key.\

References

Last updated