
如何配置nginx以预防扫描IP暴露相关的域名
玩Web建站的,或多或少都会配置CDN或各类加速,一是为了提高各地用户访问速度,二是可以隐藏源站IP避免被攻击。但是否域名不解析源站IP就万无一失了呢?
注:本文中TLS与SSL一词取同义。虽然狭义上的TLS已经取代了狭义上的SSL,但本文部分文字中出于表述习惯仍称SSL。
下面做一个实验:
实验:扫描以确定该IP相关的域名
实验准备
首先,建立两台Debian虚拟机Server(192.168.125.139)、Client(过程略)。
自签名证书
在Server上使用openssl建立自签名CA并签发SSL证书:
1 | # 添加SAN配置 |
1 | # 建立CA私钥和证书 |
1 | # 建立私钥和证书申请 |
1 | # CA签发证书 |
配置nginx
首先安装nginx。
1 | sudo apt install nginx |
修改站点配置文件,建立https服务。
1 | sudo nano /etc/nginx/sites-available/default |
站点配置文件全文见下:
1 | server { |
然后重启nginx。
1 | sudo /etc/init.d/nginx restart |
实验开始
在Client机运行curl -vvvv -k https://192.168.125.139
,得到如下输出:
1 | * Trying 192.168.125.139:443... |
其中注意到以下内容:
1 | * Server certificate: |
有趣的是,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 | user www-data; |
修改后的nginx.conf:
1 | user www-data; |
站点配置文件修改:
1 | # 在原server块前添加以下server块,用于接收Host字段不匹配的连接 |
修改后重启nginx测试,在Client机运行curl -vvvv -k https://192.168.125.139
,得到如下输出:
TLS握手未完成即中断。
抓包结果也验证了这一情况:Client Hello发送后,TCP连接即被简化的四次挥手关闭。
而运行curl -vvvv -k https://testdomain.com
通过绑定的域名访问则一切正常:(在Client机已配置hosts文件将testdomain.com指向Server机)
抓包可以看到,Client Hello中包含了正确的SNI,TLS握手顺利完成。
后记:哼哼啊啊啊啊啊啊这什么破代码高亮啊怎么一片灰白啊(恼