目录
- 引言
- session
- CSRF
- 真相大白
- 破解?
一、引言
这段时间一直在想,Rails 的 CSRF token 要破解不是很简单么?GET请求到网页拿到 token,随后发送恶意POST请求到服务器不就破解了么?事实证明我还是 Too young too simple.
二、尝试
写了个程序在ruby中去GET其他服务器的网页,获取到token后,伪造恶意POST请求,附带token,结果402 invalid token
真实打开浏览器,获取网页中token,随后复制该token,在ruby中恶意伪造POST请求并附带token,结果还是402 invalid token
真实打开浏览器,获取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进行验证。
四、真相大白
- 验证时需要
session内的_csrf_token,而用ruby代码直接发送http请求时是不会附带cookies的,所以验证肯定不会通过,这也是上面尝试第一二步失败的原因。
- 使用
postman是会自带cookies发送过去的,因此验证通过。
Tips
附带cookies是浏览器的行为
五、破解?
这时有人会问,rails是开源的,有人看了源码之后,拿到网页的csrf_token,按照步骤一步步转换回非加密状态,不就破解了?
还是 too young!
在服务器是将session[_csrf_token]跟用户的csrf_token进行验证,就算你将csrf_token还原了也没用,还是需要session[_csrf_token],而session是经过secret加密过的,因为无法篡改session,因而也破解不了。