第一部分:Duo Security Web SDK的一个格式化注入漏洞

翻译自 http://sakurity.com/blog/2015/03/03/duo_format_injection.html

格式化注入和SQL注入有些类似,但是不是通过用户输入的单引号'来改变查询,而是打破自定义的分隔符/:|,;&来修改签名数据。

下面是Duo Security的web集成产品的工作原理:

duo.png

这个系统即使在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=victimexp=12345678

app使用victim||9999999999|IKEY|12345678为攻击者签名,user, ikey, exp = string.split('|')返回user=victimexp=9999999999(token是永远有效的了)。

更多的例子:

{
"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的调用流程是:

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。这是一个很棒的例子展示了一些库或者特性本来想增加安全性,但是实际上因为了安全漏洞的。

smsauthy.png

是的,攻击者只要简简单单的在任何使用authy的产品的网站填写../sms就能绕过两步验证了。