О скрытии сообщений в эмодзи и взломе казначейства США

в 13:01, , рубрики: beyond trust, postgresql, ruvds_перевод, базы данных, внедрение sql-кода, кибербезопасность, уязвимость нулевого дня

О скрытии сообщений в эмодзи и взломе казначейства США - 1


30 декабря 2024 года, пока большинство из нас суетились в преддверии празднования Нового года, Казначейство США готовило для Финансового комитета Сената США важное уведомление. В нём сообщалось, что его системы, которые, очевидно, содержат особо чувствительные конфиденциальные данные, были взломаны группой правительственных хакеров Китая.

Даже не знаю, как пропустил эту новость. Обычно я всё активно отслеживаю, особенно уязвимости опенсорсного ПО, которые касаются казначейства моей страны 🤷♂

И это ещё не самое безумное. Сейчас я расскажу вам, как именно они это сделали!

Начну с того, что это была старая добрая атака внедрением SQL-кода (подробнее об этих атаках скажу чуть позже).

За защиту серверов казначейства отчасти отвечает инструмент управления привилегированным доступом (Privileged Access Management, PAM) компании Beyond Trust (просто гениальное имя для поставщика услуг безопасности). К сожалению для них (да и для всех нас, у кого есть записи в казначействе (😅)), сотрудники Beyond Trust были вынуждены сообщить эту новость правительству, поскольку именно их ПО хакеры использовали в качестве точки входа.

Но давайте не будем спешить вешать всех собак на эту компанию. На её месте мог оказаться любой из нас, так как в основе уязвимости лежит PostgreSQL — одна из самых распространённых реляционных баз данных в мире.

Хотя здесь есть один нюанс — это была нетипичная явная уязвимость к внедрению SQL-кода (SQLi). Для осуществления подобной атаки необходимо, чтобы жертва использовала вывод внутреннего метода экранирования строк Postgres и передавала его непосредственно в psql (инструмент командной строки в Postgres). Или как об этом сказали специалисты самой PostgreSQL:

> В частности, для внедрения SQL-кода нужно, чтобы приложение использовало вывод функции для создания ввода в psql, то есть в интерактивный терминал PostgreSQL. — выдержка из отчёта PostgreSQL о регистрации уязвимости в системе CVE.

Как же так получилось, что в PostgreSQL более 9 лет просидела нераскрытой уязвимость нулевого дня? Причём ещё и уязвимость SQLi.

▍ Коротко о внедрении SQL-кода

Для тех, кто не в курсе, уязвимость к SQL-инъекции стара как мир.

«Не учи меня тайной магии, колдунья, она писалась при мне!» — Аслан (король Нарнии)

Внедрение SQL-кода длительное время являлось краеугольным камнем теории безопасности для разработчиков и исследователей. Обычно именно с этой темы начинается первая глава любого учебника по кибербезопасности.

Возьмём, к примеру, этот ужасный код Ruby:

# @Substack, добавь выделение синтаксиса. Обещаю, это не сложно.

require 'pg'

# Подключение к базе данных PostgreSQL.
conn = PG.connect(dbname: 'testdb', user: 'user', password: 'password')

# Получение ввода пользователя из командной строки.
puts "Enter your username:"
username = gets.chomp


# Простой запрос уязвим к SQL-инъекции.
result = conn.exec("SELECT * FROM users WHERE username = '#{username}'")

# Вывод результата.
puts result.getvalue(0, 0) # Предполагается, что в результате содержится столбец.

Здесь, если пользователь введёт в качестве имени admin' OR '1' = '1, фактически будет выполнен такой запрос:

SELECT * FROM users WHERE username = 'admin' OR '1' = '1'

Поскольку 1 == 1, в ответе вернутся записи всех пользователей из базы данных.

Конечно, это вымышленный пример, но можно экстраполировать использование аналогичного паттерна в гораздо более злостных целях — как, например, в истории с bobby tables (думали, я буду говорить о внедрении SQL-кода и не вспомню о bobby tables??)

Поэтому лучше записать запрос так:

