Rails-CSRF-Explore

目录

  1. 引言
  2. session
  3. CSRF
  4. 真相大白
  5. 破解?

一、引言

这段时间一直在想,Rails 的 CSRF token 要破解不是很简单么?GET请求到网页拿到 token,随后发送恶意POST请求到服务器不就破解了么?事实证明我还是 Too young too simple.

二、尝试

  1. 写了个程序在ruby中去GET其他服务器的网页,获取到token后,伪造恶意POST请求,附带token,结果402 invalid token

  2. 真实打开浏览器,获取网页中token,随后复制该token,在ruby中恶意伪造POST请求并附带token,结果还是402 invalid token

  3. 真实打开浏览器,获取token,打开postman,发送POST请求。成功200

这是为什么呢?

二、session

在 Rails 中,默认的session存储方式是:ActionDispatch::Session::CookieStore

也就是默认将session的内容存放到客户端cookie中。

1
2
_glassx_session:
TE9xZ3Zud1dxRFYxdEtSek5mYldTMkpnZ1NWUlF5SjdzODVRSkJYNGN6VmNDc1VGb1lzSGJPU0FLYWhoMU5ZSHZCeXUwNTFWdWFQaWpKZmZSUC96c0dWbFdDcmlZK3RTcENabXZoaFVScWx1SWlxR1dEbmcwU1BXSDBZWUVOVW1EN0ZscmZpWkJsOFBZajZST0Z3VWxJM09NOGVTRFp2djRyYVB4SVJZNkVWRlM4dmw0TVNYb01jOGJYdVRXKzYxY1pyeXNtT0VkVWZ4YjZFcTdVU1FLVEU2aXlXbGRIOWY3c0Q1THlPRGtWeFhOb1BCd1M0S3hOQ04xTHNSajh3MC0tS3I0UVZkK2tuWGx0d1BDSVp3b1diZz09--cd6094c15ae50026d0377b6c32b7e0986b447d74

session中的数据保存在cookie中时是先marshal后,然后利用密码来加密的。

1
marshal(data)---digest_with_secret_key(marshal(data))

这里所用secret key则是我们在config/secrets.yml设置的。

加密的原因是防止 session 被偷看,并且防止 session 被修改

三、CSRF

我们在访问服务器的网页时,会找到这样的 token

1
2
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="yT5/q+GxMDy96ISmdNfE4esNrc8YZOBUrQefXO21tJ19iGD1XjRkJ2/ELC1A952U5qDp3vpo6MhhHQB8fOmivw==">

token是服务器生成后返回的。

具体流程是:

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
# 生成加密的token(也就是我们在网页上见到的csrf-token)
def masked_authenticity_token(session)
  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
  encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
  masked_token = one_time_pad + encrypted_csrf_token
  Base64.strict_encode64(masked_token)
end

# 生成 _csrf_token,并且将其放入`session[:_csrf_token]`中
def real_csrf_token(session)
  session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
  Base64.strict_decode64(session[:_csrf_token])
end

# 验证 token 是否有效(这段代码我简化过)
def valid_authenticity_token?(session, encoded_masked_token)
  masked_token = Base64.strict_decode64(encoded_masked_token)
  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
       compare_with_real_token masked_token, session
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
     one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
       encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
       csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)

       compare_with_real_token csrf_token, session
    end
end

# 开始验证
def compare_with_real_token(unmasked_token, session)
   ActiveSupport::SecurityUtils.secure_compare(unmasked_token, real_csrf_token(session))
end

从源码可以看出,服务器会将生成的_csrf_token放入用户的session中,当客户端将csrf_token随着request发送过来时,服务器会将csrf_token转换回非加密,随后与session中的_csrf_token进行验证。

四、真相大白

  1. 验证时需要session内的_csrf_token,而用ruby代码直接发送http请求时是不会附带cookies的,所以验证肯定不会通过,这也是上面尝试第一二步失败的原因。
  2. 使用postman是会自带cookies发送过去的,因此验证通过。

Tips

附带cookies是浏览器的行为

五、破解?

这时有人会问,rails是开源的,有人看了源码之后,拿到网页的csrf_token,按照步骤一步步转换回非加密状态,不就破解了?

还是 too young!

在服务器是将session[_csrf_token]跟用户的csrf_token进行验证,就算你将csrf_token还原了也没用,还是需要session[_csrf_token],而session是经过secret加密过的,因为无法篡改session,因而也破解不了。