第一部分:Duo Security Web SDK的一个格式化注入漏洞
翻译自 http://sakurity.com/blog/2015/03/03/duo_format_injection.html
格式化注入和SQL注入有些类似,但是不是通过用户输入的单引号'
来改变查询,而是打破自定义的分隔符/:|,;&
来修改签名数据。
下面是Duo Security的web集成产品的工作原理:
- 用户在客户端使用有效用户名和密码登陆,然后收到用来请求二步验证TX token和代表通过第一步的验证APP token。
- 现在用户使用TX token来通过Duo API(使用Duo的推送、短信或者电话)换取AUTH token。AUTH token代表用户可以通过了第二步的验证。
- 之前获取的APP token和AUTH token传递到
/final_login
,然后验证两个token是有效的,而且是属于这个用户的。verify_response
响应返回username
,然后你现在就可以以这个人的身份登录了。
这个系统即使在SKEY被泄露了的情况下,攻击者也不能登录账号,因为他没有你的AKEY,还有他没办法伪造一个有效的APP token。但是我们发现一个Duo在对APP token签名的时候的一个格式化漏洞。
Duo安全部门已经确定这个问题存在于特定版本的Duo Web SDK里面,在他们获取到Duo集成产品的secret key的时候,这可能导致攻击者绕过两步验证,然后可以创建包含管道符
|
的有效用户名。 注意:这个漏洞不影响任何官方的产品,它只影响使用部分受影响的Web SDK的客户自助集成的产品。
安全风险是低,这是因为要登录进入一个账号,你仍然需要一个有效的AUTH token,这意味着你必须知道你的SKEY。如果你的应用受到影响,请马上重置你的AKEY。
受影响的都是使用Ruby,PHP,Perl,Java和ColdFusion SDK的。用户名处可以出现管道符,同时用户名也是被用作为Duo ID的(这个可以是用户id或者邮箱,这个不能使用管道符)。
看下面的这段Ruby代码
require 'base64'
require 'openssl'
def hmac_sha1(key, data)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), key, data.to_s)
end
def sign_vals(key, vals, prefix, expire)
exp = Time.now.to_i + expire
val_list = vals + [exp]
val = val_list.join('|')
b64 = Base64.encode64(val).gsub(/\n/,'')
cookie = prefix + '|' + b64
sig = hmac_sha1(key, cookie)
return [cookie, sig].join('|')
end
def parse_vals(key, val, prefix)
ts = Time.now.to_i
u_prefix, u_b64, u_sig = val.to_s.split('|')
sig = hmac_sha1(key, [u_prefix, u_b64].join('|'))
return nil if hmac_sha1(key, sig) != hmac_sha1(key, u_sig)
return nil if u_prefix != prefix
user, ikey, exp = Base64.decode64(u_b64).to_s.split('|')
return nil if ts >= exp.to_i
return user
end
如果你想创建一个用户名是victim||9999999999
的用户,我们获取到的APP token会和用户名叫victim
的解析出一样的结果。
sig1 = sign_vals('AKEY',['victim','IKEY'],'APP',3600)
puts parse_vals('AKEY', sig1, 'APP') #returns 'victim'
sig2 = sign_vals('AKEY',['victim||9999999999','IKEY'],'APP',3600)
puts parse_vals('AKEY', sig2, 'APP') #returns 'victim' too
如果你仍然没看懂的话,就仔细看下面的。
app使用victim|IKEY|12345678
为受害者签名,user, ikey, exp = string.split('|')
返回的是user=victim
和exp=12345678
。
app使用victim||9999999999|IKEY|12345678
为攻击者签名,user, ikey, exp = string.split('|')
返回user=victim
和exp=9999999999
(token是永远有效的了)。
更多的例子:
-
像是这样的字符串
val+DELIMITER+user_input+DELIMITER+...
或者[user_input,val2,val3].join(':')
的用法,都容易出现格式化注入漏洞。 -
在一些api和oatuh里面,
openURL('http://oauth/?client_id=1&client_secret=2&code='+params[:unescaped_code])
,可以使用code=&client_id=new_client_id&client_secret=new_client_secret
的方法替换掉客户端凭据,导致验证绕过。 -
'{"val":"'+user_input+'"}'
或者'<xml>'+user_input+'</xml>'
,因为xss也是格式化漏洞的一种。 -
很多支付网关也是使用了自定义的数据格式。这是Liqpay和WalletOne对订单签名的方法(没有分隔符,只是按照字母顺序排序然后组合)
{
"WMI_MERCHANT_ID"=>"119175088534",
"WMI_PAYMENT_AMOUNT"=>"100.00",
"WMI_CURRENCY_ID" => "643",
"WMI_PAYMENT_NO" => "12345-001",
"WMI_DESCRIPTION" => "BASE64:f",
"WMI_EXPIRED_DATE" => "2019-12-31T23:59:59",
"WMI_SUCCESS_URL" => "https://myshop.com/w1/success.php",
"WMI_FAIL_URL" => "https://myshop.com/w1/fail.php"
}.sort.map{|key,value| value}.join
# # => 643BASE64:f2019-12-31T23:59:59https://myshop.com/w1/fail.php119175088534100.0012345-001https://myshop.com/w1/success.php
第二部分:../sms
是怎么绕过Authy的两步验证的
翻译自 http://sakurity.com/blog/2015/03/15/authy_bypass.html
API的调用流程是:
- 客户端请求新的token
http://api.authy.com/protected/json/sms/AUTHY_ID?api_key=KEY
,AUTHY_ID是公开的和当前用户关联的凭据,期望的响应是{"success":true,"message":"SMS token was sent","cellphone":"+1-XXX-XXX-XX85"}
,http status是200。 - 用户输入token,然后验证这个token是否有效,url是
http://api.authy.com/protected/json/verify/用户输入的token/AUTHY_ID?api_key=KEY
,如果验证通过,就返回{"success":true,"message":"Token is valid.","token":"is valid"}
,http status是200。
Authy-node没有编码用户输入的token
在authy-node里面有一个问题:用户输入的token没有进行url编码,代码是this._request("get", "/protected/json/verify/" + token + "/" + id, {}, callback, qs);
这就意味着如果我们输入VALID_TOKEN_FOR_OTHER_AUTHY_ID/OTHER_AUTH_ID#
,我们就能修改掉之前的那个路径,然后让客户端发出/protected/json/verify/VALID_TOKEN_FOR_OTHER_AUTHY_ID/OTHER_AUTH_ID#/AUTH_ID
这样的请求。因为#
之后的会被忽略掉,实际上响应的是/protected/json/verify/VALID_TOKEN_FOR_OTHER_AUTHY_ID/OTHER_AUTH_ID?api_key=KEY
,这样就让攻击者登录进来了。
在服务器端根本没办法分辨伪造的请求,因为#/AUTHY_ID
就根本没有发送过去。
Authy-python也有漏洞
然后我注意到Python的urllib.quote方法没有编码/
,但是由于某种原因,它编码了斜线以为的所有的字符,而且在文档上就是这么说的。urllib.quote("#?&=/")
返回的是%23%3F%26%3D/
。这就以为着我们的../sms
不会被编码。
当浏览器解析/../
,/%2e%2e/
,甚至/%252e%252e/
的时候,就会进入到上一个文件夹,到那时服务器不会。不管怎么,我尝试了一下,而且可以工作:Authy的api会把/../
之前的文件夹移除。
这就引入了一个路径遍历漏洞,可以让攻击者更加容易的去攻击。你只需要输入../sms
就能把/verify
请求转到/sms
上(/verify/../sms/authy_id
),然后返回的http status是200,就绕过了两步验证。
等等,貌似所有的人都受影响
几个小时以后我意识到目录遍历是怎么造成的了,我刚刚看了Daniel’s interview on Authy,然后知道了他们在使用Sinatra,默认使用rack-protection的。
我发现貌似进行url编码也是徒劳的,rack-protection中的path_traversal模块会将%2f
再解码为/
!这样就会影响所有运行Sinatra和在url中获取参数的api。这是一个很棒的例子展示了一些库或者特性本来想增加安全性,但是实际上因为了安全漏洞的。
- 攻击者将
../sms
填入短信验证码文本框。 - 客户端将其编码为
..%2fsms
,然后调用Authy的apihttps://api.authy.com/protected/json/verify/..%2fsms/authy_id
- 目录遍历中间件将路径解码为
https://api.authy.com/protected/json/verify/../sms/authy_id
,通过斜线分割,然后移除了/..
之前的目录。 - 实际上Authy api看到的路径是
https://api.authy.com/protected/json/sms/authy_id
,这样就把另一条短信发给了authy_id(攻击者),然后相应200,内容是{"success":true,"message":"SMS token was sent","cellphone":"+1-XXX-XXX-XX85"}
- 所有的authy sdk都把http status 200认为是一个成功的响应,然后让攻击者登录进来。即使有的客户自己集成的产品是看响应里面的
"success":"true"
,但是我们/sms
响应里面也是有这个的。所以唯一的安全的方法就是看响应里面有没有"token":"is valid"
是的,攻击者只要简简单单的在任何使用authy的产品的网站填写../sms
就能绕过两步验证了。