conn.exec_params("SELECT * FROM users WHERE username = $1", [username])

В этом коде используется метод exec_params из пакета pg, который помещает на место нашего плейсхолдера $1 очищенное имя пользователя.

Примечание. Этот доработанный код Ruby включает так называемую «подготовленную инструкцию» (prepared statement). Подготовленные инструкции — это своеобразный стандарт индустрии по защите от SQLi.

Подобный вид атак настолько известен, что их существование воспринимается почти как данность. Все разработчики знают, что от них нужно защищаться. Поэтому весьма дико, что подобная уязвимость около 10 лет скрывалась в PostgreSQL, одном из самых пристально наблюдаемых опенсорсных проектов. Как вышло, что в системе, которую используют тысячи разработчиков и экспертов по безопасности, столь фундаментальная вещь оставалась незамеченной так долго.

Что ж, в данном случае атака внедрением SQL-кода была на порядок сложнее нашего вымышленного примера (или случая с bobby tables).

▍ Атака

В основе атаки лежало всего два байта: c0 27.

Примечание. Байты c0 27 представлены в шестнадцатеричном виде.

Далее для лучшего понимания процесса пробежимся по стеку вызовов.

Атакующий включил эти два байта в путь выполнения кода, который в конечном итоге достиг метода pg_escape_string.

pg_escape_string — это метод из PHP, применяемый для очистки ввода. В Beyond Trust он использовался в файле dbquote.php.

Пока вроде всё в порядке. Специалисты Beyond Trust проявили должную осмотрительность, применяя метод очистки к строке ввода пользователя, используемой в запросе PostgreSQL.

Но идём дальше…👇

По факту pg_escape_string не выполняет экранирование сам. Он вызывает функцию PostgreSQL PQescapeStringInternal.

PQescapeStringInternal является частью libpq, библиотеки C, предоставляющей интерфейсы для взаимодействия с сервером Postgres и, что важно, включающей инструмент командной строки psql. Вам libpq также может быть знакома как «одна из библиотек в Dockerfile, которую ради соответствия SOC 2 приходится патчить, закрывая обнаруженные уязвимости».

Метод PQescapeStringInternal существует именно для этого — чтобы «экранировать» строку, то есть очищать её, для безопасного использования в Postgres. Чтобы это реализовать, PQescapeStringInternal должен вызвать pg_utf_mblen, который отвечает за определение длины символов Юникода, состоящих из нескольких байтов.

А вот здесь стоит остановиться и внимательно рассмотреть pg_utf_mblen.

Через этот метод проходит любой текст в PostgreSQL, требующий экранирования.

// Код на момент 2025/03/13

int
pg_utf_mblen(const unsigned char *s)
{
	int			len;

	if ((*s & 0x80) == 0)
		len = 1;
	else if ((*s & 0xe0) == 0xc0)
		len = 2;
	else if ((*s & 0xf0) == 0xe0)
		len = 3;
	else if ((*s & 0xf8) == 0xf0)
		len = 4;
#ifdef NOT_USED
	else if ((*s & 0xfc) == 0xf8)
		len = 5;
	else if ((*s & 0xfe) == 0xfc)
		len = 6;
#endif
	else
		len = 1;
	return len;
}

(Ссылка на исходный код для самых пытливых умов).

Если вкратце, то этот метод нужен, потому что Postgres позволяет использовать эмодзи (не делайте этого).

CREATE TABLE "😀_table" (
  id serial PRIMARY KEY,
  description text
);

Несмотря на то, что внешне эмодзи представляет всего 1 символ, в кодировке UTF-8 он занимает 4 байта.

  • Символ ASCII A занимает 1 байт.
  • Символ é (являющийся частью расширенного набора Юникода) занимает 2 байта.
  • Символ 中 (китайский символ Юникода) занимает 3 байта.
  • Эмодзи 😀 в кодировке UTF-8 выглядит как F0 9F 98 80, то есть занимает 4 байта.

Так что при парсинге символа Юникода (например, эмодзи) Postgres нужно знать, сколько байтов он содержит.

