GLPI Core exposure map showing vulnerability scans, asset inventory, service desk, helpdesk, and external exposures

By Javier Medina ( X / LinkedIn)

TL;DR

Hardened perimeters mask HTTP replies, causing false positives or false negatives on standard scanners. This post shows how we mapped plugins from an updated and hardened GLPI SaaS instance by shifting from folder guessing to source code-driven path fuzzing.

By generating a wordlist from open-source plugin repositories and tracking application execution signals (302, 405, 500), we isolated active extensions, allowing us to prioritize endpoints that mapped back to local open-source code for detailed review.

We are releasing this approach as glpi-plugin-surface-mapper, a tool for identifying reachable GLPI plugin code paths before vulnerability research.

1# The starting point

In many mid-sized and enterprise environments, GLPI (Gestionnaire Libre de Parc Informatique) is widely used to combine ticketing, asset management (ITAM), and CMDB functions into a single platform.

As we showed in our previous GLPI Agent research, GLPI can also interact with that infrastructure, making any exposed instance a high-priority target during internal or perimeter assessments.

With these ingredients, during a recent assessment, we targeted a managed GLPI SaaS tenant. The instance was running a recent and updated version (v10.0.26) with common risk factors, such as the core API and external password reset workflows, disabled.

Our initial reconnaissance, using the well-known tool glpwnme, returned the following baseline:

[+] Version of glpi found: 10.0.26
[+] Operating system found: Unix
[+] GLPI root dir found: /var/www/glpi
[+] GLPI API is disabled
[+] GLPI password forget is disabled
[+] Inventory is enabled
[+] Plugins in use: glpiinventory, pdf, fields, glpicloud, advanceddashboard,
branding, anonymize, tag, news, actualtime
[!] Checking all exploits...
❌ Checking CVE_2020_15175
...
❌ CVE_2024_27937 Version not vulnerable
❌ CVE_2026_26026 Version not vulnerable

As expected on a managed tenant, public core CVEs were not viable. However, the output listed several plugins.

GLPI instances almost always rely on extensions to manage specific needs or tasks. These third-party components run on the same web context as the rest of the core code, but for the purposes of this analysis, they may not be subject to the same level of security controls as the core platform, making them a logical and, judging by the results, productive, place to look for vulnerabilities.

However, to do a proper source code review, we first needed an accurate inventory of which plugins, and which specific files, are deployed and were accessible.

2# GLPI Plugin Enumeration Caveats

Tools like glpwnme are excellent for rapid footprinting, but their plugin discovery logic operates through mechanisms that are designed for verification rather than inventory. Reviewing these mechanics shows why some approaches fail against hardened perimeters.

Passive HTML Extraction

The initial output showing active plugins doesn’t come from an active file scan. During init_session(), the tool installs a hook to parse the application’s HTML responses. It actively looks for translation and localization asset queries matching a specific regular expression.

PLUGINS_NAME_FORMAT = re.compile(r'/front/locale.php\?domain=(\w+)&', re.I)

If the web UI references a translation domain for a plugin, the scanner catches it, strips out the base glpi domain, and prints it. While highly accurate for the specific view rendered, this vector is passive. It depends on whether a plugin exposes string locales in the specific UI context observed by the tool. And so, although it is fast and effective, the way it works can lead to false negatives.

Active Folder Probing

When forced into active discovery mode via --list-plugins, the tool switches to a static, hardcoded array (plugins_list) containing, at the time of our assessment, 42 common plugin names, such as fusioninventory, glpiinventory, fields, or formcreator.

For each entry, it tests two base endpoints without allowing redirects:

GET /plugins/<plugin_name>/
GET /marketplace/<plugin_name>/

It flags a plugin as “found” if the server returns either an HTTP 200 OK or an HTTP 403 Forbidden.

On managed SaaS platforms or similar hardened setups, this root-directory checking strategy fails. If the server is configured to return a blanket 403 Forbidden or a global redirect, the scanner treats every check as a positive match.

Against our client’s SaaS instance, this resulted in a plugin list filled with false positives:

[+] Plugin useditemsexport found on /plugins
[+] Plugin glpiinventory found on /plugins
[+] Plugin fusioninventory found on /plugins
[+] Plugin addressing found on /plugins
[+] Plugin archibp found on /plugins
[+] Plugin collaborativetools found on /plugins
[+] Plugin behaviors found on /plugins
...
[+] Plugin advanceddashboard found on /plugins
[+] Plugin agentconfig found on /plugins
[+] Plugin manufacturersimports found on /plugins

