AdaptAuthoringToolNosqli
AdaptAuthoringToolNosqli
Summary
This blog post highlights two vulnerabilities discovered in Adapt Authoring Tool: Incorrect
Access Control (CVE-2024-50671) and NoSQL Injection (CVE-2024-50672). The incorrect access
control vulnerability allows attackers with Authenticated User roles to disclose all registered
email addresses, while the NoSQL injection vulnerability enables unauthenticated attackers to
reset user and administrator passwords. Additionally, users with administrative privileges can
upload custom plugins. Although this is an intended feature, it can be exploited in conjunction
with the aforementioned vulnerabilities, allowing attackers with Authenticated User roles to
perform remote code execution on the backend server.
adapt_authoring/lib/permissions.js
/**
* Checks if a resource uri pattern matcher matches a resource uri
*
* @param {string} pattern - resource matching pattern from a statement
* @param {target} target - the resource uri to match against
* @return {boolean} true if it pattern includes target, false
otherwise
*/
return false;
});
return captured;
},
To authorize access to each endpoint, the application employs Role-Based Access Control
(RBAC), which specifies allowed actions for each user role through a predefined set of rules.
One key component of these rules is the resource matching pattern, which determines
whether a requested resource URI matches a specified pattern. The resource URI pattern-
matching logic, as shown in the code, checks for matches by splitting both the pattern and the
target resource URI into segments.
When a path segment in the resource matching pattern is * , the condition pattern[index]
=== '*' evaluates to true , causing the subsequent check, pattern[index] ===
target[index] , to be bypassed. However, the logic does not validate whether the number of
path segments in the pattern matches that of the target URI. As a result, patterns ending with
* inadvertently allow access to resource URIs with one fewer path segment, provided they
match the preceding pattern.
Since users with Authenticated User roles are allowed to perform the read action on
endpoints that match {{tenantid}}/api/user/* , attackers with this role can perform the
read action on {{tenantid}}/api/user to access the Get users feature and obtain all
registered email addresses.
adapt_authoring/plugins/auth/local/index.js
if (!resetPasswordToken) {
return next(new auth.errors.UserResetPasswordError('Token was not
found'));
}
usermanager.retrieveUserPasswordReset({ token: resetPasswordToken },
function (error, usrReset) {
if (error) {
logger.log('error', error);
return res.status(500).end();
}
if (!usrReset) {
return res.status(200).json({});
}
self.internalResetPassword({ id: usrReset.user, password:
req.body.password }, function (error, user) {
if (error) {
logger.log('error', error);
return res.status(500).end();
}
res.status(200).json(user);
});
});
};
adapt_authoring/lib/usermanager.js
/**
* Retrieves a single user password reset
*
* @param {object} search - fields to match: should use 'token' which
is unique
* @param {function} callback - function of the form function (error,
userReset)
*/
retrieveUserPasswordReset: function (search, callback) {
var timestampMinAge = this.xHoursAgo(MAX_TOKEN_AGE);
database.getDatabase(function(err, db) {
db.retrieve('UserPasswordReset', search, function (error, results)
{
if (error) {
return callback(error);
https://pages.dos-m0nk3y.com/blog/cve/Adapt Authoring Tool 0.11.3 - Authenticated Remote Code Execution 4/10
4/23/25, 10:57 PM Adapt Authoring Tool 0.11.3 - Authenticated Remote Code Execution | m0nk3y's Blog
}
if (!results || results.length === 0) {
return callback();
}
if (results.length > 1) {
return callback(new Error('User password reset search expected
a single result but returned ' + results.length + ' results'));
}
var resetData = results[0];
if (resetData.issueDate.getTime() < timestampMinAge) {
return callback(new Error('Reset token has expired'));
}
callback(null, resetData);
});
}, configuration.getConfig('dbName'));
},
adapt_authoring/lib/dml/mongoose/index.js
/**
* Implements Database.retrieve
*
* @param {string} objectType - the type of object to find, e.g. 'user'
* @param {object} search - fields to search on
* @param {object} [options] -
* @param {function} callback - of the form function (error, results)
...
*/
MongooseDB.prototype.retrieve = function(objectType, search, options,
callback) {
[...]
if (Model = this.getModel(objectType)) {
var query = {};
if (distinct) {
query = Model.distinct(distinct, search);
} else if (elemMatch) {
query = Model.find(null).elemMatch(elemMatch.collection,
elemMatch.query);
} else {
query = Model.find(search, fields);
}
[...]
if (!jsonOnly) {
query.exec(callback);
} else {
query.lean().exec(callback);
}
} else {
callback(new Error('MongooseDB#retrieve: Failed to retrieve model
with name ' + objectType));
}
};
The sanitizeFilter option is not explicitly set when using the find() function, causing it
to default to false . In this case, Mongoose does not sanitize the query filters against query
selector injection attacks.
sanitizeFilter : false by default. Set to true to enable the sanitization of the query
filters against query selector injection attacks by wrapping any nested objects that have a
property whose name starts with $ in a $eq .
Due to the lack of input validation and Mongoose's default behavior, unauthenticated
attackers can perform NoSQL injection to reset user and administrator account passwords.
adapt_framework/grunt/tasks/server-build.js
module.exports = function(grunt) {
grunt.registerTask('server-build', 'Builds the course without JSON
[used by the authoring tool]', function(mode) {
const requireMode = (mode === 'dev') ? 'dev' : 'compile';
grunt.task.run([
'_log-vars',
'build-config',
'copy',
'scripts:adaptpostcopy',
'schema-defaults',
'language-data-manifests',
'less:' + requireMode,
'handlebars',
'javascript:' + requireMode,
'replace',
'scripts:adaptpostbuild',
'clean:temp'
]);
});
};
The scripts task iterates through all installed plugins to check if the scripts.${mode}
property exists in the plugin's bower.json file. In this context, mode refers to the provided
argument—either adaptpostcopy or adaptpostbuild . If the property exists, its value,
which specifies the path to the compile-time script, is retrieved, and the corresponding
function within the script is executed.
adapt_framework/grunt/tasks/scripts.js
try {
const buildModule = require(path.join(process.cwd(), plugindir,
script));
buildModule(fs, path, grunt.log.writeln, {
...buildConfig,
...options,
plugindir
}, done);
} catch (err) {
grunt.log.writeln(chalk.red(err));
done();
}
}, taskCallback);
}
});
Users with Super Admin roles can upload custom plugins using the Upload plugin feature.
However, the system restricts most plugins from executing compile-time scripts. The
pluginsFilter() function ensures that only whitelisted plugins— adapt-contrib-xapi
and adapt-contrib-spoor by default—are allowed to execute these scripts.
adapt_framework/grunt/config/scripts.js
adapt_framework/grunt/helpers.js
const exports = {
[...]
scriptSafe: [
'adapt-contrib-xapi',
'adapt-contrib-spoor'
]
},
[...]
isPluginScriptSafe: function(pluginPath) {
return isIncluded;
},
[...]
scriptSafeFilter: function(filepath) {
return exports.isPluginScriptSafe(filepath);
},
Proof-of-Concept Exploit
Adapt Authoring Tool 0.11.3 - Remote Command Execution (RCE) | Exploit Database
https://pages.dos-m0nk3y.com/blog/cve/Adapt Authoring Tool 0.11.3 - Authenticated Remote Code Execution 9/10
4/23/25, 10:57 PM Adapt Authoring Tool 0.11.3 - Authenticated Remote Code Execution | m0nk3y's Blog