NTP 放大攻击,原理与实践

昨天在微博上看到乌云知识库分享了一篇关于 NTP 放大攻击的文章,点进去看了一下,发现内容很有意思,漏洞本身利用的原理很简单,实现的难度也并不高,但是产生的效果却十分明显,于是决定按照文章中的方式简单复现一下攻击。

本文大部分内容来自 《NTP Amplification Discovery》 原文链接

首先在进行介绍之前,需要你具有基本的背景知识,比如什么是 DDos 攻击。

NTP 放大攻击所利用的协议是 NTP 协议,全称是 Network Time Protocol,网络时间协议。是用于在互联网中对不同主机进行时间同步的协议。其中运行某些版本NTP 的服务器默认开启了对 MONLIST 命令的支持,这条命令的作用是向请求者返回最近通过 NTP 协议与本服务器进行通信的 IP 地址列表,最多支持返回600条记录。也就是说,如果一台 NTP 服务器有超过600个 IP 地址使用过它提供的 NTP 服务,那么通过一次 MONLIST 请求,你会收到返回的包含600条记录的数据包。这应该远远大于你发送查询命令的数据包大小。

那么,都有谁可以发起 MONLIST 查询请求呢?与 DNS 协议类似,NTP 协议也是基于 UDP 实现的,并且无法识别伪造了来源的请求数据包,使得任何人都可以伪装成需要攻击的目标主机对 NTP 服务器发起查询请求,支持 MONLIST 命令的 NTP 服务器在收到攻击者的数据包后,会将结果通过 UDP 的方式发送到攻击者指定的 IP 地址,造成流量攻击。而 NTP 放大攻击的可怕之处在于,它能够将攻击流量上百倍的放大,在最理想的情况下,可以将攻击的流量放大206倍(来源:https://blog.cloudflare.com/understanding-and-mitigating-ntp-based-ddos-attacks/)。在我后面的测试中,放大的效果也很轻松的达到了100倍以上。

介绍完相关背景,下面开始记录一次简单的模拟攻击流程。

首先第一步是发现网络上开启了 NTP 协议的主机,NTP 协议使用的端口是123端口。所以可以通过扫描123端口来获得一份开启 NTP 的主机列表。使用 Masscan 工具可以快递的完成大量的端口扫描任务。

./masscan -pU:123 -oX ntp_alive.xml --rate 160000 100.0.0.0-220.0.0.0 --exclude 255.255.255.255

其中,–exclude 255.255.255.255 是在扫描量较大的时候要求强制使用的,否则无法发起扫描,会得到如下的错误提示。

FAIL: range too big, need confirmation
 [hint] to prevent acccidents, at least one --exclude must be specified
 [hint] use "--exclude 255.255.255.255" as a simple confirmatio

在进行测试的时候,不需要使用100.0.0.0-220.0.0.0这样巨大的范围,因为这样的话,你的网络流量将会变成下面这样。

masscan_traffic

在 25Mbps 的带宽下,这个扫描请求跑了近三个半小时才完成。绿色部分代表出口流量,蓝色部分代表返回流量。即便是当发出和收到的数据包大小相等的时候,在线的 NTP 主机占比也不到10%,实际上考虑到 NTP 的放大效应,粗略地估计最终捕获到的主机数量所占扫描总量的比例应该在0.1%以内。

我一共对120*2^24=2013265920台主机的123端口进行了扫描,其中包括了一些非公网的 IP 地址,大约是16777216+1048576+65536=17891328。所以一共是1995374592+1(127.0.0.1)个IP。最终得到的扫描结果去重后是263493,大概占到了0.013%。

下一步是删选出可以接受 MONLIST 命令的主机,使用开头链接里博客给出的代码

from scapy.all import *
import thread
#Raw packet data used to request Monlist from NTP server
rawData = "\x17\x00\x03\x2a" + "\x00" * 61
#File containing all IP addresses with NTP port open.
logfile = open('uniq_ntp.txt', 'r')
#Output file used to store all monlist enabled servers
outputFile = open('monlistServers.txt', 'a')
def sniffer():
    #Sniffs incomming network traffic on UDP port 48769, all packets meeting thease requirements run through the analyser function.
    sniffedPacket = sniff(filter="udp port 48769 and dst net your_server_ip", store=0, prn=analyser)

def analyser(packet):
    #If the server responds to the GET_MONLIST command.
    if len(packet) > 200:
        if packet.haslayer(IP):
            print packet.getlayer(IP).src
            #Outputs the IP address to a log file.
            outputFile.write(packet.getlayer(IP).src + '\n')

thread.start_new_thread(sniffer, ())

for address in logfile:
    #Creates a UDP packet with NTP port 123 as the destination and the MON_GETLIST payload.
    send(IP(dst=address)/UDP(sport=48769, dport=123)/Raw(load=rawData))
print 'End'

需要将代码部分中 your_server_ip 替换成进行测试的主机 IP,这样才能接受到返回的 MONLIST 数据包并对主机是否可用进行判断。最终这263493主机中,筛选出了5121台主机。比 Cloudfare 遭受攻击时使用的4529台服务器还要多。

其实在这里是可以进行优化的,因为每台 NTP 主机所在的网络环境和繁忙程度不同,可能一部分 NTP 主机只有为数不多的主机与其进行通信,这样他返回的数据包的大小并没有比请求的数据包高出太多,会拉低整体的放大效果。因此可以稍微修改 analyser 函数,将放大倍数较小的主机抛弃,或者依据返回的大小安排不同的权重,从而达到在有限带宽下获得最大放大倍数的效果。

接下来我们已经有了一份可以利用的 IP 列表并进行测试了。这里我拿自己的 VPS 进行了简单的测试,测试的方法和数据可能不是十分准确,但是应该足以说明这种攻击方式的实现难度和效果了。

iptraf

通过图中的数字可以看到 48769测试端口收到的流量是102050K,发出的流量是1253082,简单的计算后81.44倍的放大效果。实际上放大倍数比这个值要大,因为数据包发出后服务器收到再返回会有一定的延迟,所以实际最后收到的流量应该大于当前实时看到的数字。在我上午进行的测试中,最后的放大倍数在110左右。在没经过任何优化的情况下取得这样的效果,还是很可怕的。具体的攻击测试脚本就不放出来了。以免造成了不希望看到的结果,如果需要测试的话可以在前面的扫描脚本上进行简单的修改就能够得到一个威力强大的攻击工具。如果部署在一定数量的服务器上同时运行,会产生很可观的攻击流量。

相关参考:

https://blog.cloudflare.com/understanding-and-mitigating-ntp-based-ddos-attacks/

https://blog.cloudflare.com/technical-details-behind-a-400gbps-ntp-amplification-ddos-attack/