标签归档:网络安全

HTTPS和SNI

问题:有两个站点架在同一台服务器上,并且强制HTTPS访问,地址分别为https://sub.domain.com和https://admin.sub.domain.com,HTTPS证书签名的域名分别为*.domain.com和*.sub.domain.com,使用HttpClient(4.1.1版本)去调用一个接口:

1
2
3
4
5
6
7
public class TestGet {
  public static void main(String args[]) throws Exception {
    HttpGet httpGet = new HttpGet("https://sub.domain.com/api/nodes");
    HttpClient httpClient = new DefaultHttpClient();
    httpClient.execute(httpGet);
  }
}

返回 javax.net.ssl.SSLException异常:

Exception in thread "main" javax.net.ssl.SSLException: hostname in certificate didn't match:  != <*.sub.domain.com> OR <*.sub.domain.com>
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:231)
    at org.apache.http.conn.ssl.BrowserCompatHostnameVerifier.verify(BrowserCompatHostnameVerifier.java:54)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:152)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:133)
    at org.apache.http.conn.ssl.SSLSocketFactory.verifyHostname(SSLSocketFactory.java:559)
    at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:534)
    ……

问题分析:从异常信息来看,显然是SSL连接握手阶段出现证书不匹配的问题,客户端请求的是sub.domain.com域名,服务器返回了*.sub.domain.com的证书。但是浏览器访问又正常,这是为何?

解决方法:故事要从IPv4的设计开始说起,设计者认为32位够用了,后续的上层协议设计者也纷纷假设一个IP提供一个服务的前提,比如HTTP/1.0、SSL等,一个大坑就这样被埋下。从90年代中期开始,互联网发展太快,人们开始意识到IP资源越来越稀缺,于是着手设计IPv6,但是从IPv4到IPv6需要一个很长的过渡期,远远大于IPv4地址耗尽的时间。于是一些IP复用的方案出现了,CIDR和NAT技术能在一定程度上缓和IPv4资源稀缺的问题,互联网协议也适时进行了升级,比如HTTP,在HTTP/1.1版本中加入Host头,这个头部在虚拟主机(Virtual Host)中非常重要,它支持不同站点架在一个IP上,客户端通过Host头告知服务器需要访问的站点。但是Host头无法解决HTTPS场景中客户端与虚拟主机(多站点共享同一个IP)之间加密连接的建立,因为HTTPS依赖的SSL/TLS协议并没有同步跟进,SSL握手阶段客户端向服务器端发送的信息中未包含Host,所以服务器端也就没法返回正确的HTTPS证书了。这样的背景下,解决HTTPS虚拟主机的方法主要有:

1. 绑定不同的端口。为不同的虚拟主机绑定不同的HTTPS端口(默认端口443)。缺点显而易见,每次访问都需要显式指定端口号。
2. 绑定不同的IP。前面说过,IPv4资源越来越稀缺,而且这种方法也违背了虚拟主机的初衷。
3. 购买泛域名SSL证书(Wildcard Certificate)。
4. 购买多域名SSL证书(Multi Domain Certificate)。

这些方法都只是绕过限制,要根本解决问题还是得修改SSL/TLS协议,所以SNI(Server Name Indication)就适时被提了出来,它扩展了TLS协议(SSL 3.0不支持,在TLS 1.0以后支持,RFC4366RFC6606),在客户端请求的CLIENTHELLO阶段加入Host信息,告知服务器端要与哪个主机建立加密连接。
SNI

图片来源

实际上,SNI需要通信双方(服务端、客户端)都支持,根据Wiki上的总结,目前一些常见浏览器、服务器软件、类库的支持情况如下:

浏览器:

IE 7+
Mozilla Firefox 2.0+
Opera 8.0+
Google Chrome 5.0.342.1+
Safari 3.0+

注:这里的支持度还跟操作系统相关,比如Windows XP上所有IE均不支持,Chrome在不同操作系统下开始支持的版本也不同。

服务器软件:

Apache 2.2.12+
Nginx(依赖支持sni的openssl库)
Apache Traffic Server 3.2.0+
HAProxy 1.5+

类库:

OpenSSL: 0.9.8f(compiled in with config option '--enable-tlsext'), 0.9.8j+
libcurl / cURL 7.18.1+
Python 3.2
Qt 4.8
Oracle Java 7 JSSE
Apache HttpComponents 4.3.2
wget 1.14
Android 4.2 (Jellybean MR1)
Go (client and server)