The scanner cannot differentiate between a legitimate restricted plugin folder and a global server-level access rule.

This makes standard root-folder probing unviable for systematic analysis on hardened targets. If a global access rule masks the true state of the filesystem, an alternative is to probe for internal files that can only exist if the plugin is installed.

3# Deploying a Better Approach for Fuzzing

To automate this process, we generated a wordlist derived from the source code of the GLPI plugins. Instead of relying on a generic wordlist, we downloaded the publicly available plugin repositories and analyzed the structure of their internal files to see what paths are exposed after installation.

The builder script recursively crawls plugin source code, indexing valid file formats (.php, .phtml, .inc) while filtering out test suites or static documentation.

python3 glpi_plugin_wordlist_builder.py \
--out-dir glpi-plugins-wordlist \
--php-extensions .php,.phtml,.inc

Instead of compiling raw filenames, the builder also structures each entry to preserve the parent plugin directory as a prefix:

fields/front/container.php
fields/ajax/dropdownFields.php
glpiinventory/ajax/taskjob.php
[...]

This layout matches the physical directory structure of the application. GLPI extensions expose their public entry points, such as UI forms or AJAX handlers, as standard PHP files inside subfolders like front/ or ajax/. Because the web server serves these files from disk, the public URL follows the path of /plugins/<plugin_name>/<internal_path> or /marketplace/<plugin_name>/<internal_path>.

By keeping the plugin directory attached to the filename, each entry becomes a functional URL path. This allows us to test the entry points against the web root, rather than guessing folder names and guessing what might be inside.

4# Scanning and Behavioral Analysis

Because the security industry suffers from a severe shortage of custom Python wrappers around HTTP requests, we wrote glpi_plugin_scanner.py.

You don’t have to thank us.

Obviously, a standard fuzzer could have sent the same requests. The useful part was expanding each canonical path across /plugins/ and /marketplace/, preserving the plugin context, classifying response behavior and producing output that could be mapped back to the local source tree for review.

The scanning script takes the compiled wordlist, containing 10,553 unique paths in this iteration, expands them across both the legacy /plugins/ and modern /marketplace/ roots, and runs the fuzzing routine.

python3 glpi_plugin_scanner.py \
https://[redacted].com \
glpi-plugins-corpus/php_unique_canonical_paths.txt \
-o scan_results.csv

Execution against the hardened SaaS tenant yields the following summary:

[INFO] summary:
[INFO] paths: 10553
[INFO] attempts: 21106
[INFO] signals: 123
[INFO] errors: 0
[INFO] status_counts: {'200': 32, '302': 88, '403': 16874,
'404': 4109, '405': 2, '500': 1}
[INFO] verdict_counts: {'baseline_like': 4109, 'direct_hit': 32,
'method_not_allowed': 2, 'protected_or_forbidden': 16874, 'redirect_signal': 88,
'server_error': 1}
[INFO] outputs written under: glpi-plugin-scan

Analyzing the CSV means separating the server’s default behavior from actual application responses. The 16,874 403 Forbidden and 4,109 404 Not Found responses represent the baseline—pure noise.

In contrast, the 32 200 OK responses, 88 302 Found redirects, two 405 Method Not Allowed errors, and the single 500 Internal Error prove the requests bypassed perimeter controls and reached the application layer.

This highlights another limitation of passive discovery. Parsing locale.php?domain= links from the UI often shows commercial extensions exclusive to the GLPI Network enterprise tier. Auditing closed-source SaaS modules without the underlying code offers a poor effort-to-reward ratio.

The source-driven approach fixes this. Because the wordlist comes from open repositories, every valid response maps directly to local source code, exposing the exact files accessible on the server.

5# Thoughts on Real Reconnaissance

The takeaway from this assessment is that classic black-box fuzzing fails against hardened perimeters. When WAFs, proxies, or access policies mask the filesystem, treating the web root as a mystery leads to noise.

Using the open-source blueprints of the GLPI ecosystem fixes this. Instead of guessing, known file paths force the backend to reveal its state.

During this assessment, this approach isolated six active open-source plugins on the server. Instead of guessing what was hidden on disk, the reconnaissance phase was completed with a verified inventory based on real source code paths.

Part II, following the vendor’s fix, will cover the code review, the vulnerability found in one of these installed plugins and the functional exploit of the discovered CVE.