本文转载自:https://zinglix.xyz/2021/10/04/nginx-ssl-reject-handshake/

原文

TL;DR

nginx 配置避免 IP 访问时证书暴露域名

利用 nginx 1.19.4 后的新特性 ssl_reject_handshake on;,将其置于默认访问时配置中,IP 访问时会终止 TLS 握手,也就不会暴露域名了。

使用如下命令查看你的nginx版本

1
2
# nginx -v
nginx version: nginx/1.20.1

细说

CDN 是建站时常用的工具,在自己的主机外面套一层 CDN 是常见操作,一般这样认为自己的主机就安全了,有人来攻击也会先到 CDN 服务器,攻击者根本无法获取到自己主机的 IP,但事实真的是这样吗?

我们先来看看一般配置后会出现什么问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80 default_server;

# Redirect all HTTP requests to HTTPS.
return 301 https://$host$request_uri;
}

server {
listen 443 default_server;
server_name _;
include conf.d/ssl.config;
return 444;
}

上面是一个很常用的 nginx 配置,HTTP 访问全部重定向到 HTTPS 的 443 端口上,没有配置过的域名返回 444 终止连接。

好了,现在尝试用 IP 和 HTTPS 访问你的网站,你应该能够看到预想中访问失败、证书无效等连接失败的提示。

但是!注意下浏览器左上角提示的不安全,点开查看证书信息,你就会发现你的域名其实随着证书发送了过来。此时如果你是攻击者,那么其实就可以知道该域名背后的源主机 IP 就是这个。

上图即为用 IP 访问后,依旧能看到证书内容。这是因为返回 444 是 HTTP 层面的事情,意味着到达这一步下层的 TLS 握手已经完成。证书不被信任是一回事,但说明已经拿到了服务器的证书。

CDN 确实避免了直接 DNS 查询暴露 IP 的问题,但攻击者通过扫描全网 IP,用上述方式依旧可以知道每个 IP 对应的域名是什么,这也是为什么很多站长用了 CDN 后并且反复更换 IP 却依旧被攻击者迅速找到 IP 的原因。

Censys 就一直在干这件事,全网扫描 IP 并找到其对应的域名

那该怎么办呢?

问题根源出在 client 在 TLS 握手时发送了 ClientHello 后,nginx 在 ServerHello 中带着含有域名的默认证书返回了,因为 nginx 期望可以完成握手,这可能可以算是 nginx 的一个缺陷。

如果你不熟悉 TLS 握手流程,那么可以看看 这篇文章

笨办法

既然 nginx 默认提供了带有域名的证书,那么想不暴露也很简单,提供一个不含有正确域名的证书即可。

nginx 设置中 HTTPS 访问如果没有设置证书,那么就会报错。但反正 IP 访问也不需要提供服务,那么直接自签一个 IP 证书,或者随便一个域名的证书都可。当然,如果能搞定合法的 IP 证书也不是不行。

搞定证书后,添加一个配置,让 IP 访问返回错误证书就完事了。

1
2
3
4
5
6
7
8
9
server {
listen 443 ssl default_server;
server_name your_ip;

ssl_certificate xxxx.pem;
// and more ssl config ...

return 444;
}

好方法

这种方法还得自己搞个证书,如果服务器多每个都得这么搞也挺麻烦的,好在这个问题 nginx 这已经有了很完美的解决方案。

ClientHello 中是带着 SNI 的,所以其实握手阶段是可以知道访问的域名是否合法的,nginx 1.19.4 中添加了一个新的配置项 ssl_reject_handshake 用于拒绝握手,也就不会提供证书。

使用方法也很简单,将原本默认配置中的 return 444 替换成 ssl_reject_handshake on 即可。

1
2
3
4
5
6
7
8
9
server {
listen 443 default_server;
server_name _;
include conf.d/ssl.config;

# 不用返回 444 了,直接拒绝握手
ssl_reject_handshake on;
# return 444;
}

配置后,再尝试 IP 访问,会发现浏览器报了 ERR_SSL_UNRECOGNIZED_NAME_ALERT 的错误,也看不到证书信息,目标达成!

其实还没完

上述方法是通过 ClientHello 中的 SNI 确定访问是否合法的,那如果 SNI 就是正确的域名呢?

这种场景发生于攻击者已经确定要攻击某个域名,那么他就可以将带着该域名的握手信息遍历所有 IP,握手成功就找到,这样访问其实与正常访问并无区别,唯一解决方法就是白名单只允许 CDN 服务器访问。

例如攻击者用 hosts 直接硬写 IP,将域名强行指向某个 IP

或者用这种方式 curl https://example.com --resolve 'example.com:443:172.17.54.18'

如下,在 nginx 里面的 location 配置添加上 allow 的 IP 段,只允许 CDN 运营商的 IP 访问你的服务,就能避免绕过 CDN 造成的攻击

1
2
3
4
5
location / {
allow 172.1.2.0/24; # 允许CDN运营商的IP
allow 1.2.3.4/32;
deny all; # 阻止其他任何IP
}

上述 IP 段只能向 CDN 服务提供商询问,一般文档中都是有相关信息的。