Java是在JDK1.7里才支持SNI的,因此要在Java应用里使用,前提就是将OpenJDK或者Oracle JDK升到1.7,如果同时使用HttpClient进行HTTP接口调用,那么还必须将HttpClient版本升到4.3.2及以后,这个JIRA单描述了如何支持SNI(代码设计层面)的前因后果。

回到开头的异常问题,可以通过以下步骤修复:

1. 升级JDK至少到1.7。
2. 升级HttpClient至少到4.3.2。
3. 更新少量代码如下:

1
2
3
4
5
6
7
8
public class TestGet {
  public static void main(String args[]) throws Exception {
    HttpGet httpGet = new HttpGet("https://sub.domain.com/api/nodes");
    //HttpClient httpClient = new DefaultHttpClient();
    CloseableHttpClient httpClient = HttpClients.createDefault();
    httpClient.execute(httpGet);
  }
}

--EOF--

SSH和Puppet的身份认证机制

互联网上最成熟的身份验证方式就是非对称加密和数字证书技术,它是互联网安全的基石,以此为基础的SSH和SSL/TLS也是不可信网络下进行安全通信的标准协议,可以有效避免通信数据被窃听、篡改和伪造。

这里简单总结下SSH远程登录和Puppet是如何进行通信双方的身份识别。

1. SSH远程登录

SSH的一个主要应用就是安全可靠地登录远程机器,所有数据经过加密传输。客户端和远程主机(服务端)在通信之前需要握手,进行身份验证。首先,服务端任性地向客户端发送自己的公钥PubKey,客户端没得选择,只能线下确认PubKey是否准确,然后接受。如果不进行确认就接受公钥,容易发生中间人攻击,严重的话会导致密码泄露。有了服务端公钥PubKey之后,客户端就要向对方表明自己的身份了,SSH提供了两种方式:第一种是密码登陆,客户端用PubKey加密密码后发送给服务端,服务端用私钥解密后进行身份验证;第二种是密钥登陆,客户端公钥事先存储在服务端,当用户选择密钥登陆时,服务端向客户端发送一个随机数,客户端用用户私钥对随机数加密后发回服务端,服务端将数据解密后得到的随机数如果与之前的一样,表示身份验证通过。密钥登陆能避免中间人攻击。

2. Puppet

Puppet是一个C/S架构的配置管理和部署工具。采用SSL证书对客户端和服务端之间的通信进行身份认证和数据加密传输。Puppet自己提供本地CA,用于签发客户端和服务端的证书,通常情况下,服务端进程会兼任本地CA的职责。当服务端初始化时,首先创建一对本地CA的公私钥,用于签发证书,然后为服务端创建一对公私钥和一张数字证书,这张证书包含了服务端的公钥和公钥签名(用CA私钥签),最后将这张证书分发到所有的客户端;当客户端初始化时,它生成一对公私钥,并且把公钥发送到本地CA(服务端),申请为这个公钥签发一张证书。本地CA(服务端)可以根据不同策略对客户端的申请请求进行确认,这样服务端就有了一张客户端的数字证书。此时,客户端持有了自己的私钥和服务端数字证书,服务端持有了自己的私钥和客户端数字证书,之后的通信就是SSL协议的内容了,双方首先通过本地CA的公钥认证对方数字证书和公钥的合法性,确认双方身份,然后通过非对称加密算法进行随机数的加密传送,推导出双方的会话密钥,最后通过这个会话密钥进行传输内容的对称加解密。

References:
[1]. 『Secure Shell』.
[2]. 『SSH原理与运用(一):远程登录』.
[3]. 『数字签名是什么?』.
[4]. 『SSL』.
[5]. 『SSL/TLS协议运行机制的概述』.
[6]. 『What's the difference between SSH and SSL/TLS?』.
[7]. 『Puppet Labs: Certificates and Security』.

--EOF--

理解Oauth2.0的设计

oauth2
上图是从网上找来的Web Server Flow类型的Oauth2.0流程,授权流程较为清晰。相比Oauth1.0,它已经精简了很多,但是仍旧是一个复杂的授权协议。

1. 为什么有authorization_code?

