Associated Vulnerability
Title:Rails 代码问题漏洞 (CVE-2022-32224)Description:Rails是美国Rails团队的一套基于Ruby语言的开源Web应用框架。 Rails 存在安全漏洞,该漏洞源于当使用YAML(默认)的序列化列被反序列化时,Rails使用YAML.unsafe_load将YAML数据转化为Ruby对象。如果攻击者可以操纵数据库中的数据(通过SQL注入等手段),那么攻击者就有可能升级到RCE。
Readme
CVE-2022-3222 ActiveRecord シリアライズ 動作確認
===
- https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
- https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html
- https://github.com/rails/rails/pull/45584
### 準備
```
bundle install
bin/rails db:migrate
```
### モデル
```
bin/rails generate model User values:string
```
```ruby
class User < ApplicationRecord
serialize :values, Array
end
```
### 動作確認
#### 配列をシリアライズして保存
```ruby
❯ bundle exec rails c
Loading development environment (Rails 7.0.3.1)
irb(main):001:0> User.create(values: [16 * 16, "ffff"])
(0.6ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
User Create (0.6ms) INSERT INTO "users" ("values", "created_at", "updated_at") VALUES (?, ?, ?) [["values", "---\n- 256\n- ffff\n"], ["created_at", "2022-07-17 07:46:18.593587"], ["updated_at", "2022-07-17 07:46:18.593587"]]
TRANSACTION (0.4ms) commit transaction
=>
#<User:0x0000000105ee1a78
id: 3,
values: [256, "ffff"],
created_at: Sun, 17 Jul 2022 07:46:18.593587000 UTC +00:00,
updated_at: Sun, 17 Jul 2022 07:46:18.593587000 UTC +00:00>
irb(main):002:0> User.last.values
User Load (0.5ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
=> [256, "ffff"]
```
`class_name_or_coder` として `Array` が指定されているため、文字列をそのままは渡せない
```ruby
irb(main):001:0> User.create(values: 'aaa')
(0.6ms) SELECT sqlite_version(*)
.../activerecord-7.0.3.1/lib/active_record/coders/yaml_column.rb:36:in `assert_valid_value': can't serialize `values`: was supposed to be a Array, but was a String. -- "aaa" (ActiveRecord::SerializationTypeMismatch)
```
#### 更新
```ruby
irb(main):001:0> user = User.create!(values: ["aaa"])
(0.6ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
User Create (0.4ms) INSERT INTO "users" ("values", "created_at", "updated_at") VALUES (?, ?, ?) [["values", "---\n- aaa\n"], ["created_at", "2022-07-17 11:50:58.515968"], ["updated_at", "2022-07-17 11:50:58.515968"]]
TRANSACTION (0.4ms) commit transaction
=>
#<User:0x00000001057b65a0
...
irb(main):002:0> user.update_column(:values, ["aaaa"])
User Update (0.9ms) UPDATE "users" SET "values" = ? WHERE "users"."id" = ? [["values", "---\n- aaaa\n"], ["id", 8]]
=> true
irb(main):003:0> user.update_attribute(:values, ["fffff"])
TRANSACTION (0.1ms) begin transaction
User Update (0.5ms) UPDATE "users" SET "values" = ?, "updated_at" = ? WHERE "users"."id" = ? [["values", "---\n- fffff\n"], ["updated_at", "2022-07-17 11:52:01.795259"], ["id", 8]]
TRANSACTION (1.0ms) commit transaction
=> true
irb(main):004:0> user.update(values: ["aaaddd"])
TRANSACTION (0.1ms) begin transaction
User Update (0.7ms) UPDATE "users" SET "values" = ?, "updated_at" = ? WHERE "users"."id" = ? [["values", "---\n- aaaddd\n"], ["updated_at", "2022-07-17 11:52:25.306288"], ["id", 8]]
TRANSACTION (1.1ms) commit transaction
=> true
```
#### `"`や`'`、改行を含む文字列のシリアライズ・デシリアライズ
```ruby
irb(main):001:0> User.create(values: ["'", '"', {'"' => [{"aaa" => "aa\na\r"}]}])
(0.6ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
User Create (0.7ms) INSERT INTO "users" ("values", "created_at", "updated_at") VALUES (?, ?, ?) [["values", "---\n- \"'\"\n- \"\\\"\"\n- \"\\\"\":\n - aaa: \"aa\\na\\r\"\n"], ["created_at", "2022-07-17 07:50:43.091916"], ["updated_at", "2022-07-17 07:50:43.091916"]]
TRANSACTION (0.4ms) commit transaction
=>
#<User:0x000000010dfb3cc8
id: 4,
values: ["'", "\"", {"\""=>[{"aaa"=>"aa\na\r"}]}],
created_at: Sun, 17 Jul 2022 07:50:43.091916000 UTC +00:00,
updated_at: Sun, 17 Jul 2022 07:50:43.091916000 UTC +00:00>
irb(main):002:0> User.last.values
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
=> ["'", "\"", {"\""=>[{"aaa"=>"aa\na\r"}]}]
```
### 任意のクラスの復元
7.0.3では任意のクラスがシリアライズでの保存、読み込み時のデシリアライズができる
```ruby
❯ bundle exec rails c
Loading development environment (Rails 7.0.3)
irb(main):001:0> gemspec = Gem::Specification.new("test")
=>
Gem::Specification.new do |s|
...
irb(main):002:0>
irb(main):003:0> user = User.create!(values: [gemspec])
(0.5ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
User Create (0.4ms) INSERT INTO "users" ("values", "created_at", "updated_at") VALUES (?, ?, ?) [["values", "---\n- !ruby/object:Gem::Specification\n name: test\n version:\n platform: ruby\n authors: []\n autorequire:\n bindir: bin\n cert_chain: []\n date: 2022-07-17 00:00:00.000000000 Z\n dependencies: []\n description:\n email:\n executables: []\n extensions: []\n extra_rdoc_files: []\n files: []\n homepage:\n licenses: []\n metadata: {}\n post_install_message:\n rdoc_options: []\n require_paths:\n - lib\n required_ruby_version: !ruby/object:Gem::Requirement\n requirements:\n - &1\n - \">=\"\n - !ruby/object:Gem::Version\n version: '0'\n required_rubygems_version: !ruby/object:Gem::Requirement\n requirements:\n - *1\n requirements: []\n rubygems_version: 3.3.7\n signing_key:\n specification_version: 4\n summary:\n test_files: []\n"], ["created_at", "2022-07-17 12:08:19.272191"], ["updated_at", "2022-07-17 12:08:19.272191"]]
TRANSACTION (0.9ms) commit transaction
=>
#<User:0x00000001171ea3a8
...
irb(main):004:0>
irb(main):005:0> User.last.values
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
=>
[Gem::Specification.new do |s|
s.name = "test"
s.version = nil
s.installed_by_version = Gem::Version.new("0")
s.date = Time.utc(2022, 7, 17)
s.require_paths = ["lib"]
s.rubygems_version = "3.3.7"
s.specification_version = 4
s.summary = nil
end]
```
7.0.3.1では保存、読み込みともにエラーになる
```ruby
❯ bundle exec rails c
Loading development environment (Rails 7.0.3.1)
irb(main):001:0> User.last.values
(1.0ms) SELECT sqlite_version(*)
User Load (0.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
.../psych/class_loader.rb:99:in `find': Tried to load unspecified class: Gem::Specification (Psych::DisallowedClass)
irb(main):002:0> gemspec = Gem::Specification.new("test")
=>
Gem::Specification.new do |s|
...
irb(main):003:0> user = User.create!(values: [gemspec])
TRANSACTION (0.1ms) begin transaction
TRANSACTION (0.0ms) rollback transaction
.../psych/class_loader.rb:99:in `find': Tried to load unspecified class: Gem::Specification (Psych::DisallowedClass)
.../psych/class_loader.rb:99:in `find': Tried to load unspecified class: Gem::Specification (Psych::DisallowedClass)
```
7.0.3であっても`ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy`のインスタンスを保存しようとするとエラーとなってうまく行かない
```ruby
.../psych/visitors/emitter.rb:32:in `scalar': wrong argument type ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy (expected String) (TypeError)
```
### シンボルの保存
7.0.3では保存できる
```ruby
❯ bundle exec rails c
Loading development environment (Rails 7.0.3)
irb(main):001:0> User.create(values: [{a: 2}])
(1.8ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
User Create (0.5ms) INSERT INTO "users" ("values", "created_at", "updated_at") VALUES (?, ?, ?) [["values", "---\n- :a: 2\n"], ["created_at", "2022-07-17 08:03:26.435339"], ["updated_at", "2022-07-17 08:03:26.435339"]]
TRANSACTION (0.3ms) commit transaction
=>
#<User:0x00000001138835d8
id: 5,
values: [{:a=>2}],
created_at: Sun, 17 Jul 2022 08:03:26.435339000 UTC +00:00,
updated_at: Sun, 17 Jul 2022 08:03:26.435339000 UTC +00:00>
```
7.0.3.1では保存できない
```ruby
irb(main):002:0> User.create(values: [{a: 2}])
TRANSACTION (0.1ms) begin transaction
TRANSACTION (0.1ms) rollback transaction
.../psych/class_loader.rb:99:in `find': Tried to load unspecified class: Symbol (Psych::DisallowedClass)
.../psych/class_loader.rb:99:in `find': Tried to load unspecified class: Symbol (Psych::DisallowedClass)
```
---
### delayed_job
- https://github.com/collectiveidea/delayed_job_active_record
```
bundle install
rails g delayed_job:active_record
rake db:migrate
```
```ruby
class TestJob < ApplicationJob
queue_as :default
def perform(*arg)
puts arg
end
end
```
jobの引数はYAMLとして保存され、delayed_jobにより追加された`YAML.load_dj`でYAMLが読み込まれる
https://github.com/collectiveidea/delayed_job/blob/v4.1.10/lib/delayed/psych_ext.rb#L15
```ruby
irb(main):001:0> TestJob.perform_later({a: 2})
(0.6ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
Delayed::Backend::ActiveRecord::Job Create (0.8ms) INSERT INTO "delayed_jobs" ("priority", "attempts", "handler", "last_error", "run_at", "locked_at", "failed_at", "locked_by", "queue", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["priority", 0], ["attempts", 0], ["handler", "--- !ruby/object:ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper\njob_data:\n job_class: TestJob\n job_id: bb5c0a70-0ee8-4435-8e1c-2fc934f90c38\n provider_job_id:\n queue_name: default\n priority:\n arguments:\n - a: 2\n _aj_symbol_keys:\n - a\n executions: 0\n exception_executions: {}\n locale: en\n timezone: UTC\n enqueued_at: '2022-08-06T05:24:25Z'\n"], ["last_error", nil], ["run_at", "2022-08-06 05:24:25.560338"], ["locked_at", nil], ["failed_at", nil], ["locked_by", nil], ["queue", "default"], ["created_at", "2022-08-06 05:24:25.560377"], ["updated_at", "2022-08-06 05:24:25.560377"]]
TRANSACTION (0.4ms) commit transaction
```
delayed_jobのシリアライズはActiveRecordのものではないため、Railsのバージョンの影響を受けず、クラスが制限されない
```ruby
❯ bundle exec rails c
Loading development environment (Rails 7.0.3.1)
irb(main):001:0> TestJob.perform_now("test")
Performing TestJob (Job ID: 350a9420-0609-4b82-b860-d01be744dfaa) from DelayedJob(default) enqueued at with arguments: "test"
test
Performed TestJob (Job ID: 350a9420-0609-4b82-b860-d01be744dfaa) from DelayedJob(default) in 1.66ms
=> nil
irb(main):002:0> gemspec = Gem::Specification.new("test")
irb(main):003:0> TestJob.perform_now(gemspec)
Performing TestJob (Job ID: 35ca24aa-18e4-4477-a92f-2247621c211e) from DelayedJob(default) enqueued at with arguments: #<Gem::Specification:...
#<Gem::Specification name=test version=>
Performed TestJob (Job ID: 35ca24aa-18e4-4477-a92f-2247621c211e) from DelayedJob(default) in 0.73ms
=> nil
irb(main):004:0>
```
File Snapshot
[4.0K] /data/pocs/430abe888ef2d9ac143f90a4fd983e1d823012d4
├── [4.0K] app
│ ├── [4.0K] assets
│ │ ├── [4.0K] config
│ │ │ └── [ 63] manifest.js
│ │ ├── [4.0K] images
│ │ └── [4.0K] stylesheets
│ │ └── [ 721] application.css
│ ├── [4.0K] controllers
│ │ ├── [ 57] application_controller.rb
│ │ └── [4.0K] concerns
│ ├── [4.0K] helpers
│ │ └── [ 29] application_helper.rb
│ ├── [4.0K] jobs
│ │ ├── [ 269] application_job.rb
│ │ └── [ 94] test_job.rb
│ ├── [4.0K] models
│ │ ├── [ 74] application_record.rb
│ │ ├── [4.0K] concerns
│ │ └── [ 62] user.rb
│ └── [4.0K] views
│ └── [4.0K] layouts
│ └── [ 292] application.html.erb
├── [4.0K] bin
│ ├── [ 175] delayed_job
│ ├── [ 141] rails
│ ├── [ 90] rake
│ └── [1010] setup
├── [4.0K] config
│ ├── [1.2K] application.rb
│ ├── [ 207] boot.rb
│ ├── [ 464] credentials.yml.enc
│ ├── [ 620] database.yml
│ ├── [ 128] environment.rb
│ ├── [4.0K] environments
│ │ ├── [2.2K] development.rb
│ │ ├── [3.3K] production.rb
│ │ └── [2.1K] test.rb
│ ├── [4.0K] initializers
│ │ ├── [ 504] assets.rb
│ │ ├── [1.0K] content_security_policy.rb
│ │ ├── [ 396] filter_parameter_logging.rb
│ │ ├── [ 649] inflections.rb
│ │ └── [ 384] permissions_policy.rb
│ ├── [4.0K] locales
│ │ └── [ 849] en.yml
│ ├── [1.8K] puma.rb
│ ├── [ 196] routes.rb
│ └── [1.1K] storage.yml
├── [ 160] config.ru
├── [4.0K] db
│ ├── [4.0K] migrate
│ │ ├── [ 154] 20220717043019_create_users.rb
│ │ └── [1.3K] 20220806051757_create_delayed_jobs.rb
│ ├── [1.4K] schema.rb
│ └── [ 374] seeds.rb
├── [ 859] Gemfile
├── [4.2K] Gemfile.lock
├── [4.0K] lib
│ ├── [4.0K] assets
│ └── [4.0K] tasks
├── [4.0K] log
├── [4.0K] public
│ ├── [1.7K] 404.html
│ ├── [1.7K] 422.html
│ ├── [1.6K] 500.html
│ ├── [ 0] apple-touch-icon.png
│ ├── [ 0] apple-touch-icon-precomposed.png
│ ├── [ 0] favicon.ico
│ └── [ 99] robots.txt
├── [ 227] Rakefile
├── [ 11K] README.md
├── [4.0K] storage
└── [4.0K] tmp
27 directories, 46 files
Remarks
1. It is advised to access via the original source first.
2. If the original source is unavailable, please email f.jinxu#gmail.com for a local snapshot (replace # with @).
3. Shenlong has snapshotted the POC code for you. To support long-term maintenance, please consider donating. Thank you for your support.