写在前面:之前在设计授权系统的时候,使用了 openssl 的公私钥方式,但是 Rails 中是如何对关键信息做加解密解密处理呢?今天我们先探究下Rails中的加密和解密。
提到的内容:
- Rails 的 key 和 credentials.yml.enc
- 两个类,MessageVerifier 和 MessageEncryptor
- 签名 cookie 和加密 cookie
- 如何读取加密的内容
一、Rails 中的 key 和 secret_key_base
在初始化 Rails 项目的时候,框架会自动创建一个被 git ignore 的文件,config 下的 master.key。而且初始化结束后,总会提示,这个 key 千万不能丢。同时,项目还会创建一个 credentials.yml.enc 文件,它是可以放入版本库的,如果使用 `EDITOR="vim" rails credentials:edit` ,你会看到它的内容。
# aws:
# 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: 19b57cbd6b06......
保存后,这些内容会被写入到 credentials.yml.enc 文件中。网上很多文章都写到了这里就止住了。问题是,它是如何被加密的呢?
其实,Rails内部存在自己的加密和解密的方法,它使用 master.key 对这个文件进行加密,生成凭证文件 (credentials.yml.enc),并且使用 master.key 来解密这个文件,取出里面的 secret_key_base 并保存到运行环境中,随时使用。如果你使用这几个命令可以看到它的存在。
2.6.6 :002 > Rails.application.credentials.secret_key_base
=> "19b57c..."(注意这里)
要注意的是,这里取出的是 SecureRandom.hex(64) 生成的随机字串(我也是写的时候才发现),和后面所用的 secret 值是不同的。但是,他们名字相同。(起名很重要啊)
2.6.6 :002 > Rails.application.secrets
=> {:secret_key_base=>"53c8b81...", :secret_token=>nil}
2.6.6 :003 > Rails.application.secret_key_base
=> "53c8b818....."(注意这里,和上面的不同哦)
这个 secret_key_base 后面还会用到,它会作为生成加密结果的 secret 使用。如果丢失了 master.key,后果将是无法解密凭证文件。
同时,也可以把一些需要加密的数据写到这里,比如数据库连接密码等等。
2.6.6 :006 > Rails.application.credentials.mysql_password
=> "iampassword"
二、MessageVerifier 和 MessageEncryptor
MessageVerifier 是用来校验加密信息是否被篡改的,MessageEncryptor 用来加密数据。
举一个简单的例子:
2.6.6 :007 > verifier = ActiveSupport::MessageVerifier.new 'iamSecret'
=> #<ActiveSupport::MessageVerifier:0x00007ff59c166068 @secret="iamSalt", @digest="SHA1", @serializer=Marshal, @options={}, @rotations=[]>
2.6.6 :008 > signed_message = verifier.generate 'a private message'
=> "BAhJIhZhIHByaXZhdGUgbWVzc2FnZQY6BkVU--0e06f6b8d9c1ad194eecdade533e3d4f476c763e"
2.6.6 :009 > verifier.verified(signed_message)
=> "a private message"
这个例子里,初始化的时候用了 iamSecret 这个字符串作为 secret,这就像是弱密码一样容易被猜到,Rails 会用 secret_key_base (53c8开头的那个)来作为 secret。
在处理敏感数据的时候,通常有2种方法,指纹和加密。指纹是指数据可读,但是不可篡改。加密就是数据不可读,更不可篡改了。
在Rails中,常见用这两种方法处理 cookie 里的数据。通常,我们写入cookie 的内容是直接可读的,比如下图
对于签名 cookie 和加密 cookie,不可直接读取。
三、签名 cookie
签名 cookie 是这种结构。
InRvbSI%3D--7c000522c18ecc11013cf2512e51efc87e5fac59
前后两部分由 -- 分隔,前面是加密后的内容,后面是签名字串。
只需要对前面部分做一次 URL decode 和 Base64 解码即可看到原文。
Burp截图
2.6.6 :052 > Base64.strict_encode64("\"tom\"")
=> "InRvbSI="
strict_encode64 不会增加换行符,encode64 会在60个长度字符后增加换行符(\n),并且在最后增加一个换行符(\n)。
后面的签名可以校验前面的内容是否被篡改,Rails 使用的是 MessageVerifier,我把方法单独写出来。
# 读取 cookie 的值
cookie_value = URI.unescape("InRvbSI%3D")
# 读取 secret
secret = Rails.application.secret_key_base
# 设置 key
key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret, "signed cookie", 1000, 64)
# 生成签名
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get("SHA1").new, key, cookie_value)
=> 7c000522c...(与浏览器比较为相同的加密结果)
四、加密 cookie
和签名cookie不同的是,加密cookie无法在外部读取内容,它由三部分组成,且每次刷新都会变化。签名 cookie 每次刷新是不变的。
AwIPXyc%3D--rlibe2Ci5biKE9wq--OOEw1gH0cHSbo0Ly5o1iOw%3D%3D
中间多出来的,叫 Initialization vector,在密码学里,它叫做初始化向量(英语:initialization vector,缩写为IV),或初向量,又或初始变量(starting variable,缩写为SV),是一个固定长度的输入值。一般的使用上会要求它是随机数或伪随机数(pseudorandom)。(来自百度百科)
Rails 使用 MessageEncryptor 来进行加密,我们来进行一次解密操作。
cipher = OpenSSL::Cipher.new("aes-256-gcm")
secret_key_base = Rails.application.secret_key_base
secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, "authenticated encrypted cookie", 1000, cipher.key_len)
data = Base64.strict_decode64(URI.unescape("AwIPXyc%3D"))
iv = Base64.strict_decode64(URI.unescape("rlibe2Ci5biKE9wq"))
auth_tag = Base64.strict_decode64(URI.unescape("OOEw1gH0cHSbo0Ly5o1iOw%3D%3D"))
cipher.decrypt
cipher.key = secret
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.auth_data = ""
puts cipher.update(data) # => "tom" (这是解密结果)
cipher.final
在收集资料的时候,这篇文章给了非常重要的参考,节省了大量时间,在此对作者表示感谢。
文章参考:https://binarysolo.chapter24.blog/demystifying-cookies-in-rails-6/
五、提高Rails的安全属性
在破解签名 cookie 和加密 cookie 的时候,所用的加密算法和salt是框架默认的,我们可以修改配置,建议在项目初始化的时候,把这几点做出修改,以提高项目安全性。
修改算法。
config.action_dispatch.encrypted_cookie_cipher # sets the cipher to be used for encrypted cookies. This defaults to "aes-256-gcm".
config.action_dispatch.signed_cookie_digest # sets the digest to be used for signed cookies. This defaults to "SHA1".
修改默认 salt。
config.action_dispatch.http_auth_salt # sets the HTTP Auth salt value. Defaults to 'http authentication'.
config.action_dispatch.signed_cookie_salt # sets the signed cookies salt value. Defaults to 'signed cookie'.
config.action_dispatch.encrypted_cookie_salt # sets the encrypted cookies salt value. Defaults to 'encrypted cookie'.
config.action_dispatch.encrypted_signed_cookie_salt # sets the signed encrypted cookies salt value. Defaults to 'signed encrypted cookie'.
config.action_dispatch.authenticated_encrypted_cookie_salt # sets the authenticated encrypted cookie salt. Defaults to 'authenticated encrypted cookie'.
六、自问自答
Q:为什么要花这么多时间研究和复现 Rails 的加解密?
A:在应用安全加固上,安全厂商提供了无侵入式的防护功能,比如动态加密,动态混淆,cookie防篡改,防暴库等功能。这些功能是在应用的外部来弥补应用自身的不足,见招拆招,打补丁。我一直在想,Rails自身的加密解密功能,是否可以提供相同的功能呢,所以先探究下 Rails 的加解密实现方案。
写在后面:今天爆出 Apache Log4j 存 在任意代码执行漏洞,该组件存在Java JNDI注入漏洞,当程序将用户输入的数据进行日志,即可触发此漏洞,成功利用此漏洞可以在目标服务器上执行任意代码。Apache Struts2、Apache Solr、Apache Druid、Apache Flink等众多组件与大型应用均受影响,鉴于此漏洞危害巨大,利用门槛极低。写在此处,当个日记吧。