authorization_code在Oauth2.0中是用来跟Authorization Server换取access_token的。如果没有省略它,直接将上图中的(3)、(4)步骤合并,那么会存在两个问题:
1). access_token容易泄露:如果access_token由Authorization Server通过HTTP 302重定向的方式返回,容易被人通过嗅探、扫日志、扫浏览器记录等方式窃取。别人拿到这个access_token就可以根据授权scope任意存取Resource Server上的资源。要避免被窃取,势必要求第三方应用提供的redirect_uri是可信信道,例如https + POST请求,那就对第三方应用要求太高了。
2). 第三方应用(如上图中的facebook.com)身份难以确认:Authorization Server要确认第三方应用的身份,必然要求应用对步骤(1)的请求进行签名,这会加大协议复杂性。

加了authorization_code后,只要保证步骤(4)、(5)的信道是安全的即可,即便authorization_code在步骤(3)中泄露,别人拿去也没用,因为没法换取access_token。通过authorization_code换取access_token最简单的方式是使用https,由Authorization Server提供https支持,这样第三方应用直接将secret传过去即可,secret作为口令的方式使用。如果没有https链路,secret可以用来对请求进行签名。就不存在上面说的两个问题了。

注:User-Agent flow (Implicit Grant)类型的Oauth授权流程中没有authorization_code,access_token是直接通过步骤(3)的redirect_uri返回,所以安全性没有Web Server Flow类型的高。User-Agent flow一般用在没有服务器端的手机APP,浏览器插件(JS)中,这类应用难以向Authorization Server证明自己身份,因为secret会暴露在外,所以干脆取消authorization_code换access_token的流程。

2. 为什么在获取authorization_code时加入state参数可以避免CSRF攻击?

一般第三方应用在引导用户跳转到Authorization Server之前,先将生成的state参数(随机数)放入session中,然后通过步骤(1)传给Authorization Server,Authorization Server会原样返回,第三方应用得到Authorization Server返回的state参数后,跟session中的比对即可。

假设没有state参数,试想以下场景:有一个第三方应用给大家发红包,会发到google账户里,用户用google账户登录第三方应用后即可提现。恶意用户M用自己的google账户完成Oauth授权的步骤(1)、(2)、(3),拿到步骤(3)中的authorization_code,然后在自己的个人网站上伪造个跨站请求(如用iframe,src填上第三方应用的redirect_uri?code=${authorization_code}),某个小白用户A访问M的个人网站后,实际上就触发了第三方应用的授权流程,第三方应用收到一个authorization_code,然后拿这个authorization_code去交换access_token,用access_token去获取google账户信息(实际上是恶意用户M的),然后给他发红包。

现在有了state参数,每次第三方应用在redirect_uri上收到一个请求,首先判断请求中的state参数和当前会话中的state参数是否一致。如果一致,表示此请求是正常的授权流程;如果不一致,则表示是CSRF攻击。

3. 为什么会有refresh_token?

refresh_token是一个长期有效的token,被设计用来在当前access_token过期后重新获取一个新的access_token,它由Authorization Server返回(需事先在获取authorization_code时指定access_type=offline)。不同于access_token有个短暂的过期时间,refresh_token只有用户主动取消授权才会失效。

给access_token一个过期时间,同时通过一个长期有效的refresh_token来换取access_token,这么设计的出发点在于性能层面。假如没有refresh_token,并且access_token不设置过期时间,那么为了支持用户取消授权,必须要求Resoure Server在收到每一次API请求,都将access_token送到Authorization Server进行验证,看token是否仍旧有效。如果用户取消授权,token失效;如果未取消授权,token有效。Authorization Server验证access_token的过程无疑是耗时的(有读数据库或者缓存的操作)。有了access_token过期时间,access_token就可以被设计为自编码(self-encoded),通过一些CPU计算进行解码既可验证此token是否有效有权,每次API请求均可减少一次读数据库或者缓存的时间。当然每次用refresh_token获取新access_token还是避免不了一次读数据库或者缓存的操作,但是这个消耗可以忽略不计了。这个设计的缺陷就是用户取消授权会有一定延时。除非access_token过期,否则第三方应用可以拿着access_token继续调用Resource Server上的资源,即便用户已经取消了授权。

--EOF--

密码重置功能漏洞分析和解决思路[下]

上篇文章『密码重置功能漏洞分析和解决思路[上]』总结了几个互联网服务已爆出的密码重置功能漏洞类型,本文来说说在我项目中密码重置功能的实现思路。

上次说到的几种安全漏洞类型,归纳起来其实就两个问题:

1. 密码重置者身份的唯一性标识太容易伪造。比如手机验证码、安全问题答案存客户端等方式。
2. 未做密码重置的水平权限控制。比如允许某个用户越权重置另一个用户的密码。

要解决第一个问题,可以为每次用户密码重置请求生成一个复杂的唯一链接,发送到用户邮箱或手机(效果一样,但链接会较长,相比6位验证码,用户体验会差很多)。要解决第二个问题,最简单的方法就是不信任用户自己的身份声明。他说自己是张三不管用,我们有自己的判断依据。

有一种方式可以统一这两个问题的解决方案:对称密钥加密运算。它的加密算法等同于解密算法,使用相同的密钥,连续加密两次后会得到原始的明文,详见Wiki。用在密码重置的场景里,我们可以:

1. 服务器端对申请密码重置者的身份(userName)和当前时间戳(t)做个对称加密运算,得到一个复杂字符串密文{encryptedString}。加密算法可采用3DES或AES等。
2. 用第1步得到的密文构造此用户的密码重置链接http://domain/{encryptedString},发送到用户邮箱或手机。
3. 用户点击链接后进入密码重置页面,输入用户名、新密码。提交。
4. 服务器端得到来源URL中的{encryptedString},以及用户提交的用户名和新密码。首先使用相同的密钥解密,得到原始明文中的userName和时间戳t。如果解密失败,返回错误。
5. 如果解析出来的userName与用户输入的用户名不匹配,表示此请求非法,此请求可能是非用户本人发出,返回错误。
6. 否则,如果当前时间戳t'与解析出来的t的间隔大于某个阈值,比如24小时,则表示此链接已失效,返回错误。
7. 否则,确定为有效的密码重置请求,重置用户密码,返回成功。

另外,这是一种“无状态”的方案,把密码重置链接的过期时间信息打包在密文中,通过URL来回传递,使服务器无需维护用户名、重置链接标识({encryptedString}、token之类的)和过期时间等信息,降低对缓存和数据库的依赖。

--EOF--

密码重置功能漏洞分析和解决思路[上]

今天看到乌云上一篇关于网站密码找回功能的漏洞分析文章『密码找回功能可能存在的问题』,我几乎是带着震惊的表情看完的,真是触目惊心,这些中枪的网站包括微信、搜狐、当当、360、携程等等。即使CSDN、人人网曾经爆出过密码泄露事件,我心里还是比较信赖大型互联网企业在网络安全方面所提供的保障,现在看来,连密码找回这种基础服务都能爆出这么多的安全漏洞,这不得不让我认识到需要更新下安全观了。这篇文章先把那几个低级漏洞重新拿出来鞭一下尸,以警示自己和后人。

1. 手机验证码设置太弱,并且允许频繁试错。比如当当网微信的这两个漏洞,只要用户输入手机上收到的验证码,就能改密码,而验证码设置又非常简单,是6位的纯数字,暴力搜索一小会儿的事情。这种方式进行密码重置功能应该至少加个验证码或者服务器端判定该用户的修改请求是否太频繁的机制,当当网是两者都缺失,微信则是后一种机制没落实好,留下bug。

2. 找回密码的凭证在客户端可以直接得到。比如走秀网,直接将验证码返回到页面,然后等待与用户输入的进行匹配。搜狐邮箱也是直接将安全问题的答案在HTML中返回。我在想到底是出于什么样的考虑才会做出这么脑残的设计,难道为了让用户体验更好更流畅?完全把用户当傻子。

3. 重置密码的链接太容易伪造。360的漏洞在于密码重置链接很容易伪造,链接的唯一性标识就是当前时间戳的MD5值,只要知道邮箱,对近期的时间戳依次做MD5运算,暴力破解不是问题。

4. 水平权限控制不到位。比如身份通网易邮箱携程网的这几个漏洞,之前有篇文章提到过,基于role的垂直权限控制比较好做,只要分配不同的role即可,但是相同role之间的权限控制就繁琐很多,是与业务紧耦合的。回到重置密码链接/账号绑定的问题,上面几个漏洞都是服务器端只验证了链接唯一性标识(token)的有效性,而没有去验证这个token是否有权去修改指定的账号,根本没做水平的权限控制。

账户安全问题不做好其他都是白搭,以上这些漏洞都是些非常低级的设计逻辑缺陷,破解这些漏洞甚至都不需要有非常高的计算机专业素养。下篇文章会讲讲一种我认为的比较安全的密码重置功能解决方案。

--EOF--