Хорошо, теперь вернёмся к нашим двум байтам: c0 27.

Поскольку строка начинается с c0, pg_utf_mblen делает вывод, что длина этого символа Юникода равна 2 (видно в блоках if/else выше).

Postgres способна определить это, так как здесь мы имеем дело с кодировкой UTF. По сути, в первом байте хранится информация об общей длине символа UTF. 0xc0, он же 110xxxxx, означает, что перед нами два байта.

В итоге эта длина 2 используется в PQescapeStringInternal.

А вот и момент, где возникает баг.

Метод PQescapeStringInternal не проверяет, представлена ли строка, которую он парсит с помощью pg_utf_mblen, в кодировке Юникода. Он просто получает длину 2 и использует следующий байт, которым в нашем случае является 27.

Значение 27 представляет не что иное, как излюбленный символ для атак внедрением SQL-кода: ' (одинарную кавычку). PQescapeStringInternal слепо копирует эту кавычку, не экранируя.

И всё бы ничего, если бы ребята из Beyond Trust не написали функциональность, которая позволяла пользователям напрямую программно взаимодействовать с psql.

В итоге хакеры просто взяли в оборот эту неэкранированную кавычку и получили полный контроль над psql. Всего этого я не знал, пока не вник в инцидент. И если получение атакующим доступа к вашей БД через psql ещё не достаточно пугает, то по факту это также даёт ему возможность выполнять произвольные системные команды:

! <command>

// или...

! ./break_all_the_things

Так что, да… ничего хорошего.

Ну и прежде, чем подвести итоги, опять же, для самых пытливых умов дам ссылку на исправление бага.

▍ Выводы: строки, конечно, загадочны, но Юникод ещё загадочнее

В ходе своего расследования я наткнулся на меткое изречение одного из пользователей YouTube:

О скрытии сообщений в эмодзи и взломе казначейства США - 2

Ещё одним хорошим справочным ресурсом по теме будет Rust Book, Chapter 8.

Если говорить в целом, то строки — сложная штука. И в разных языках программирования эту сложность преподносят программистам по-разному. В Rust решили по умолчанию требовать корректную обработку данных String во всех программах, то есть программисту нужно изначально вдумчиво использовать данные в формате UTF-8. Такой компромисс раскрывает дополнительную сложность строк, которая не столь очевидна в других языках, зато избавляет вас от необходимости обрабатывать возможные ошибки, связанные с символами не в кодировке ASCII.

После внимательного изучения инцидента со взломом систем казначейства я гораздо лучше понял, как кодирование, хранение и обработка строк могут внести неожиданное поведение в любое ПО, даже PostgreSQL.

Например, Пол Батлер писал о том, как можно «Скрытно передавать данные через эмодзи». Так что да, теоретически вы можете закодировать безграничное количество произвольных данных в любой символ Юникода. Только представьте себе горизонты возможностей!

Всё это вдохновило меня более углублённо изучить строки и систему Юникод.

Вот вы, к примеру, знали, что символы Юникода определяются стандартами Юникода, которые регулируются одноимённым Консорциумом?

Ну и скучное же получилось предложение…

А знали ли вы, что за небольшую сумму в $5 000 можно зарегистрировать себе личный символ Юникода?

Вот это уже дело.

На данный момент такую эксклюзивную регистрацию оформили почти 100 человек. Среди них и те, кто сделал это в знак внимания своему возлюбленному, и крупные технологические компании, демонстрирующие Консорциуму своё почтение.

Вот пара моих любимых:

  • Oakland A (бейсбольная команда из главной лиги) потратила $15 000 на символы бейсбольного мяча⚾, слона 🐘 и листопадного дерева 🌳.
  • И сеть ресторанов Buffalo Wild Wings, зарегистрировавшая за собой символ окорочка ⁨🍗
  • Кому интересно, здесь полный список.

К слову, написать эту статью меня побудил YouTube-ролик на канале PwnFunction.

Надеюсь, она вам понравилась.

Автор: Bright_Translate

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js