Атака Slowloris — це специфічний тип кібератаки, спрямований на виведення з ладу веб-серверів шляхом встановлення та підтримання численних незавершених HTTP-з’єднань. Цей метод дозволяє одному атакуючому пристрою ефективно використовувати мінімум ресурсів для досягнення максимального впливу, не зачіпаючи інші сервіси та порти. Вразливими до цієї атаки є сервери, які не мають належних механізмів захисту від повільних HTTP-запитів.
Дисклеймер: Під час створення цієї статті жоден сервер не зазнав шкоди. Сервіс httpbin.org використовувався виключно в демонстраційних цілях. Жодних реальних атак не проводилося, а всі запити були контрольованими та застосовувалися лише для перевірки роботи коду. Матеріал має виключно освітній характер і не спрямований на заохочення зловмисних дій.
Ця стаття призначена для ознайомлення з принципами атаки Slow Loris. Варто зазначити, що цей метод є доволі застарілим, і сучасні системи вже мають вбудовані механізми захисту від нього.
Раніше ми досліджували подібну поведінку, експериментуючи з виснаженням воркерів у Nginx. Тепер детальніше розглянемо, як функціонує Slow Loris і яким чином його можна реалізувати за допомогою Python.
Атака Slow Loris базується на відкритті великої кількості з’єднань із сервером і підтриманні їх відкритими якомога довше. Це досягається шляхом надсилання часткових HTTP-запитів, які періодично доповнюються новими заголовками, щоб запобігти закриттю сокетів.
Коли ця атака була вперше реалізована та протестована у 2009 році, швидкість інтернету була значно нижчою, а веб-сервери не мали чітких обмежень на час отримання повного запиту. Через це сервери закривали з’єднання лише тоді, коли протягом певного періоду не отримували жодних даних, наприклад, заголовків. Однак, якщо сервер отримував хоча б частину заголовка, він вважав з’єднання активним і продовжував його утримувати.
Сьогодні ситуація змінилася: інтернет став значно швидшим, а сучасні веб-сервери вже мають встановлений таймаут, протягом якого клієнт повинен надіслати повний запит. У цій статті буде враховано ці сучасні механізми захисту, протестовано атаку Slow Loris і перевірено, наскільки вона все ще може бути ефективною у сучасних умовах.
Щоб успішно реалізувати атаку Slow Loris, спершу необхідно визначити таймаут сервера — тобто час, протягом якого сервер утримує з’єднання відкритим в очікуванні даних. Для цього можна скористатися утилітами Linux, такими як netcat, а також Wireshark для детального аналізу трафіку.
Як тестовий майданчик у цьому випадку буде використано сайт httpbin.org, що дозволяє безпечно проводити перевірки та експерименти.
Перетворимо доменне ім’я на IP-адресу за допомогою nslookup:
$ nslookup httpbin.org Server: 127.0.0.53 Address: 127.0.0.53#53 Non-authoritative answer: Name: httpbin.org Address: 3.230.67.98 Name: httpbin.org Address: 3.211.25.71
Як бачимо, у домену httpbin.org є дві IP-адреси. Це зв’язано з балансуванням навантаження. Для нашого експерименту зосередимося на 3.230.67.98.
Відкриємо Wireshark і вкажемо фільтр для потрібної IP-адреси:
ip.addr eq 3.230.67.98
netcatУ терміналі підключимося до сервера без надсилання даних:
$ nc 3.230.67.98 443
Цим самим ми встановимо TCP-з’єднання, але не будемо передавати жодних даних.
У Wireshark побачимо наступне:
Пакет
[1]— ACK-пакет, який завершує TCP-рукостискання.
Пакет
[2]— FIN ACK-пакет, який завершує з’єднання.
Час між пакетом [1] та пакетом [2] можна побачити в колонці Time. Наприклад:
Пакет
[1]: 2.5 секунди
Пакет
[2]: 62.67 секунди
Різниця між ними — 60 секунд. Таким чином, таймаут сервера становить 60 секунд. Примітка: SSL Handshake не скидує таймаут.
Slow Loris – це про велику кількість з’єднань. Давайте порахуємо пропускну здатність вашого інтернет-з’єднання, щоб встигнути передати дані до того, як сервер закриє з’єднання через таймаут.
Для прикладу, якщо ви відкриєте 65 000 з’єднань і кожне з них передаватиме мінімальний запит GET / HTTP/1.1\r\nHost: httpbin.org\r\nConnection: keep-alive\r\n\r\n розмір якого 61 байт, то ось що ми отримуємо:
65 000 × 61 байти = 3 965 000 байт ≈ 3.78 МБ
Якщо сервер має таймаут 60 секунд, то необхідна швидкість для передачі всіх цих даних:
3.78 МБ / 60 секунд ≈ 61 КБ/сек
Для підтримки такої атаки вам вистачить навіть повільного інтернет-з’єднання. Головне – це правильно розподілити запити та дотримуватися таймінгів, щоб зберігати з’єднання якомога довше без таймаутів. Не забудьте вказати заголовок Connection: keep-alive, щоб сервер не закривав з’єднання одразу після відповіді.
Відкриваємо TCP-з’єднання. Це перший крок, щоб встановити базове підключення до сервера.
Визначаємо таймаут сервера. У нашому прикладі таймаут становить 60 секунд, але краще припустити похибку в 5 секунд, тому встановимо його на 55 секунд.
SSL Handshake. Після встановлення TCP-з’єднання необхідно виконати SSL Handshake, щоб забезпечити захищене підключення.
Відправка незавершеного HTTP-запиту. Наприклад, стандартний запит:
GET / HTTP/1.1\r\n Host: httpbin.org\r\n Connection: keep-alive\r\n \r <-- Зверніть увагу! Останній \n відсутній!
Утримання з’єднання. Щоб не допустити завершення з’єднання через таймаут, приблизно на 54–55-й секунді ми відправляємо частину даних HTTP-запиту (наприклад, один байт тіла запиту). Це перезапускає таймер очікування на стороні сервера. Після цього ми продовжуємо утримувати з’єднання, періодично надсилаючи невеликі фрагменти незавершеного HTTP-запиту, кожного разу поновлюючи таймер.
Паралельне відкриття додаткових з’єднань. Відправка незавершеного запиту займає декілька мілісекунд або секунд, тому в нас є достатньо часу, щоб відкрити ще сотні або навіть тисячі нових з’єднань!
Як ви можете бачити, стратегія тримання з’єднання значно відрізняється від оригінальної Slow Loris атаки. В одному Connection: keep-alive з’єднанні можна відправити сотню запитів!
При швидкості 1 запит кожні 55 секунд і кількості 100 запитів — це тримання з’єднання 90 хвилин!
Звичайно, частина з’єднань може закритись, тому ми відкриватимемо їх знову.
Рахувати таймаут можна для кожного з’єднання окремо. Так буде простіше в розумінні та імплементації. Інший варіант — це групувати з’єднання та рахувати таймаут від першого сокета. Плюс цього способу в тому, що з нього витікає, що сервер отримає багато запитів в один момент, які будуть опрацьовувати в один момент.
Якщо ви хочете максимальне навантаження в один момент — груповий таймаут підходить краще.
Для гнучкого контролю кожного з’єднання — використовуйте окремий таймаут.
Не будемо занурюватися у деталі принципу роботи Event-Driven архітектури, проте саме її використання дозволить встановити величезну кількість з’єднань. Цей підхід набагато ефективніший, ніж застосування окремих потоків – уявіть собі, що створення 65 тисяч потоків виглядає як справжній кошмар.
Для реалізації цього буде сконструйовано цикл подій, який моніторитиме активність у сокетах, зокрема, коли вони готові до операцій читання або запису. Операційні системи надають для цього спеціальні системні виклики, серед яких можна виділити select() або epoll() в Linux, kevent() в деяких версіях FreeBSD та macOS, а також WaitForMultipleObjects() у Windows.
Python постачає вбудовану бібліотеку selectors, яка забезпечує високорівневе та ефективне мультиплексування введення/виведення. За замовчуванням, вона обирає оптимальне рішення для конкретної операційної системи (через DefaultSelector()), і саме цей механізм буде застосовано для управління з’єднаннями.
Основний план такий:
Створюємо обов’язково неблокуючий сокет:
import socket
ADDRESS = ("3.230.67.98", 443)
def connect():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setblocking(False) # !!! MUST HAVE
s.connect_ex(ADDRESS) # Використовуємо connect_ex, щоб уникнути виключення
return s
Створюємо селектор, який моніторить сокети. Метод .register() третім аргументом може містити будь-які дані. Ми будемо використовувати його для зберігання колбека — функції, яка викликатиметься, коли сокет буде готовий.
import selectors
sel = selectors.DefaultSelector()
def on_connected(sock):
print("Socket connected!!")
sock = connect()
sel.register(sock, selectors.EVENT_WRITE, on_connected)
SSL handshake:
import ssl
ctx = ssl.create_default_ctx()
req = b'GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r' # <-- no \n at the end
def delay(time, sock):
# Implement delay logic
sock.send(b'\n')
def on_ssl_handske_completed(sock):
# Handshake completed
sock.send(req)
delay(55, sock)
def on_connected(sock):
sock = ctx.wrap_socket(sock, ...)
try:
sock.do_handshake()
except ssl.SSLWantReadError:
... # Handle this with selectors
except ssl.SSLWantWriteError:
... # and this
while True:
events = sel.select()
for selector_key, event in events:
callback = selector_key.data
callback(selector_key.fileobj)
Однак, спробуйте імплементувати це самостійно — це буде чудовим проєктом для розуміння основ асинхронності.
Запустимо програму, яка створює велику кількість з’єднань і підтримує їх. Перше, на що хочу звернути увагу — після відкриття приблизно 29 тисяч з’єднань ми почали отримувати помилку:
OSError [errno 99] cannot assign requested address
Тому обмежимося цією кількістю. Кожні 55 секунд програма відправлятиме запит і виводитиме в термінал щось на кшталт:
Synced 25562 sockets
Де 25562 — це кількість активних сокетів. Звичайно, після кожного запиту деякі з’єднання могли закритися.
Щоб додатково перевірити кількість активних з’єднань, можна скористатися наступною командою:
netstat | grep 3.230.67.98 | grep ESTABLISHED | wc -l
У відповідь отримаємо число, близьке до кількості активних з’єднань у програмі. Похибка можлива лише через різницю в часі між відправкою запиту та перевіркою.
Незважаючи на досягнуту кількість з’єднань, цього все ж виявилося недостатньо, щоб вивести з ладу сервер httpbin.org, що свідчить про якісну конфігурацію останнього. Проте, відступати не передбачено!
Для продовження експерименту запропоновано провести повторну атаку із застосуванням віртуальних серверів, кожен з яких має свою унікальну IP-адресу. Це рішення дозволить суттєво збільшити кількість одночасних з’єднань і, відповідно, підвищити ефективність обраної стратегії.
Отже, наступний крок — масштабування атаки!
Для посилення нападу було використано три сервери, кожен з яких створював і підтримував по 28 тисяч з’єднань. Загалом це дало змогу встановити 84 000 одночасних з’єднань із сервером httpbin.org. Проте навіть за таких умов сервер залишався стабільним і не зазнав відмови.
Це не означає, що сама атака є неефективною. Насправді, це свідчить про відмінну конфігурацію сервера. Правильно налаштовані таймаути, обмеження кількості з’єднань від одного клієнта та інші захисні механізми запобігли успішній атаці.
Ось приклад, як виглядатиме результат на check-host.com для сервера, який вразливий до атаки:
Помилка Broken pipe вказує на те, що сервер, зокрема nginx, не має достатньо ресурсів для обробки великої кількості з’єднань, через що з’єднання не може бути встановлено або оброблено.
Вище, коли ми говорили про стратегію утримання з’єднання, була запропонована тактика групового таймауту, що призводить до одночасного надходження великої кількості запитів. У такій ситуації можна отримати Server Error. Це більше схоже на наслідок високого навантаження на бекенд, ніж на атаку Slow Loris.
httpbin.org демонструє приклад хорошої конфігурації сервера, який не вразливий до кількості з’єднань, що ми тестували. Однак, це не зупиняє нас на шляху до більш масштабних тестів — можливо, наступного разу все вийде! В той же час, є сервери, які вразливі навіть до 6 тисяч з’єднань, і не витримують навантаження.
Стаття аналізує принцип атаки Slow Loris із внесенням певних модифікацій у методику, оскільки класичний підхід уже не володіє тією вразливістю, що була раніше. Проте, сама вразливість зберігається в сучасних умовах за умови застосування правильних стратегій та масштабування атаки. Отже, відповідь на запитання про те, чи може цей метод вважатися вразливістю сьогодні, є позитивною: атакувати сучасні системи цим методом все ще можливо. Для досягнення успіху необхідно проводити численні експерименти та шукати альтернативні підходи, зокрема використання проксі-серверів, що дозволяє адаптувати методику до сучасних умов і підвищити ефективність атаки.