As part of our recent research activity, we stumbled upon FormaLMS. The project is an open source Learning Management System built by forma.association and aimed at companies who want a learning platform for internal employees, partners, dealers and sellers.
The project is opensource and could be downloaded from the main website: formalms.org and the tested version is the 2.3.
Update: the exploit is working on version <= 2.4.4
Responsible disclosure timeline
Responsible disclosure timeline
Digging into the source
The application is written in PHP language and it uses custom routing logic to forward the requests to the specific controller. The architecture resemble the Model-View Controller one in a weird way and the requests are routed through the "front controller", which is the
index.php file located in the root of the project.
The routing mechanism
Once received, the front controller is responsible to parse the requested route via the
r query string parameter, split the string by
/ and determine which controller and module to load (the green rectangle).
A quick analysis on the
_homepagecatalog_base_ constants shows these values:
So we can now reconstruct the routing mechanism:
- First part of the
rparameter is the controller name (the red mark)
- Second part is the module name (green mark)
- Third part is the method name (orange mark)
- A "Controller" static suffix is appended at the end (yellow mark)
The authorization mechanism
While it is obvious that the routing mechanism allows every user to arbitrarily call any class, the authorization mechanism denies some actions to the administrative part of the application such as the
So, in summary:
- The front controller,
index.php, instantiate a new class based on the
rinput parameter and calls a constructor with the mvc name.
- The destination class,
AdminrulesAdmControllerin our case, which extends the
AdmControllerand is a subclass of the
Controllerclass, calls a constructor with the mvc name and, afterwards, the init function
init()function of the "AdmController" is called and the
checkPerms()are invoked based on the definition of the
COREconstant, which is declared in various points of the code base.
While we had no luck trying to circumvent the authorization mechanism, one part of the
index.php front controller caught our attention.
Apparently, if the parameters
token are passed to the query string, the application flows to a different location defined in the
And, lastly, the part that clearly helped us to find the bug:
While it looks like that the developers were aware of this ugly default value ("orribile questo default" translates to "This is an horrible default value"), this was the part that motivated us to investigate further.
Digging into the SSO function
Until now, we know that in order to access the
sso functionality, the
sso_token setting should be enabled.
The "sso" setting is present in the admin section of the website and, as shown in the screenshots above, it accepts a NULL value for the
Exploiting the flaw
Knowing this, it is just a matter of sending an HTTP request on the index.php which contains:
login_user: the target username
time: the expiration of the session in epoch format. Adding some time will be useful to circumvent the expiration check
token: calculated like the following:
$recalc_token = strtoupper(md5($login_user . ',' . $time . ',' . $secret));
sso_secret: the default value, which is:
8ca0f69afeacc7022d1e589221072d6bcf87e39c or, in some cases, empty if the value of
SSO secret for the token hash is set to empty.
Following a simple PoC script that automates the process:
And, finally, the bypass:
Following a nuclei template, which could be useful for testing the vulnerability on single/multiple targets.
Note: the exploit uses blank value for the SSO Secret value, feel free to change it accordingly
The exploit needs environment variables in order to generate correct time in epoch format and token.