Update 1/2/24
According to our sensor network, SonicWall is seeing a large number of exploitation attempts of
CVE-2023-51467. We highly recommend upgrading to Apache OFBiz version 18.12.11 or newer.

Overview
SonicWall Capture Labs threat research team has discovered an Authentication Bypass vulnerability being tracked as
CVE-2023-51467 with a CVSS score of 9.8. It was discovered while researching the root cause for the previously disclosed CVE-2023-49070. The security
measures taken to patch CVE-2023-49070 left the root issue intact and therefore the authentication bypass was still present. Apache OfBiz is an open-source Enterprise Resource Planning (ERP) system. It may seem unfamiliar, but as part of the software supply chain it has a wide install base in prominent software, such as Atlassian’s JIRA (used by over
120K companies). As a result, like with many supply chain libraries, the impact of this vulnerability could be severe if leveraged by threat actors. Our research demonstrates that this flaw could lead to the exposure of sensitive information or even the ability to execute arbitrary code as demonstrated in the short video below using version 18.12.10, where the system “ping” application is executed by an unauthenticated attacker.
SonicWall is committed to helping provide defenders with the necessary resources to protect their organizations. As part of this effort, we responsibly disclosed the discovered vulnerability to Apache OFBiz providing them advanced noticed with the intent that patches or other mitigation strategies can be deployed. We advise anyone using Apache OFbiz to update to version 18.12.11 or newer immediately. In addition to the patch, SonicWall has developed IPS signature IPS:15949 to detect any active exploitation of this vulnerability.
Technical Analysis and Discovery
We were intrigued by the chosen mitigation when analyzing the
patch for CVE-2023-49070 and suspected the real authentication bypass would still be present since the patch simply removed the XML RPC code from the application. As a result, we decided to dig into the code to figure out the root cause of the auth-bypass issue. As anticipated, the root issue was in the login functionality. We focused our analysis on the
LoginWorker.java file in order to understand the flow of data within the various functions and checks during the authentication process. This led us to run a couple of testcases which we have outlined below to examine the authentication functionality using Apache OFbiz version 18.12.09. For testing, we started by using the publicly available
poc1 and
poc2 for CVE-2023-49070.
Testcase 1
Our first test case was based on using empty
USERNAME and
PASSWORD parameters while including the parameter
requirePasswordChange=Y in URI This test was derived from the testing of CVE-2023-49070 during our signature development to ensure detection in all use cases. The question was posed, what if there is no username and password in the request? For instance, the request might look like https[:]//www.example.com:8443/webtools/control/xmlrpc/?USERNAME=&PASSWORD=&requirePasswordChange=Y. In this testcase (
lines #437 to #448 from the LoginWorker.java file), the
login function returns the value
requirePasswordChange due to username and password being empty, and requirePasswordChange set to ‘Y’ as seen in the code snippet in Figure 1.
List<String> unpwErrMsgList = new LinkedList<String>();if (UtilValidate.isEmpty(username)) {unpwErrMsgList.add(UtilProperties.getMessage(resourceWebapp, “loginevents.username_was_empty_reenter”, UtilHttp.getLocale(request)));}if (UtilValidate.isEmpty(password) && UtilValidate.isEmpty(token)) {unpwErrMsgList.add(UtilProperties.getMessage(resourceWebapp, “loginevents.password_was_empty_reenter”, UtilHttp.getLocale(request)));}boolean requirePasswordChange = “Y”.equals(request.getParameter(“requirePasswordChange”));if (!unpwErrMsgList.isEmpty()) {request.setAttribute(“_ERROR_MESSAGE_LIST_”, unpwErrMsgList);return requirePasswordChange ? “requirePasswordChange” : “error”;//return value depends on the requirePasswordChange parameter}Figure 1: Login function when empty username and password is provided
Subsequently, the given return value from the function
login is passed to the
checkLogin function. Unexpectedly, the flow doesn’t enter in the
conditional block shown in Figure 2 due to the boolean checks (username == null) and (password == null) returning
false even though both the parameters are empty or blank. Additionally, the "error".equals(login(request, response)) also holds
false due to the return value given by login function was
requirePasswordChange.if (userLogin == null) {
// check parameters
username = request.getParameter(“USERNAME”);
password = request.getParameter(“PASSWORD”);
token = request.getParameter(“TOKEN”);
// check session attributes
if (username == null) username = (String) session.getAttribute(“USERNAME”);
if (password == null) password = (String) session.getAttribute(“PASSWORD”);
if (token == null) token = (String) session.getAttribute(“TOKEN”);// in this condition log them in if not already; if not logged in or can’t log in, save parameters and return error
if (username == null
|| (password == null && token == null) // This condition is getting checked.
|| “error”.equals(login(request, response))) {
Figure 2: Code responsible to verify the empty username/password
As a result, the
checkLogin function ends up returning
success, allowing the authentication to be bypassed.
Testcase 2
In this testcase, we attempted to authenticate with a known invalid USERNAME and PASSWORD parameter with the parameter requirePasswordChange set equal to ‘Y’ This testcase is derived from the original public poc for CVE-2023-49070 and used to further our understanding of how the authentication process works. For instance, the request would look like, https[:]//www.example.com:8443/webtools/control/xmlrpc/?USERNAME=x&PASSWORD=y&requirePasswordChange=Y. In this scenario, lines
#601 to #605 from the LoginWorker.java file in the
login function return the value
requirePasswordChange due to the parameter requirePasswordChange=Y as seen in the code snippet in Figure 3.
} else {
Map<String, String> messageMap = UtilMisc.toMap(“errorMessage”, (String) result.get(ModelService.ERROR_MESSAGE));
String errMsg = UtilProperties.getMessage(resourceWebapp, “loginevents.following_error_occurred_during_login”, messageMap, UtilHttp.getLocale(request));
request.setAttribute(“_ERROR_MESSAGE_”, errMsg);
return requirePasswordChange ? “requirePasswordChange” : “error”;
}
Figure 3: Code responsible for return value when non-empty username and password
Subsequently, the given return value from the function
login is passed to the
checkLogin function. Here, the flow didn’t enter in the
conditional block in Figure 2 due to username and password not being null. Additionally, the "error".equals(login(request, response)) also held
false due to the return value given by login function was
requirePasswordChange, similar to testcase 1
. Hence, the checkLogin function returns
success, allowing the authentication to be bypassed.
Conclusion
Considering the above result, it can be concluded that the
requirePasswordChange=Y, the magic string, is causing the authentication to be bypassed regardless of the username and password field or other parameters. As a result, removing the XML RPC code was not an effective patch and the bypass remained.
Patch Review
The vulnerability was fixed swiftly (Kudos!) by the Apache OFbiz with commit
d8b097f and
ee02a33. For due diligence, we confirmed the patch was effective by running the same two testcases.
Verification of Testcase 1
In this scenario, the lines
#436 to #446 in the function
login still returns
requirePasswordChange, but now there is an added utilization of the function
UtilValidate.isEmpty. This comes into play on lines
#341 to #343 in the function
checkLogin as seen in the code snippet in Figure 4.
if (UtilValidate.isEmpty(username)
|| (UtilValidate.isEmpty(password) && UtilValidate.isEmpty(token))
|| “error”.equals(login(request, response))) {
Figure 4: Use of UtilValidate.isEmpty function to verify empty values
Here, boolean checks
UtilValidate.isEmpty(username) and
UtilValidate.isEmpty(password) return
true, unlike (username == null) and (password == null), before resulting in the code returning the value
error within the
checkLogin function. This prevents the authentication bypass from occurring and confirms testcase 1 has been patched.
Verification of Testcase 2
In this scenario, the lines
#609 to #614 in the function
login return in contrast to requirePasswordChange before the patch as seen in Figure 5.
} else {Map<String, String> messageMap = UtilMisc.toMap(“errorMessage”, (String) result.get(ModelService.ERROR_MESSAGE));
String errMsg = UtilProperties.getMessage(RESOURCE, “loginevents.following_error_occurred_during_login”,
messageMap, UtilHttp.getLocale(request));
request.setAttribute(“_ERROR_MESSAGE_”, errMsg);
return “error”;
}
Figure 5: Code changes to return error in case of error during login
This leads to return
true by the boolean check "error".equals(login(request, response)) in the
checkLogin function conditional block seen in Figure 4. This ends up returning the value
error by the
checkLogin function preventing the authentication bypass.
Acknowledgement
We appreciate the prompt response and remediation by the Apache OFBiz team. They demonstrated extreme care for the security of their customers and were a pleasure to work with.