0×01 前言
因为现代浏览器的工作机制原因,造成一种WEB攻击形态的存在, 这种攻击形式叫做CSRF攻击,以往我们是从攻击角度分析这种攻击的原理和操作。这次我们给出攻击原理同时,给出CSRF在服务器端的防御的解决方案。 CSRF是现代WEB程序要面对的共通性问题,在很多流行的WEB框架中,都会将CSRF的问题直接在WEB框架层面解决。
我们先抛出CSRF这个问题,然后介绍基于时间与签名的防护手段,并且给出的这种防御手段的具体代码实现。 过程中使用了Lua语言进行实现功能, LUA是一种容易理解的脚本语言,大家可以把Lua代码直接看成的伪语言描述工具,对于实现代码核心的加密函数,sha、base64这种函数,各大语言库都有支持, 如果各位老师想实践一下这种基于时间和签名的算法处理流程,本文最后给出了Lua平台上,各种对这些主要加密函数支持的库,老师们可以实际操作调试LUA代码,更深的体会防护处理流程。
0×02 CSRF攻击原理
CSRF概念:CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解:
攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。 如下:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户。
CSRF攻击原理及过程如下:
1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
0×03 CSRF防御原理
CSRF防护的一个重点是要对“用户凭证”进行校验处理,通过这种机制可以对用户的请求是合法进行判断,判断是不是跨站攻击的行为。因为“用户凭证”是Cookie中存储的,所以防护机制的处理对像也是Cookie的数据,我们要在防护的数据中加入签名校验,并对数据进行生命周期时间管理,就是数据过期管理。
Lapis框架是一种基于Moonscript语言开发的WEB框架,框架中有一段针对CSRF(Cross—Site Request Forgery)的防护代码, 是一种围绕时间戳和签名验证的CSRF防护设计,后来Lapis的作者Leafo老师还更新了CSRF的处理代吗:
Changes
Replaced the CSRF implementation, removed the key parameter and replaced with it randomly generated string stored in cookie.
跨站攻击的本质是, 攻击者拿着你的“身份凭证”,冒充你进行的相关攻击行为。
为了防止CSRF的发生,创建Token处理机制,Token数据结构与时间、加密签名直接相关, 这么设计的的目的如上所说,是给“身份凭证”加上时间生存周期管理和签名校验管理,如果的凭证被人拿到了, 要先判断Token中的“签名”与时间戳是否都有效,再进行正常的业务处理, 这样通过对非法数据的校验过滤,来降低CSRF攻击的成功率。
0×04 签名与时间戳防护处理流程
Token产生
1.Token构成
为了防止CSRF攻击,Token要求不能重复,需要含有时间戳信息、签名信息。
下面的图描述了一个token的数据构成:
Token的数据结构。
-----------------------------------------------------------------------------
| msg | separator | signature |
-----------------------------------------------------------------------------
| key | timestamp | . | Base64(sha256(msg)) |
-----------------------------------------------------------------------------
token由三部分组成:
a). 消息[msg]:而msg本身也有两部分组成:一部分:随机字符串,过期时间戳。
b). 分割符[separator]:用于分隔msg部分与加密后生成的signature签名部分,这里用的是”.“
c). 签名[signature]:signature。signature签名,是对“msg消息”用特定算法进行加密后的串。
token = base64(msg)格式化..base64(sha256("秘锁", msg))
Token由被Base64的msg编码串+先256加密msg再进行Base64编码,两个串的内容结合。
2.Token的加密
首先,是按照合适的加密方法对数据进行加密。这里我们通用的就使用了sha256散列算法,然后进行BASE64的格式转换。然后,我们需要在token串中隐含过期时间的设定,这种机制要保证,每条与服务器交互的Token有过期时间控制,一旦token过期服务器不处理请求。
3.Token的验证校验
当用户向服务提出访问请求时,产生Token再提交给服务器的时候,服务器需要判断token的有效性(是否过期,签名有效),一旦传向服务器的请求中的Token异常,就可以判定是可疑行为不做处理,返回异常提示。
Token校验
a. Token解包
先把接受到的token,进行分解,“.”为分隔符,分为msg部分+signature签名部分。
b. 比对签名
对msg部分的base64码反向decode_base64(msg)解码,在对解码后的msg明文,进行同样的encode_base64(sha256(msg))签名串转换处理。如果秘锁相同,判断加密后的数据和客户端传过来的token.signature的部分是否一致。如果一致,说明这个token是有效的。
c. 判断时间过期
如果签名有效的,取出msg中的timestamp字段数据,与当前系统时间进行比较,如果过期时间小于当前时间,那这个token是过期的,需要重新的取得token。
0×05 流程实现
文字版本的防护原理上面讲了,下面我们将整个防护流程分解成函数实现, 直接通过代码的形式来看实现,其实比看文字描述更简单。
Lua代码如下:
local gen_token = function(key, expires)
--做成一个过期时间戳。
if expires == nil then
expires = os.time() + 60 + 60 * 8
end
--对msg部分进行base64编码。
local msg = encode_base64(
json.encode({
key = key,
expires = expires
}))
--进行sha256哈希。
local signature = encode_base64(hmac_sha256('testkey', msg))
--拼接成一条token。
return msg .. "." ..signature
end
local val_token = function(key,token)
--对输入数据的判空操作
if not (token) then
return nil, 'mssing csrf token'
end
--对token的msg部分,signature签名部分进行拆分。
local msg, sig = token:match("^(.*)%.(.*)$")
if not (msg) then
return nil, "malformed csrf token"
end
sig = encoding.decode_base64(sig)
--对解包后msg,按照相同的加密key:"testkey",重新进行sha256哈希,比对signature,
--如果不一致,说明这个token中的数据有问题,无效的token。
if not (sig == hmac_sha256('testkey', msg)) then
return nil, "invalid csrf token(bad sig)"
end
--对msg进行base64解码,判断其中的key和传入的key是否一致。
--如果不一致说明token也是无效的。
msg =json.decode(decode_base64(msg))
if not (msg.key == key) then
return nil, "invalid csrf token (bad key)"
end
--取出msg部分的时间戳,判断是否大于当前时间,如果大于,说明token过期无效了。
if not (not msg.expires or msg.expires > os.time()) then
return nil, "csrf token expired"
end
end
因为本文提到的 CSRF防护,是Moonscript实现的,最后翻译成Lua语言, 而用的Token编码的函数与signature签名用的加密算法,也都是基于Lua库,所以下面列出了这些常用的库的相关信息。
库一览列表:
http://lua-users.org/wiki/CryptographyStuff
0×06 核心安全算法库
要实现上文所说的Token机制,要有库函数Bash64与sha256加密的工具包库支持。
1.SecureHashAlgorithm和SecureHashAlgorithmBW
这个工具包是支持sha256加密的,而且是纯lua方法的实现,问题是,这两个包分别依赖lua5.2和lua5.3。
大部分老系统的运行环境是lua5.1,因为大部分的生产环境都是lua5.1,因为历史原因暂时没法改变。如果要把5.2的程序移植到5.1下运行,还需要移植一个lua5.2才独有的包,这是lua5.2升级之后才有的部件:bit32,而在lua5.3中又将这个部件去掉了,移植的动力不大,lua5.1的用户可以考虑使用其他的库。
2.Lcrypt
这个包不是纯lua的实现,底层加密用的是C语言,而且额外还有依赖另外另个工具包 libTomCrypt和libTomMath,github上有源码,所以要想让这个包正常运行需要手动make安装3个源码工程。
网站:
http://www.eder.us/projects/lcrypt/
3.LuaCrypto
这个包的安装用的是luarocks,就比较简单了
luarocks install luacrypto
我们选用这个包进行加密处理。LuaCrypto其实是openssl库的前端lua调用,依赖openssl,openssl库显然会支持sha256加密,相对也比一般的第三方实现更可靠。写一个简单的加密程序:
local crypto = require("crypto")
local hmac = require("crypto.hmac")
local ret = hmac.digest("sha256", "abcdefg", "hmackey")
print(ret)
ret的返回结果是,如下这个字符串。
704d25d116a700656bfa5a6a7b0f462efdc7df828cdbafa6fbf8b39a12e83f24
我们需要改造一下代码,在调用digest的时候指定输出的形式是raw二进制数据形式,然后在编码成base64的数据形式。
local ret = hmac.digest("sha256", "abcdefg", "hmackey",rawequal)
print(ret)
这时候的输出结果是:
cE0l0RanAGVr+lpqew9GLv3H34KM26+m+/izmhLoPyQ=
lua-base64
使用的是下面的库,lua库就是这样,有很多功能程序有很多的实现,并且很多非官方的第三方实现。
https://github.com/toastdriven/lua-base64
关于库的部分就介绍这些,可以找一个可运行lua的运行环境实践这个处理过程。
0×07 总结
现在随着对安全的重视,过去的攻击行为都会遇到新的防御方法。对抗在此消彼长的过程中动态变化,开发工程师应该比过去任何时候都应该注意自己程序输入数据的有效性, 而渗透攻击也不是一成不变,如果防护采用新的手段,也需要渗透人员了解防御背后的工作原理,从新的角度发现问题,只要是人写的程序可能都会不同成度的出现漏洞,都会因为软件之间不得不产生的依赖,而彼此关联, 安全问题从攻击与防御的双重角度去了解,才能适应未来动态变化的安全情态。