tomcat白名单(八)SNI侵入2
0 先看一下sni的作用
四层 | 七层 | |
浏览器(客户端) |
dns解析 connect ip 在clientHello中用浏览器地址栏host塞入sni |
在http头中塞入Host头 |
网关(服务端) | 根据SNI路由 |
根据Host头路由 |
openshift根据SNI路由passthrough的tcp流量 所以openshift必然会通过某种方式,获取host来路由,这也从性质上决定了我不能完全侵入sni 所以它只可以通过域名访问而不能通过ip 应用1:app1.host 应用2:app2.host app1.host ip === app2.host ip 然后openshift就通过sni拿到了host ,无论客户端是浏览器或java或curl |
jiutou,nginx负责统一证书和ssl解码,根据浏览器域名路由到不同tomcat实例 缺点是无法双向ssl |
|
浏览器验证 |
如果作为反向代理,由各应用端自己返回 如果仅是多域名多证书同ip情况,则有服务器框架根据sni选取证书 |
nginx给唯一域名的证书就行 |
openshift的passthrought可以完成双向ssl握手,决定了它一定是第一个包都直接转发了,ssl第一个包是clienthello,那么clienthello里必然有怎么路由的host
openshift/HAproxy 的passthrough route它本身就是个非标准的https tunnel,基于SNI实现,同时有标准协议,各大浏览器、curl、各种java/c等客户端支持
openshift的二级域名就是给浏览器塞sni用的
参考:
https://blog.csdn.net/chen1415886044/article/details/116330304
该扩展使得可以在TLS握手期间指定网站的主机名或域名 ,而不是在握手之后打开HTTP连接时指定。
SNI通过让客户端发送虚拟域的名称作为TLS协商的ClientHello消息的一部分来解决此问题。这使服务器可以及早选择正确的虚拟域,并向浏览器提供包含正确名称的证书。
服务器名称指示(SNI)有效负载未加密,因此客户端尝试连接的服务器的主机名对于被动的窃听者是可见的。
Web服务器通常负责多个主机名–或域名。如果网站使用HTTPS 则每个主机名将具有其自己的SSL证书。
在HTTPS中,先有TLS握手,然后才能开始HTTP对话。如果没有SNI,客户端将无法向服务器指示正在与之通信的主机名。
如果服务器可能为错误的主机名生成SSL证书。那么SSL证书上的名称与客户端尝试访问的名称不匹配,则客户端浏览器将返回错误信息,并通常会终止连接。
通过 SNI,拥有多虚拟机主机和多域名的服务器就可以正常建立 TLS 连接了。
https://blog.csdn.net/qq_21127151/article/details/107032419
在HTTP协议中,请求的域名作为主机头(Host)放在HTTP Header中,所以服务器端知道应该把请求引向哪个域名,但是早期的SSL做不到这一点,因为在SSL握手的过程中,根本不会有Host的信息,所以服务器端通常返回的是配置中的第一个可用证书。因而一些较老的环境,可能会产生多域名分别配好了证书,但返回的始终是同一个。
在SSLv3/TLSv1中被启用
所以nginx建立SSL连接时不知道所请求主机的名字,因此,它只会返回默认主机的证书。
nginx支持TLS协议的SNI扩展(Server Name Indication,不过,SNI扩展还必须有客户端的支持,另外本地的OpenSSL必须支持它。 如果启用了SSL支持,nginx便会自动识别OpenSSL并启用SNI。是否启用SNI支持,是在编译时由当时的 ssl.h 决定的(SSL_CTRL_SET_TLSEXT_HOSTNAME),如果编译时使用的OpenSSL库支持SNI,则目标系统的OpenSSL库只要支持它就可以正常使用SNI了。 nginx在默认情况下是TLS SNI support disabled,需要重新编译nginx并启用TLS。启用方法步骤如下:
https://help.yunaq.com/faq/5256/index.html
浏览器在访问使用HTTPS协议的站点时,需与服务器建立SSL连接,建立连接的第一步是请求域名证书,此时如服务器部署了多个证书,因客户端还未发送实际的数据请求,服务器无法区分请求的域名
大多数操作系统和浏览器都已经很好地支持SNI扩展,OpenSSL 0.9.8已内置这一功能,新版的Nginx也已支持SNI
https://zhuanlan.zhihu.com/p/547260827?utm_id=0
在 TLS 握手信息中并没有携带客户端要访问的目标地址。这样会导致一个问题,如果一台服务器有多个虚拟主机,且每个主机的域名不一样,使用了不一样的证书,该和哪台虚拟主机进行通信?
和 HTTP 协议用来解决服务器多域名的方案类似:HTTP 在请求头中使用 Host 字段来指定要访问的域名。
TLS 的做法,也是加 Host,在 TLS 握手第一阶段 ClientHello 的报文中添加。
SNI 在 TLSv1.2 开始得到支持。从 OpenSSL 0.9.8 版本开始支持。所以基本市场上的终端设备都支持。
https://www.cnblogs.com/lofanmi/p/17595342.html
可以看到,HTTPS 并没有完全加密我的访问请求,因为 Server Name
依然是明文传输的。它发生在 HTTPS 传输过程中的 Client Hello
握手阶段,在 TCP 三次握手之后。
TLS 1.3,也将 SNI 信息加密了。
https://blog.csdn.net/lyzz0612/article/details/102821201
JDK 是从 1.7 开始才真正支持 SNI,而 HttpClient 是从 4.3.2 开始支持 SNI 的
1 既然openshift通过SNI获取目标host,那么tomcat白名单(六)方案二,SNI侵入中侵入nsi的做法,openshift就应该压根就不知道往哪里路由,而不是路由了又替换了
openshift通过SNI获取目标host的证据:
1)app1.host与app2.host,指向相同ip;如果指向不同ip就没什么好说的,指向相同ip,则必然有一种手段让openshift知道怎么路由
2)有没有可能是通过/app1/ 与 /app2/来分辨路由?
不可能,/app1/ /app2/已经属于http协议,现在在tls握手阶段
3)
所以怀疑tomcat白名单(六)方案二,SNI侵入中侵入nsi的结果,篡改sni应该仅会让openshift无法路由,而不应该又在应用端sni中收到一个原来的域名,这个日志可能是浏览器访问原域名服务留下的,而篡改的请求压根没到日志
2 证明openshift(haproxy)通过sni路由
2.1 篡改sni为random
private String hostport = "app2.host:443";
System.out.println(new NettyHttpClientForHost("random", "/monitor/v1/info").send("GET"));
果然
2.2 用正常的app2.host跑一次
private String hostport = "app2.host:443";
System.out.println(new NettyHttpClientForHost("app2.host", "/monitor/v1/info").send("GET"));
active
read
400
end
<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400 – Bad Request</h1></body></html>
ee
小插曲,这里之所以是400,因为http的HOST策略服务器可能调整过了,不允许有“:”,改一下之后返回正确报文
小插曲2,之前用netty的client,直接访问app2.host:443/monitor/v1/info,不带sni可以,现在居然不行了,必须我显塞sni进去才行;也可能是之前用netty client压根就没有试过正常得请求
2.3 用app1的域名塞到sni,访问app2的url
private String hostport = "app1.host:443";
System.out.println(new NettyHttpClientForHost("app2.host", "/monitor/v1/info").send("GET"));
connect app1.host:443
塞app2.host进sni
active
read
200
end
{"build time":"Tue Feb 20 08:20:11 UTC 2024","image name":"masxfgdsgdfgdfgfg6e","commit":"a3sdfsdfsdfsc","start time":"Sat Feb 24 02:11:18 UTC 2024","branch":"master","swagger":"/xxx/swagger-ui/index.html"}
ee
返回了app2的信息,至此证明了openshift用sni路由tcp流量
3 既然如此 ,我能不能加一个sni?netty的sni接收端是个list,这使至少客户端为可能;先在线上环境试一下,线上不行,本地就不用试了
3.1
type给0
private static class MySNIServerName extends SNIServerName {
protected MySNIServerName(int type, byte[] encoded) {
super(type, encoded);
}
public MySNIServerName(String host) {
this(0, host.getBytes(StandardCharsets.US_ASCII));
}
}
sniServerNameList.add(new SNIHostName(sni));
sniServerNameList.add(new MySNIServerName("1111a.-21370"));
java.lang.IllegalArgumentException: Duplicated server name of type 0
at java.base/javax.net.ssl.SSLParameters.setServerNames(SSLParameters.java:343)
at com.jds.test.httpproxy.miniserver.NettyHttpClientForHost$1.initChannel(NettyHttpClientForHost.java:107)
at com.jds.test.httpproxy.miniserver.NettyHttpClientForHost$1.initChannel(NettyHttpClientForHost.java:91)
netty client 先报错了
3.2
type 给1,1在后
调用成功
加-Djavax.net.debug=all
第二个sni为1111a,\u0031\u0031\u0031\u0031\u0061
客户端反正是携带上了
证明了至少这个版本的haproxy能够接受2个sni一起传过去
3.3
type给1,1在前
io.netty.handler.codec.DecoderException: javax.net.ssl.SSLHandshakeException: Received fatal alert: unrecognized_name
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:499)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: unrecognized_name
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:355)
at java.base/sun.security.ssl.Alert$AlertConsumer.consume(Alert.java:293)
at java.base/sun.security.ssl.TransportContext.dispatch(TransportContext.java:201)
at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:172)
at java.base/sun.security.ssl.SSLEngineImpl.decode(SSLEngineImpl.java:688)
at java.base/sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:643)
at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:461)
at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:440)
at java.base/javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:637)
at io.netty.handler.ssl.SslHandler$SslEngineType$3.unwrap(SslHandler.java:309)
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1441)
at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1334)
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1383)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468)
猜测,这个版本openshift(haproxy)只用第一个,无论第一个是0还是其它的
4 既然线上2个sni可以跑(0在前),本地尝试解析第二个sni
private static String extractSniHostname(ByteBuf in) { int offset = in.readerIndex(); int endOffset = in.writerIndex(); println("2:" + in.getByte(6)); println("3:" + in.getByte(7)); println("4:" + in.getByte(8)); int index2 = (int)in.getByte(6); int index3 = (int)in.getByte(7); String res = null; if(true) { if (index2 != 0 && index3 != 0) { println("no20 route to " + getOriginHttpServiceHost() + ":" + getOriginHttpServicePort()); res = getOriginHttpServiceHost() + "-" + getOriginHttpServicePort(); } else { int index4 = (int) in.getByte(8); String tmp = HttpServerJob.mapIndex2.get(String.valueOf(index4)); println("route proxy tcp request " + index4 + " -- " + tmp); if(tmp != null) { res = tmp; } else { println("unknown code, still route to" + getOriginHttpServiceHost() + ":" + getOriginHttpServicePort()); res = getOriginHttpServiceHost() + "-" + getOriginHttpServicePort(); } } } offset += 34; if(endOffset - offset >= 6) { short sessionIdLength = in.getUnsignedByte(offset); offset += sessionIdLength + 1; int cipherSuitesLength = in.getUnsignedShort(offset); offset += cipherSuitesLength + 2; short compressionMethodLength = in.getUnsignedByte(offset); offset += compressionMethodLength + 1; int extensionsLength = in.getUnsignedShort(offset); offset += 2; int extensionsLimit = offset + extensionsLength; println("extensionsLimit " + extensionsLimit); println("endOffset " + endOffset); if(extensionsLimit <= endOffset) { while(extensionsLimit - offset >= 4) { int extensionType = in.getUnsignedShort(offset); println("extensionType " + extensionType); offset += 2; println("offset after get extension type " + offset); int extensionLength = in.getUnsignedShort(offset); println("extensionLength " + extensionLength); offset += 2; println("offset after get extension length " + offset); if(extensionsLimit - offset < extensionLength) { break; } if(extensionType == 0) { offset += 2; if(extensionsLimit - offset >= 3) { short serverNameType = in.getUnsignedByte(offset); println("serverNameType " + serverNameType); ++offset; if(serverNameType == 0) { int serverNameLength = in.getUnsignedShort(offset); println("serverNameLength0 " + serverNameLength); offset += 2; if(extensionsLimit - offset >= serverNameLength) { String hostname = in.toString(offset, serverNameLength, CharsetUtil.US_ASCII); println("hostname -- " + hostname.toLowerCase(Locale.US)); offset = offset + serverNameLength; } } serverNameType = in.getUnsignedByte(offset); println("serverNameType1 " + serverNameType); ++offset; if (serverNameType == 1) { int serverNameLength = in.getUnsignedShort(offset); println("serverNameLength1 " + serverNameLength); offset += 2; if(extensionsLimit - offset >= serverNameLength) { String hostname = in.toString(offset, serverNameLength, CharsetUtil.US_ASCII); println("hostname1 -- " + hostname.toLowerCase(Locale.US)); } } } break; } offset += extensionLength; } } } return res; }
active
2:-73
3:105
4:116
no20 route to localhost:30000
extensionsLimit 371
endOffset 371
extensionType 0
offset after get extension type 97
extensionLength 76
offset after get extension length 99
serverNameType 0
serverNameLength0 32
hostname -- clxxxxxxxxxxxxxxxnet-21370
serverNameType1 1
serverNameLength1 36
hostname1 -- 1111clxxxxxxxxxxxxx.net-21370
get from top step of tls(ClientHello protocol) localhost-30000
host localhost
port 30000
-----------new connection to end----------
4.2 线上dev环境
目前,本地客户端加一个1的sni在后面不会影响dev,正常输出如2.2
目标为:
1)本地客户端加一个1的sni在后面,dev服务器打印出1的域名
2)原random服务可用
3)原服务可正常访问,加出来的读第2个sni的代码不会影响原服务的访问
5 不再继续,原因
1)尽管netty客户端带了第二个sni,但是要把所有客户端由httpclient改为netty工作量太大
为啥httpclient不行?
netty并没有基于JSSE,而是自己写了SSLHandler
jsse这个点位没有办法侵入,只能debug时变更那个snilist
删掉后放开断点:
<h1>Application is not available</h1> <p>The application is currently not serving requests at this endpoint. It may not have been started or is still starting.</p> <div class="alert alert-info"> <p class="info"> Possible reasons you are seeing this page: </p> <ul> <li> <strong>The host doesn't exist.</strong> Make sure the hostname was typed correctly and that a route matching this hostname exists. </li> <li> <strong>The host exists, but doesn't have a matching path.</strong> Check if the URL path was typed correctly and that the route was created using the desired path. </li> <li> <strong>Route and path matches, but all pods are down.</strong> Make sure that the resources exposed by this route (pods, services, deployment configs, etc) have at least one pod running. </li>
假如在这个地方换一个二级域名,则返回另一个服务
又返回了原服务的200,这里证明了sni是正确路由的充分必要条件
2)openshift(haproxy)是有可能在将来不接受2个sni的
3)其它侵入点,并没有看到有现成的client可用,否则得自己实现tls客户端代价太大
4)网上有 https://blog.csdn.net/qq_27071221/article/details/134619892 (基于Scapy修改ClientHello的SNI(三))用python干了差不多的事情