记一次WAF的SNI时间排查
0x00 背景
近日华为云WAF团队收到一个WAF旗舰版客户反馈的问题,他们的APP在部分安卓机上无法正常使用,取消WAF后又正常。秉承着“以客户价值为依归”的理论,我们立马对相关问题进行排查。首先客户的站点是HTTPS的,然后出问题的终端是部分系统版本比较低的安卓手机,这里可以初步判断是因为这部分终端不支持SNI造成的。
SNI具体的内容在第三节中将会详细介绍,请稍等。
0x01 定位
为了验证我们的推断,我在自己模拟器上面安装了客户的APP,针对手机浏览器和APP分别进行抓包,查看SNI的情况。
这里我们模拟器使用的Genymotion,系统采用的安卓5.1.0,大概的截图如下:
TIPS:这个模拟器是基于X86架构,跑起来非常快,但是我们目标APP是ARM架构的,直接还不能运行,我们需要安装额外的ARM-Translate的,这个就不在本文中介绍了,后面我会专门有文章来介绍,或者有需要的朋友可以直接联系我。
我们就在宿主机上面用Wireshark抓包即可,抓包过程也非常简单,就是分别使用浏览器打开目标网址和用APP登录,我直接给出抓包截图,我们对比看一下吧。
首先是浏览器的抓包:
后面这个是APP的包
两者区别在于SSL握手时候Client的扩展字段有没有SNI字段。
0x01 SNI介绍
SNI是Server Name Indication的缩写,是为了解决一个服务器使用多个域名和证书的SSL/TLS扩展。它允许客户端在发起SSL握手请求时(客户端发出ClientHello消息中)提交请求的HostName信息,使得服务器能够切换到正确的域并返回相应的证书。
在SNI出现之前,HostName信息只存在于HTTP请求中,但SSL/TLS层无法获知这一信息。通过将HostName的信息加入到SNI扩展中,SSL/TLS允许服务器使用一个IP为不同的域名提供不同的证书,从而能够与使用同一个IP的多个“虚拟主机”更方便地建立安全连接。
SSL握手
HTTPS其实是将HTTP的请求使用TLS加密后使用TCP协议传输给目的方,几者之间的关系如下:
TLS加密需要需要在TCP连接建立之后,双方进行SSL握手,协商随机数和证书。大概的过程是这样的:
这里和我们这次文章比较相关的部分就是客户端发送Hello后,服务端返回证书,客户端校验证书有效性。
NGINX反向代理
在现在互联网时代,IP地址越来越紧张,因此我们经常会将多个域名或者网站使用同一台服务器,同一个IP。NGINX通常就是这样的网关。当一个HTTP请求到达时候,NGINX会通过HTTP请求中的Host头来决定转发目的服务器。
NGINX要能够正常的转发,那么它必须能够解析HTTP协议,从上面图中,我们可以看到HTTPS请求中HTTP内容被TLS加密,NGINX在使用前必须进行解密,而解密需要双方协商证书。好的,问题就来了,如果是多个HTTPS网站共享一个IP和端口,SSL握手时候,服务端如何正确选择域名证书传输给客户端呢?
为了解决这个问题在RFC 6066中对TLS的扩展进行了定义,其中就提到了在握手阶段一个server_name的扩展,它的内容就是域名的名字。服务端在接收到含有SNI的Client Hello后,根据其内容,去选择该域名的证书返回给客户端。
因此从上面的解释看出来,这个问题并不是只有WAF才会存在,而是绑定了同一个IP+端口的多个HTTPS网站都会遇到这样的问题。
0x02 APP分析
在上面定位中,我们同一个系统,浏览器携带了SNI,但是客户的APP没有,因此我们决定对客户的APP再进行一轮分析。这里需要使用到JEB工具对客户的APK进行逆向分析。根据activity去查找登录方法所使用HTTP包即可。我们最后定位到MobileHttpClientManager类,实现的代码大致如下:
从代码里面看到,使用的SDK默认的DefaultHttpClient,从相关文章我们知道HttPClient默认是不使用SNI的。
0x02 解决方案
Android
通常情况下,我们可以使用其他默认支持SNI的库,比如URLConnection,OKHttp等
HttpsURLConnection
try { URL url = new URL("https://www.huaweicloud.cn"); URLConnection urlConnection = url.openConnection(); HttpsURLConnection connection = (HttpsURLConnection) urlConnection; connection.setRequestProperty("Host", "www.huaweicloud.cn"); connection.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return HttpsURLConnection.getDefaultHostnameVerifier().verify("www.huaweicloud.cn", session); } }); connection.connect(); } catch (Exception e) { e.printStackTrace(); } finally { }
自 Android 2.3 开始,HttpsURLConnection 就支持 SNI。如果您需要支持 Android 2.2(及更旧的版本),一种解决办法是在一个唯一端口上设置备用虚拟主机,以便了解要返回哪个服务器证书。
比较极端的替代方法是不使用服务器默认情况下返回的验证程序,而是将 HostnameVerifier 替换为不使用您的虚拟机主机名的验证程序
Apache HttpClient
虽然HttpClient的4.3.2版本在Oracle JRE 1.7+已经支持SNI了,但是Android可不是使用的Oracle的JRE啦,这个涉及到版权等问题。
我们最好是使用时,手动设置一下HostName。
// Android specific code to enable SNIif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Enabling SNI for " + target); } try { Method method = sslsock.getClass().getMethod("setHostname", String.class); method.invoke(sslsock, target); } catch (Exception ex) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "SNI configuration failed", ex); } } }
从代码也看到了,这个需要安卓4.2.2以后的版本才是支持的。
iOS
因为CFNetwork是支持SNI的,因此我们只需要判断协议然后决定是用上层的网络请求转发还是用底层的cfnetwork来转发。
if ([self.request.URL.scheme isEqualToString:@"https"] ) { //使用CFnetwork curRequest = req; self.task = [[CustomCFNetworkRequestTask alloc] initWithURLRequest:originalRequest swizzleRequest:curRequest delegate:self]; if (self.task) { [self.task startLoading]; } } else { //使用普通网络请求 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURLSessionTask *task = [self.session dataTaskWithRequest:req]; [task resume]; }
浏览器
目前不管是PC还是移动端,主流的现代浏览器都是支持SNI的。
0x03 参考链接
[1] SNI中文维基百科
[2] SSL握手介绍
[3] SSL/TLS协议介绍
[4] iOS的HTTP DNS方案
- 点赞
- 收藏
- 关注作者
评论(0)