В течение мно­гих лет ата­ки с исполь­зовани­ем SQL-инъ­екций в основном сво­дились к попыт­кам нарушить син­таксис зап­росов. Одна­ко с раз­вити­ем инс­тру­мен­тов акцент смес­тился на соз­дание «кру­тых наг­рузок» и раз­бор пре­дуп­режде­ний SAST, которые мно­гие игно­риру­ют. Я же поп­робовал поис­кать воз­можность инъ­екции без экра­ниро­вания — с мыслью о том, что на это у SAST или WAF не будет пра­вил.

Так я нащупал новую тех­нику для внед­рения в регуляр­ные выраже­ния. Сна­чала я нем­ного рас­ска­жу о тра­дици­онных методах, которые были нам извес­тны рань­ше, затем перей­дем к моим наход­кам. В ходе тес­тирова­ния мне уда­лось вскрыть уяз­вимость в 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

Вло­жен­ные кван­тифика­торы — это ког­да ты ста­вишь кван­тифика­тор (+, ?, *, {n,m}) не пря­мо к сим­волу, а к под­шабло­ну, который сам уже име­ет кван­тифика­тор. То есть как бы «умно­жаешь пов­торения».

Нап­ример, прос­то написать p+?+ нель­зя — такого син­такси­са нет. Что­бы добавить внеш­ний кван­тифика­тор, нуж­но сна­чала взять под­шаблон в скоб­ки: (p+?). Тог­да

  • p+ озна­чает «одна или боль­ше букв p»;
  • p+? (ленивый вари­ант) озна­чает «возь­ми минималь­но воз­можное количес­тво, но все рав­но хотя бы одну p». На стро­ке ppp это даст три сов­падения по одной p.

Ес­ли мы напишем (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

Сайт 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*)+b a+ внут­ри (1 или более a), сна­ружи (...)+ пов­торя­ется aaaac

При­мер скрип­та на Golang для демонс­тра­ции ReDoS:

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, который показы­вает зап­росы, содер­жащие стро­ку MySQL и не содер­жащие YourSQL:

mysql> SELECT * FROM articles WHERE MATCH (title,body)
-> AGAINST ('+MySQL -YourSQL' IN BOOLEAN MODE);

Я думаю, ты теперь видишь проб­лему: нет осо­бых мер, которые бы пре­дот­вра­щали выпол­нение этих спе­циаль­ных опе­рато­ров (кас­томных регуляр­ных выраже­ний), и сам зап­рос зак­лючен в кавыч­ки. Таким обра­зом, мы наш­ли неч­то, что:

  • не тре­бует экра­ниро­вания;
  • не обна­ружи­вает­ся ни WAF, ни SAST, ни DAST, ни дру­гими средс­тва­ми;
  • мо­жет при­вес­ти к утеч­ке дан­ных.

В отли­чие от при­выч­ных REGEXP, RLIKE или LIKE, конс­трук­цию MATCH ... AGAINST обыч­но не ассо­циируют с регуляр­ными выраже­ниями. Это выг­лядит прос­то как «поис­ковый опе­ратор», а не как regex. Из‑за это­го ввод в таких зап­росах час­то не филь­тру­ют от спе­циаль­ных сим­волов вро­де * или +.

Од­нако эти сим­волы в булевом пол­нотек­сто­вом поис­ке MySQL работа­ют поч­ти так же, как в регуляр­ных выраже­ниях, и могут серь­езно пов­лиять на резуль­тат. Поэто­му исполь­зование MATCH ... AGAINST может стать источни­ком уяз­вимос­тей: оно не тре­бует экра­ниро­вания, обхо­дит­ся мимо стан­дар­тных про­верок в защит­ном ПО и потен­циаль­но ведет к утеч­ке дан­ных.

Ос­новной век­тор ата­ки, на который сто­ит обра­тить вни­мание, — это фун­кции поис­ка, осо­бен­но те, которые показы­вают имя, но не отоб­ража­ют содер­жимое или показы­вают количес­тво докумен­тов с нуж­ным содер­жимым, но не выводят их текст. В общем, всё, что может извле­кать информа­цию о содер­жимом.

Да­вай поп­робу­ем про­верить это на прак­тике. Я ска­чал спи­сок опен­сор­сных веб‑при­ложе­ний, которые исполь­зуют базу дан­ных, и обна­ружил эту уяз­вимость в некото­рых из них. Самое быс­трое и единс­твен­ное на дан­ный момент исправ­ление сде­лала коман­да MyBB (CVE-2025-48941).

Преж­де чем раз­берем ее, при­веду спи­сок похожих фун­кций в дру­гих сис­темах управле­ния базами дан­ных.

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 и CVE-2025-48941. В сво­ей тес­товой сре­де я вклю­чил пол­нотек­сто­вый поиск (FTS). Выс­тавил зна­чение «Вре­мя перепол­нения поис­ка (секун­ды)» на 0, что­бы облегчить себе работу, но наличие нес­коль­ких акка­унтов или исполь­зование прок­си ока­зало бы тот же эффект (это зна­чение не столь важ­но, оно лишь уско­ряет работу уяз­вимос­ти). У меня есть две уда­лен­ные темы с заголов­ками jackie chan и 0ce3266d4eb71ad50f7a90aee6d21dcd.

 

Идентификация

Уда­лен­ные потоки вид­ны адми­нис­тра­тору при поис­ке, и фун­кция поис­ка такая же для адми­на, как и для поль­зовате­ля. Так что воп­рос в том, что имен­но будет вид­но.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии