原同学
文章27
标签11
分类9

一言

如何配置nginx以预防扫描IP暴露相关的域名

如何配置nginx以预防扫描IP暴露相关的域名

玩Web建站的,或多或少都会配置CDN或各类加速,一是为了提高各地用户访问速度,二是可以隐藏源站IP避免被攻击。但是否域名不解析源站IP就万无一失了呢?

注:本文中TLS与SSL一词取同义。虽然狭义上的TLS已经取代了狭义上的SSL,但本文部分文字中出于表述习惯仍称SSL。
下面做一个实验:

实验:扫描以确定该IP相关的域名

实验准备

首先,建立两台Debian虚拟机Server(192.168.125.139)、Client(过程略)。

自签名证书

在Server上使用openssl建立自签名CA并签发SSL证书:

1
2
# 添加SAN配置
echo "subjectAltName=DNS:testdomain.com" > server-ext.cnf
1
2
# 建立CA私钥和证书
openssl req -x509 -newkey rsa:4096 -days 114514 -nodes -keyout ca.key -out ca.crt
1
2
3
# 建立私钥和证书申请
# 注意CN字段填域名,与SAN配置一致即可
openssl req -newkey rsa:4096 -nodes -keyout server.key -out server.req
1
2
# CA签发证书
openssl x509 -req -in server.req -days 365 -CA ca.crt -CAkey ca.key -out server.crt -extfile server-ext.cnf

配置nginx

首先安装nginx。

1
sudo apt install nginx

修改站点配置文件,建立https服务。

1
sudo nano /etc/nginx/sites-available/default

站点配置文件全文见下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/cert/server.crt;
ssl_certificate_key /etc/cert/server.key;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name testdomain.com;
location / {
try_files $uri $uri/ =404;
}
}

然后重启nginx。

1
sudo /etc/init.d/nginx restart

实验开始

在Client机运行curl -vvvv -k https://192.168.125.139,得到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
*   Trying 192.168.125.139:443...
* Connected to 192.168.125.139 (192.168.125.139) port 443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
* Server certificate:
* subject: C=CN; ST=Shandong; O=Test; CN=testdomain.com
* start date: May 26 04:04:27 2024 GMT
* expire date: May 26 04:04:27 2025 GMT
* issuer: C=CN; ST=Shandong; O=Test; CN=TestCA
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* using HTTP/2
* h2h3 [:method: GET]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: 192.168.125.139]
* h2h3 [user-agent: curl/7.88.1]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x56448ff24c80)
> GET / HTTP/2
> Host: 192.168.125.139
> user-agent: curl/7.88.1
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 200
< server: nginx/1.22.1
< date: Sun, 26 May 2024 04:36:45 GMT
< content-type: text/html
< content-length: 615
< last-modified: Sun, 26 May 2024 03:45:34 GMT
< etag: "6652b05e-267"
< accept-ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host 192.168.125.139 left intact

其中注意到以下内容:

1
2
* Server certificate:
* subject: C=CN; ST=Shandong; O=Test; CN=testdomain.com

有趣的是,Client只知道一个Server的IP地址,但是却可以从返回的信息中得知服务器相关的Web域名。而很多情况下,CDN配置时,源站使用的SSL证书域名往往和CDN域名相同或在同一根域名下。这种情况是由于nginx不适当的SSL配置引起的。
有杠精要来杠了:你这是知道IP了反查域名,这和我们说的“域名不解析源站IP是否会被查出源站IP”没有可比性。
但有一个情况是不可忽视的:每时每刻都有大量的扫描针对每一个IP段发生,扫描得到的数据可以被查询、分析。如著名的搜索引擎Shodan、Censys等,均可以对互联网的IP段扫描。除此之外,攻击者也可以利用肉鸡设备进行批量扫描。通过批量扫描的结果,攻击者可以反查出某个域名相关的IP地址。
除非Web服务器配置白名单,只允许CDN回源IP连接服务器,否则仍然不可避免会存在被扫描的风险。

结果分析

TLS握手在TCP连接建立后开始,主要包括几个阶段:

  • Client Hello
    客户端以明文发起请求,包含TLS版本、支持的加密/压缩算法、一个随机数,以及可选的TLS扩展等信息。
  • Server Hello
    服务器以明文回复客户端,包含服务器选择的TLS版本、加密/压缩算法和一个随机数,以及服务器的SSL证书(链)
  • 密钥交换
  • 算法协商
  • 加密的握手

而实验中服务器证书暴露域名的情况发生在Server Hello这一步。在该步骤中,服务器发回其在此次TLS连接中要使用的证书(链),而其中(对于绝大部分公网的Web服务器)包含了服务器相关的域名。
(不要问我为什么服务器证书里会有和服务器相关的域名,出门右转必应欢迎您)

思考

在这里打断一下,考虑一个IP上托管了多个不同域名的网站的情况。
在HTTP情况下,客户端会在HTTP请求头的Host字段中提供所请求服务器的域名,Web服务器可以根据该信息确定该请求对应哪一个网站。
但在HTTPS情况下,客户端仍然在HTTP请求头的Host字段中提供域名,但发出HTTP请求时,TLS连接必须已经成功建立。而TLS连接建立时,客户端会检查服务器发回的证书中域名是否和访问的域名匹配。服务器此时还不知道客户端要访问哪一个网站,那么服务器要提供多张SSL证书中的哪一个呢?
解决方案是在Client Hello中添加TLS扩展SNI(服务器名称指示),提前告知服务器“我希望与testdomain.com建立TLS连接,请提供与其匹配的证书”。此时服务器即可根据SNI提供客户端期望的证书。
但是在上面的情况中,我们通过IP地址进行HTTPS访问,服务器显然无法提供一个能与SNI匹配的证书。这种情况下,nginx默认会返回第一个开启了TLS的server块中配置的证书。从而使得攻击者可以得到与该IP地址相关的域名。

如何配置nginx以预防扫描IP暴露相关的域名

在提出解决方案之前,我们需要强调一点:TLS并非专用于HTTPS。TLS工作在OSI模型的第4层(传输层),可与任何应用层协议搭配(只要软件支持或配置代理)。因此,问题与HTTP无关,解决方案也不应依赖HTTP的实现。
而根据上文,我们容易注意到,nginx选择返回哪一张证书的依据是Client Hello中的SNI,而在攻击者并不知道该IP绑定的正确域名时,SNI往往不可能匹配成功。因此,SNI是一个良好的解决切入点。
那么,解决方案的思路是:在nginx发回证书之前,判断SNI是否为绑定的域名。如果不是,则阻止发回SSL证书。
实现方法是先用stream模块在4层接收TLS流量,开启SSL预读获取SNI,并根据SNI将流量转发到不同的server块(注意这里开启proxy_protocol,该server块应只在本地监听,推荐使用Unix套接字或绑定环回地址),不匹配的则断开连接。为了支持stream块和SSL预读,我们需要使用完整版的nginx(在Debian上,卸载nginx包,然后安装nginx-full包)。
综上,我们安装完整版nginx,并对nginx.conf和对应站点文件的server块作修改。
原nginx.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
worker_connections 768;
}

http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

修改后的nginx.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
worker_connections 768;
}

http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
gzip on;

# 添加此处两行以向后端传递客户端的来源IP地址
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

# 添加以下整个stream块,用于从4层处理连接
stream {
map $ssl_preread_server_name $name {
# 这里填写你绑定的域名和其别名,一对一行,空格分隔,分号结尾
testdomain.com web;
default block; # 该行用于不能匹配到绑定域名的情况
}
# 对应地添加upstream块,upstream后面接对应的别名
upstream web {
server unix:/var/run/web.sock; # 此处用Unix Socket,也可以用监听本地端口
}
server {
listen 443 reuseport;
listen [::]:443 reuseport;
proxy_pass $name; # 指示根据条件选择不同的上游server
ssl_preread on; # SSL预读,允许提前处理
proxy_protocol on;
}
}

站点配置文件修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 在原server块前添加以下server块,用于接收Host字段不匹配的连接
server {
listen 80 default_server; # 设置为默认server块,下同
listen [::]:80 default_server;
server_name _; # 不绑定主机名
return 444; # 444响应码是nginx提供的特殊响应码,返回该响应码指示nginx断开该连接
}
# 原server块
server {
# 以下两行去掉
#listen 443 ssl http2;
#listen [::]:443 ssl http2;

# 添加以下行
listen unix:/var/run/web.sock ssl proxy_protocol; # 此处用Unix Socket,也可以用监听本地端口
set_real_ip_from 127.0.0.1;
set_real_ip_from unix:;
real_ip_header proxy_protocol;

# SSL配置不变
ssl_certificate /etc/cert/server.crt;
ssl_certificate_key /etc/cert/server.key;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;

# 以下配置不变
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name testdomain.com;
location / {
try_files $uri $uri/ =404;
}
}

修改后重启nginx测试,在Client机运行curl -vvvv -k https://192.168.125.139,得到如下输出:
curl TLS握手未完成
TLS握手未完成即中断。
抓包结果也验证了这一情况:Client Hello发送后,TCP连接即被简化的四次挥手关闭。
Client Hello后连接即关闭
而运行curl -vvvv -k https://testdomain.com通过绑定的域名访问则一切正常:(在Client机已配置hosts文件将testdomain.com指向Server机)
curl访问正常
抓包可以看到,Client Hello中包含了正确的SNI,TLS握手顺利完成。
TLS握手成功

后记:哼哼啊啊啊啊啊啊这什么破代码高亮啊怎么一片灰白啊(恼

本文作者:原同学
本文链接:https://blog.mzy7.cn/posts/95c661f3.html
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可,但如果本文显式指定了其他许可协议,则以指定的许可协议为准。