慕雪的测试

上面这篇文章我第一次看的时候还没有理解他说的是什么内容。后来测试了一下,明白了。

说明

以我的服务器举例,我是centos 7.2的服务器,直接用yum安装的nginx,版本nginx/1.20.1,配置路径是/etc/nginx

在默认情况下,你会有个nginx.conf,和/etc/nginx/conf.d里面的用户配置文件

1
2
3
4
5
/etc/nginx
- nginx.conf
- conf.d
- server1.conf
- server2.conf

nginx.conf里面除了加载用户配置文件,还会有一个默认的server,指向一个静态文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 用户配置文件
include /etc/nginx/conf.d/*.conf;

server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html; # 默认的静态文件路径

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

error_page 404 /404.html;
location = /404.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}

在我的服务器上,这个路径里面是如下内容

1
2
[root@bt-7274:/etc/nginx]# ls /usr/share/nginx/html
404.html 50x.html en-US icons img index.html nginx-logo.png poweredby.png

此时直接在浏览器访问你的ip,会展示这个默认路径里面的index.html,是centos的一个介绍页面。

image-20231209173337849

但是,这并不代表你当前没有解析到任何用户自定义文件!nginx默认情况下会使用第一个用户自定义conf来作为ip访问的结果(这是因为对用户自定义conf的include是在defualt server之前的,你可以理解为用户自定义文件会像C语言的头文件一样在nginx.conf中被展开)

当前实际上是访问了/etc/nginx/conf.d里面按字典排序的第一个用户conf配置

1
2
3
4
5
/etc/nginx
- nginx.conf
- conf.d
- server1.conf # 直接访问ip,使用了这个配置文件
- server2.conf

为什么在我这里依旧展示了默认的静态文件路径呢?是因为我的第一个配置文件a.conf中没有配置location /,全都是其他路径(比如/a/)的反代!所以nginx就往后采用了最末尾的default server里面提供的默认静态文件。

而浏览器链接左侧的红色不安全就告诉我们,当前其实收到了一个ssl证书,这便是上面原文中提到的ip访问会因为ssl证书泄漏域名的问题。

image-20231209174001901

我们可以点击不安全提示,再点击右上角那个带徽章的小按钮,查看当前收到的证书

image-20231209174025184

如下图,当前收到的这证书,正是我的/etc/nginx/conf.d中第一个用户配置里面的ssl证书;内部包含了该证书对应的域名,我们的域名因此泄漏!

image-20231209174102666

思路回顾

再来缕一缕思路

  • nginx会先加载用户配置文件,末尾才是默认指向/usr/share/nginx/html静态路径的配置
  • 当你使用ip访问当前服务器,nginx会给浏览器发送/etc/nginx/conf.d中按字典序排在第一位的用户配置文件中的ssl证书(即上图所示证书)
  • 恶意访问人员可以通过遍历访问所有IP地址,当访问你的服务器IP地址时,他拿到一个ssl证书,其中包括了一个域名A;
  • 假设你的域名A是按域名A->CDN->服务器IP来进行解析的,此时恶意访问人员就通过这个ssl证书直接得到了域名A->服务器IP或者CDN域名->服务器IP的对应关系,完全绕过了CDN的服务器;
  • CDN域名可以通过ping 域名A直接得到,同样也是得到了域名A->服务器IP的映射关系,知道了你的源站IP;
  • 此时他就可以通过修改hosts强制让域名A指向服务器IP,绕过CDN直接攻击你的源站;

套了CDN还暴露源站IP肯定不是我们想要的结果,所以我们需要解决这个问题!

解决办法

/etc/nginx/conf.d中直接添加一个a.conf,让其排序在字典序的第一位,里面写入如下内容,其中server_name _的含义是除了我们配置过的域名外的其他访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80 default_server;
server_name _;

# Redirect all HTTP requests to HTTPS.
return 301 https://$host$request_uri;
}

server {
listen 443 default_server;
server_name _;

# 不用返回 444 了,直接拒绝握手
ssl_reject_handshake on;
# return 444;
}

配置后直接重启nginx,没有报错就是ok了

1
2
3
4
[root@bt-7274:/etc/nginx/conf.d]# systemctl restart nginx
[root@bt-7274:/etc/nginx/conf.d]# ll
total 28
-rw-r--r-- 1 root root 193 Dec 9 17:52 a.conf

此时直接访问就会报错ssl的alert了,但是edge中估计是因为缓存的问题,依旧能看到证书

image-20231209175534238

换火狐看一下,无法连接,没有证书,目标达成!

image-20231209175736923

这里顺带贴一下火狐中一个正常ssl网站会显示成什么样子。如下是京东官网,在锁的按钮里面能看到证书的颁发者,而上图修改完毕配置文件后的测试中没有看到证书颁发者,即我们的证书并没有泄漏,目的达成。

image-20231209175834078

另外,我试了试我另外一个服务器使用的1panel安装的OpenResty,这个比较好,在默认情况下直接访问IP地址返回的是404,且没有暴露证书。不需要自己额外做配置了。