Содержание статьи
Так я нащупал новую технику для внедрения в регулярные выражения. Сначала я немного расскажу о традиционных методах, которые были нам известны раньше, затем перейдем к моим находкам. В ходе тестирования мне удалось вскрыть уязвимость в MyBB, которая позволяла просматривать названия удаленных тем без аутентификации.
Традиционные техники
В этом разделе разберем традиционные методы поиска уязвимостей, связанных с регулярками. Но прежде чем углубляться в детали, давай разберемся, чем отличаются регулярные выражения, подстановочные знаки и операторы.
ReDoS (не про операционку)
Регулярное выражение (regex) — это последовательность символов, определяющая шаблон для поиска в текстах. Главное преимущество regex — это возможность задавать сложные шаблоны строк.
Подстановочные знаки — это символы, которые заменяют собой ноль или более символов. В SQL для этого используют процент (%
) и подчеркивание (_
), а в regex — точку (.
) и звездочку (*
).
Операторы — это логические символы, например AND
, OR
, NOT
, =
, !
, <
, >
, >
, <
, +
, -
, *
, /
. При обработке данных оператор звездочка используется для расчетов, так что не путай их с подстановочными знаками.
Откат — это метод решения задач, при котором ты возвращаешься к предыдущей точке выбора (откат) в процессе и пробуешь другой вариант, если текущий путь не приводит к нужному результату. Это похоже на блуждание в лабиринте с множеством путей. Ты пробуешь один путь, но натыкаешься на тупик. Тогда ты откатываешься назад — возвращаешься к последнему перекрестку, где у тебя были другие варианты, — и пробуешь другое направление. Ты продолжаешь повторять этот процесс, пока не найдешь выход или не исчерпаешь все возможные пути.
Квантификатор указывает, сколько раз предыдущий элемент должен повториться, чтобы произошло совпадение.
Квантификатор | Значение | Регулярное выражение | Соответствие |
---|---|---|---|
* | Ноль или более раз | p* |
пустая строка, p , ph , ... |
+ | Один или более раз | p+ |
p , ph , phr
|
? | Ноль или один раз | p? |
пустая строка, p
|
{n} | Ровно n раз | p{3} | ppp |
{n,} | n или более раз | p{2,} |
pp , ppp , ... |
{n,m} | От n до m раз | p{2,4} |
pp , ppp , pppp
|
Вложенные квантификаторы — это когда ты ставишь квантификатор (+
, ?
, *
, {
) не прямо к символу, а к подшаблону, который сам уже имеет квантификатор. То есть как бы «умножаешь повторения».
Например, просто написать p+?+
нельзя — такого синтаксиса нет. Чтобы добавить внешний квантификатор, нужно сначала взять подшаблон в скобки: (
. Тогда
-
p+
означает «одна или больше букв p»; -
p+?
(ленивый вариант) означает «возьми минимально возможное количество, но все равно хотя бы одну p». На строкеppp
это даст три совпадения по однойp
.
Если мы напишем (
, совпадение не изменится — просто появилась группа, которая «запоминает» то, что нашлось внутри.
А если поставить внешний квантификатор: (
, то это значит «подшаблон p+?
(одна буква p
) повторяется один или более раз».
На строке ppp
:
- внутренний шаблон берет по одной p за раз;
- внешний + заставляет повторять процесс, пока есть буквы;
- итоговое совпадение целиком — ppp;
- а группа (скобки) «помнит» только последний результат подшаблона, то есть одну p.
Шаблон | Значение шаблона | Регулярное выражение | Результат совпадения |
---|---|---|---|
(...) | группирует подшаблон | (p+?)+ | "ppp" (совпадение с шаблоном), "p" (совпадение с группой) |
ReDoS (Denial of Service через регулярные выражения) — это уязвимость, при которой неправильно составленное регулярное выражение позволяет злоумышленнику ввести такой текст, который заставляет движок выполнять много ненужных проверок, что значительно замедляет работу программы и вызывает отказ в обслуживании. Часто ReDoS возникает, если в регулярных выражениях используются вложенные квантификаторы.
Чтобы создать ReDoS-пейлоад, наш внутренний квантификатор должен захватывать как можно больше. Поэтому субпаттерн может выглядеть как a+
или a*
. Если на входе будет aaaac
, это совпадет с aaaa
. Затем мы можем создать вложенный квантификатор, используя a*
как субпаттерн, добавив квантификатор +
. Таким образом, группа a*
будет повторяться до успеха. Наше новое регулярное выражение: (
.
Если на входе aaaac
, то первое совпадение — это aaaa
. Поскольку звездочка (*
) означает «ноль или более», мы также получаем совпадения нулевой длины. Например, после того как aaaa
найдено, есть пустое совпадение между aaaa
и c
и еще одно пустое совпадение после c
, что в сумме дает три совпадения (два из которых пустые).
Мы можем добиться несоответствия после этих совпадений, изменив шаблон так, чтобы он ожидал строк, заканчивающихся на b, даже если наша строка заканчивается на c: (
, а наш ввод останется aaaac
. Часть (
может соответствовать aaaac
множеством способов, потому что a*
может соответствовать нулю или более a, а квантификатор + позволяет провести несколько таких совпадений.
Каждый вариант того, как именно подшаблон (
может разобрать часть строки, называется его возможным результатом. Когда весь шаблон (
не совпадает сразу, движку приходится «отматывать назад» и пробовать другой результат для (
, чтобы проверить, не получится ли продолжить совпадение и найти b
.
www
Сайт regex101.com поможет тебе в составлении и тестировании регулярных выражений.
Чем больше ввод, тем больше создается возможностей для обратного отслеживания, что может привести к потенциальной DoS-атаке.
Например (чисто для демонстрации), рассмотрим, как можно разделить aaaa
на одну или несколько непустых групп a. Большинство движков регулярных выражений работает не совсем так, но зато наглядно видна концепция множества способов сопоставления.
1: ("aaaa")
2: ("aaa")("a")
3: ("aa")("aa")
4: ("a")("aaa")
5: ("aa")("a")("a")
6: ("a")("aa")("a")
7: ("a")("a")("aa")
8: ("a")("a")("a")("a")
Шаблон | Описание | Ввод |
---|---|---|
( |
a+ внутри (1 или более a), снаружи (...) повторяется |
aaaac |
Пример скрипта на Golang для демонстрации ReDoS:
package mainimport ( "fmt" "os" "os/exec" "os/signal" "strings" "syscall" "time" "github.com/dlclark/regexp2")func main() { fmt.Println("Демонстрация ReDoS") count := 200 pid := os.Getpid() re := regexp2.MustCompile((a*)+b, 0) input := strings.Repeat("a", count) + "c" sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) done := make(chan struct{}) go func() { <-sigs; close(done); os.Exit(0) }() go func() { re.MatchString(input); close(done) }() for { select { case <-done: return default: out, _ := exec.Command("ps", "-p", fmt.Sprintf("%d", pid), "-o", "%cpu=").Output() cpu := strings.TrimSpace(string(out)) fmt.Printf("Использование CPU: %s\n", cpu) time.Sleep(500 * time.Millisecond) } }}
Ответ:
khatai@5df0825ade8a tmp % go run main.go
ReDoS PoC
CPU usage: 0.0
CPU usage: 80.2
CPU usage: 97.7
CPU usage: 100.0
CPU usage: 100.0
REGEXP, RLIKE и другие способы поиска в строках
Регэксы встречаются не только в приложениях, но и в СУБД — для поиска строк в базах данных. Возьмем, к примеру, команду REGEXP
из MySQL. Она сама по себе может привести к утечке информации, и здесь не помогут подготовленные запросы, поскольку проблема не в самих регулярных выражениях. Все сводится к небезопасной реализации.
Рассмотрим такой запрос:
SELECT Name FROM Data WHERE Content REGEXP '^?'
Использование подготовленных запросов или экранирование (preg_quote
) не решат проблему. Если пользователь введет точку, то вся конструкция будет выглядеть следующим образом:
Входные данные: '.'Обрезано: '.*'Подготовлено для preg_quote() и экранировано: '.*'Итоговый шаблон REGEXP: '^.*'SELECT Name FROM Data WHERE Content REGEXP '^.*';
Такие проблемы легко решаются, если добавить колонку «видимость» или предусмотреть защиту от случаев с регулярными выражениями. Во многих приложениях обычно уже предусмотрены механизмы для предотвращения подобных ситуаций. Поэтому нам понадобится функция SQL, которая примет нашу «последовательность символов, указывающую шаблон сопоставления в тексте» (то есть регэкс), но которая не указана как regex-функция в документации MySQL.
Небезопасные «безопасные» реализации
Давай возьмем в качестве примера real_escape_string
; все вроде бы хорошо, кроме того, что она заэкранирует одинарные и двойные кавычки. Но если посмотреть на функции вроде backup
, ты заметишь, что в большинстве систем они не используют ни одинарные, ни двойные кавычки и вместо этого принимают имена таблиц в обратных апострофах (бэктиках).
Бэктики используются в командах REPAIR
, EXPORT
, OPTIMIZE
, ANALYZE
, TRUNCATE
, ALTER
и прочих. Это может показаться странным, но я считаю это проблемой неудачного проектирования, а не реализации. В функции C API mysql_real_escape_string_quote
обратные апострофы экранируются, в то время как другие методы этого не делают. Перечисленное — всего лишь пример, так сказать, заметка на полях.
Настоящая сила кроется в функциях полнотекстового поиска. Они используются в большинстве программ, особенно часто — в блогах, системах управления обучением, форумах и прочих движках сайтов — для более продвинутого поиска.
Статья Википедии гласит:
Полнотекстовый поиск относится к методам поиска в отдельном документе, хранящемся на компьютере, или в коллекции в полнотекстовой базе данных.
При выполнении полнотекстового поиска (или FTS) системы управления базами данных используют специальные символы с определенными значениями. Эти символы также задают шаблон поиска, который определяет регулярное выражение, но не простое, а особое.
MySQL поддерживает режим булева полнотекстового поиска с помощью специальных операторов булева режима. Синтаксис выглядит так:
MATCH (col1,col2,...) AGAINST (expr [search_modifier])
Но прежде чем углубляться в это, вот простая таблица, показывающая разницу между привычными нам регулярными выражениями и операторами булева режима.
Символ | Обычный регулярный | Режим булевой логики MySQL |
---|---|---|
+ | Один или более | Слово должно присутствовать |
- | Без специального значения | Слово не должно присутствовать |
* | Ноль или более | Универсальный символ |
^ | Начало строки или линии | Без специального значения |
$ | Конец строки или линии | Без специального значения |
. | Универсальный символ | Без специального значения |
() | Группировка подпаттернов | Группировка подвыражений |
[] | Любой символ из набора | Без специального значения |
{n,m} | От n до m | Без специального значения |
"" | Без специального значения | Точная последовательность слов |
< | Без специального значения | Увеличивает вес термина |
> | Без специального значения | Уменьшает вес термина |
~ | Без специального значения | То же, что и уменьшение веса |
Пример запроса из документации MySQL, который показывает запросы, содержащие строку MySQL
и не содержащие YourSQL
:
mysql> SELECT * FROM articles WHERE MATCH (title,body) -> AGAINST ('+MySQL -YourSQL' IN BOOLEAN MODE);
Я думаю, ты теперь видишь проблему: нет особых мер, которые бы предотвращали выполнение этих специальных операторов (кастомных регулярных выражений), и сам запрос заключен в кавычки. Таким образом, мы нашли нечто, что:
- не требует экранирования;
- не обнаруживается ни WAF, ни SAST, ни DAST, ни другими средствами;
- может привести к утечке данных.
В отличие от привычных REGEXP
, RLIKE
или LIKE
, конструкцию MATCH ...
обычно не ассоциируют с регулярными выражениями. Это выглядит просто как «поисковый оператор», а не как regex. Из‑за этого ввод в таких запросах часто не фильтруют от специальных символов вроде *
или +
.
Однако эти символы в булевом полнотекстовом поиске MySQL работают почти так же, как в регулярных выражениях, и могут серьезно повлиять на результат. Поэтому использование MATCH ...
может стать источником уязвимостей: оно не требует экранирования, обходится мимо стандартных проверок в защитном ПО и потенциально ведет к утечке данных.
Основной вектор атаки, на который стоит обратить внимание, — это функции поиска, особенно те, которые показывают имя, но не отображают содержимое или показывают количество документов с нужным содержимым, но не выводят их текст. В общем, всё, что может извлекать информацию о содержимом.
Давай попробуем проверить это на практике. Я скачал список опенсорсных веб‑приложений, которые используют базу данных, и обнаружил эту уязвимость в некоторых из них. Самое быстрое и единственное на данный момент исправление сделала команда MyBB (CVE-2025-48941).
Прежде чем разберем ее, приведу список похожих функций в других системах управления базами данных.
DBMS | Функция/предикат полнотекстового поиска |
---|---|
MySQL | MATCH( |
PostgreSQL | to_tsvector( |
SQL Server | CONTAINS( |
Oracle DB | CONTAINS( |
IBM Db2 | CONTAINS( |
Разбираем кейс MyBB
Давай разберемся с MyBB и CVE-2025-48941. В своей тестовой среде я включил полнотекстовый поиск (FTS). Выставил значение «Время переполнения поиска (секунды)» на 0, чтобы облегчить себе работу, но наличие нескольких аккаунтов или использование прокси оказало бы тот же эффект (это значение не столь важно, оно лишь ускоряет работу уязвимости). У меня есть две удаленные темы с заголовками jackie
и 0ce3266d4eb71ad50f7a90aee6d21dcd
.
Идентификация
Удаленные потоки видны администратору при поиске, и функция поиска такая же для админа, как и для пользователя. Так что вопрос в том, что именно будет видно.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее