7 октября 2019 года директор отдела исследований PortSwigger (производителя BurpSuite) опубликовал исследование о новых подходах к HTTP Request smuggling. С их помощью он заработал на bugbounty около $70000. В этой заметке мы коротко выясним суть атаки, инструментарий, а также методики исследования веб серверов, подверженных этой уязвимости.
Что такое HTTP Request Smuggling
HTTP Request smuggling — атака, направленная на рассинхронизацию frontend веб-сервера и backend веб-сервера, в результате чего злоумышленник может "контрабандой" пронести HTTP запрос мимо Frontend сервера. Картинка из оригинальной статьи служит хорошей демонстрацией:
Подобная атака может привести к самым различным последствиям — внедрению XSS в сессии других пользователей, редиректу пользователей на сторонние ресурсы, отравлению кэша сервера, подобию SSRF и ряду других.
В инкарнации HTTP Smuggling 2019 года Джеймс Кеттл эксплуатировал некорректную обработку веб-серверами заголовка
Transfer-Encoding: chunked
сообщающую, что тело сообщения будет передано частями (RFC). Из-за того, что некоторые веб-серверы не поддерживают chunked передачу, либо по-разному обрабатывают заголовок, frontend будет "видеть" только один запрос, а backend его распознает как два. Более подробно с деталями атаки можно ознакомиться в оригинальной статье, там же есть практическое задание, на котором можно потренироваться находить уязвимость вручную.
Для быстрого поиска Джеймс разработал плагин для BurpSuit, на вход принимающий запрос, а на выходе создающий отметку об уязвимости сервиса (если таковая найдется).
Примеры уязвимых веб-серверов
Нужно сказать, что проблематикой smuggling и других уязвимостей, связанных с работой веб-серверов давно занимается другой исследователь под ником regilero. За последние три года он опубликовал три статьи, описывающие найденные им уязвимости в популярных веб-серверах, большинству из которых присвоены CVE средней и высокой степени критичности. В числе уязвимых серверов — Apache Traffic Server, Jetty, Apsis.
На волне интереса к проблеме, другой исследователь — Nathan Davison, обнаружил уязвимость в HAProxy, который игнорировал некорректно сформированный заголовок
Transfer-Encoding:[\x0b]chunked
и преобразовывал его в следующий вид:
Transfer-Encoding:
chunked
Но backend сервер — gunicorn, проксирующий приложение на Flask, заголовок считывал, что и провоцировало уязвимость.
Несколько позже ряд других исследователей обнаружили уязвимость (присвоена CVE-2019-16276) в реализации http сервера golang — сервер нормализировал заголовок, если перед доветочием стоял пробел.
Запрос:
После обработки сервером:
Уязвимость могла быть проэксплуатирована в случае, если frontend сервер игнорировал заголовок с пробелом, и использовал Content-Length для подсчета размера запроса.
Веб-сервер Caddy, написаный на Go, также был уязвим, поскольку использовал ту же самую библиотеку net/http. Разработчики подтвердили, что после обновления GO и пересборки пакета, проблема исчезает.
Автор данной статьи нашел аналогичную проблему в сервере lighthttpd (CVE присвоена не была). На скриншоте видно, что сервер принимает и обрабатывает заголовок, содержащий пробел:
Разработчики не совсем согласны с RFC 7230 (да и автор тоже), и считают что ответственность за некорректную обработку заголовков лежит на прокси, которые пересылают запросы без их нормализации и проверки. Тем не менее, в новом релизе баг будет пофикшен:
By default, lighttpd parses (and normalizes) requests before reverse-proxying them to backends. Doing so thwarts the attacks mentioned in https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn to servers upstream from lighttpd.
However, as mentioned by stbuehler above, proxies downstream from lighttpd might pass anything to lighttpd.
The change that will be made in the next release of lighttpd will be to reject requests with space or tab after field-name and before the colon, but only when lighttpd is configured in the (default) mode of strict http header parsing.
Такой же баг обнаружен в веб сервере cheroot, используемом фреймворком cherrypy. Этот мини-фреймворк встречается в стартапах, его часто используют для написания API. Баг-репорт висит в статусе opened.
Необходимые и достаточные условия для уязвимости
Итак, что же является необходимыми условиями для проверки и эксплуатации уязвимости:
- POST запрос. Хотя RFC в явном виде не запрещает использование заголовков Content-Length и Transfer-Encoding для GET запросов, по факту они используются только в POST запросах.
- Наличие frontend и backend серверов — если их не будет, нечему будет рассинхронизироваться.
- Веб-серверы должны по-разному парсить заголовок Transfer-Encoding, то есть один должен его "считывать", а второй игнорировать.
Тестирование в лабораторных условиях
Для лучшего понимания и поиска проблем в существующих веб-серверах и прокси разумно развернуть тестовую среду у себя локально с помощью Docker.
Пример схемы тестовой среды:
Код приложения:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def main():
# the next line is required for Transfer-Encoding support in the request
request.environ['wsgi.input_terminated'] = True
headers = {}
for header in request.headers:
headers[header[0]] = header[1]
print (request.data)
print (headers)
return jsonify(body=str(request.data), headers=headers)
Файл с настройками фронтенда, на примере caddy:
localhost:80
log ../access.log
proxy / host.docker.internal:8888
Дальше все просто, запускаем приложение:
gunicorn --keep-alive 10 -k gevent --bind 0.0.0.0:8888 -w 4 backend:app
И контейнер:
docker run -d -t --name caddy -p 80:80 -p 443:443 -v /Users/sun/work/caddyfile:/etc/Caddyfile abiosoft/caddy:latest
Из магазина Burp устанавливаем HTTP Request Smuggler и Logger ++ для удобства отладки. Далее в Repeater формируем простой запрос, например такой:
POST / HTTP/1.1
Host: localhost
Content-Length: 8
Connection: close
body=123
И отсылаем его, чтобы проверить что все настроено корректно:
HTTP/1.1 200 OK
Content-Length: 202
Content-Type: application/json
Date: Mon, 07 Oct 2019 13:17:18 GMT
Server: Caddy
Server: gunicorn/19.9.0
Connection: close
{"body":"b'body=123'","headers":{"Accept-Encoding":"gzip","Connection":"close","Content-Length":"8","Host":"host.docker.internal:8888","User-Agent":"Go-http-client/1.1","X-Forwarded-For":"172.17.0.1"}}
Теперь запускаем Launch Smuggle Probe и смотрим на ответы.
Самое интересное начинается именно в этот момент. Нужно анализировать запросы и ответы, чтобы понять уязвимы сервисы или нет. Эту часть оставим для пытливого читателя.