tomcat白名单(八)SNI侵入2

tomcat白名单(五)其他

 

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

 4.1 本地
    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干了差不多的事情

posted on 2024-03-01 17:29  silyvin  阅读(49)  评论(0编辑  收藏  举报