Analysing CVE-2023-51467 - Apache OFBiz Authentication bypass to Remote Code Execution
Feb 26, 2024 •
java
This article aims to explore the details of CVE-2023-51467 and explain the process of constructing an exploit leading to Remote Code Execution.
Introduction
Apache OFBiz is an open-source Enterprise Resource Planning (ERP) solution featuring a suite of applications designed for streamlining and automating various business processes. Notably, a recent discovery has unveiled a critical authentication bypass vulnerability within Apache OFBiz, ultimately exposing the system to Remote Code Execution (CVE-2023-51467). This article aims to explore the details of this vulnerability and explain the process of constructing an exploit leading to Remote Code Execution.
Installing and Configuring OFBiz
Download the 18.12.05 release from the official Apache OFBiz Github repository and proceed with the local installation. For the purpose of this article, I will be using the Intellij IDEA Community Edition to navigate through the codebase.
Exploring through the directories we can see the docs
directory which usually contains interesting information including details on project structure, architecture/design and the developer manual. This is instrumental in gaining a good understanding of the core framework. Notably, within the ‘docs’ directory, the developer-manual.adoc
file contains great insights into the project’s structure
The base directory structure of Apache OFBiz is organized as follows:
A basic unit in Apache OFBiz is called component
. A component is at a minimum a folder with a file inside of it called ofbiz-component.xml
. As per the documentation, a typical component has the following directory structure.
An interesting thing to note here is the groovyScripts
directory containing a collection of scripts written in Groovy. If we can edit or trigger a Groovy file with the contents we control, post authentication bypass, we will be able to achieve remote code execution.
Understanding the patch
OFBiz has fixed the vulnerability with a very simple patch which is as simple as replacing direct null checks with UtilValidate.isEmpty()
function in the file LoginWorker.java
inside webapp/control directory. So the main file to look for the vulnerability is org.apache.ofbiz.webapp.control.LoginWorker#checkLogin
Exploring checkLogin()
Let’s go through the checkLogin()
code line by line to understand how authentication is handled:
File: ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
-
The checkLogin()
method starts by calling another method checkLogout()
to verify if the user has logged out. The result is stored in a GenericValue object named userLogin.
-
The code then retrieves the HttpSession object which is used to manage session-specific information for the user.
-
Three variables namely username
, password
, and token
are initialized to null
. These variables will be used to store user credentials.
-
If the userLogin is null
, it implies that the user has not logged in. The code proceeds to check request parameters (USERNAME, PASSWORD, and TOKEN) and session attributes for the user’s credentials.
Can we spot a bug in the above code ?
What happens if we send a GET request with the parameters namely username
, password
and token
but without a value? All the parameters gets defined but empty, which means it’s not null
but empty
!
Ex: http://localhost:8443/something?USERNAME&PASSWORD
File: ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
- If any of the required credentials (username, password, or token) is equal to
null
or if the login method returns an error
status, the code performs the following actions:
- Removes an attribute (_LOGIN_PASSED_) and sets the _PREVIOUS_REQUEST_ attribute in the session, storing the path of the request.
- Stores URL parameters and form parameters in separate maps (_PREVIOUS_PARAM_MAP_URL_ and _PREVIOUS_PARAM_MAP_FORM_) in the session.
- Returns the string
error
.
Exploring login() & bypassing authentication
One of the things we can conclude from checkLogin()
is, if username, password and token is not null
, then the login()
method returns anything other than error
as the string, and will successfully authenticate with the application. So let’s explore login()
and see if there is a way in which we can force it to return any other string other than error
.
File: ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
-
The method starts with retrieving the delegator, username, password, token, and a flag indicating whether the password is being reset (forgotPwdFlag
) from request parameters.
-
If the forgotPwdFlag
is set to true
, it attempts to decrypt the password using an EntityCrypto
instance. If any of the parameters (username, password, or token) is missing from the request, it attempts to retrieve them from the session attributes.
-
The code allows the username, password, and token to be overridden by values set in request attributes.
-
It checks for missing username
or password
or token
and populates an error message list named unpwErrMsgList
. If errors are present, it sets the error messages in the request attribute and returns either requirePasswordChange
or error
, depending on the value of requirePasswordChange
in the request parameters.
This is precisely what we need, if we explicitly set the value of requirePasswordChange
to Y
and set username
as well as password
to be empty, the login() function returns requirePasswordChange
rather than returning error
which leads to the success path of the code !
Triggering the Vulnerability:
In order to trigger the vulnerability, we first need to understand the architecture of the application and see how we can trigger the groovyScript after bypassing authentication, essentially getting RCE.
Architecture
From the docs directory, inside the developer manual, we can see an architecture diagram explaining how a request gets routed through OFBiz.
Reading further through the documentation, we can see something interesting:
The Java Servlet Container (tomcat) re-routes incoming requests through web.xml to a special OFBiz servlet called the control servlet. The control servlet for each OFBiz component is defined in controller.xml under the webapp folder. The main configuration for routing happens in controller.xml. The purpose of this file is to map request endpoints to responses with flags for security related attributes.
So essentially OFBiz uses Apache tomcat as it’s core webserver which re-reoutes incoming requests through the web.xml
to the control servlet. Each component’s control server is defined in controller.xml
, a central configuration file that defines request handlers, which are responsible for processing incoming requests. So, in-order to fully understand how request mapping works, we need to look at 3 xml files, namely ofbiz-component.xml
, web.xml
and controller.xml
. Let’s take an example /framework/webtools
to understand this:
ofbiz-component.xml
Let’s explore the ofbiz-component.xml
file:
File: ofbiz-framework-release18.12.05/framework/webtools/ofbiz-component.xml
So we can conclude that the mount-point to access the framework’s webtools directory is to hit /webtools
.
web.xml
Let’s explore the web.xml
file, specifically the control servlet mapping:
File: ofbiz-framework-release18.12.05/framework/webtools/webapp/webtools/WEB-INF/web.xml
servlet-name
specifies a name for the servlet, in this case, ControlServlet
servlet-class
specifies the fully qualified class name of the control servlet (org.apache.ofbiz.webapp.control.ControlServlet
).
url-pattern
defines the URL pattern to which the servlet is mapped. Here, it’s /control/*, meaning that the control servlet will handle requests with URLs starting with /control/
.
So this means the request mapping currently looks like /webtools/control
.
controller.xml
Let’s explore the controller.xml
file:
File: ofbiz-framework-release18.12.05/framework/webtools/webapp/webtools/WEB-INF/controller.xml
From the above file,
- The
request-map
elements define mappings for specific URIs namely ping
in this case.
- The
security
element specifies security-related attributes. auth=true
means authentication is mandatory for accessing the /ping
endpoint.
- The
event
element defines the event to be triggered when the specified URI is accessed.
- The
response
element specifies the response to be rendered.
So it’s clear that in order to hit the /ping
endpoint, we would need to be authenticated and hit the following API endpoint /webtools/control/ping
. Firing up the browser and visiting https://localhost:8443/webtools/control/ping will redirect us to the login page.
Let’s go deeper to understand how controller.xml
is being parsed. Grepping through the source code for controller.xml, we can see some interesting result:
The result of the above command will give us a list of all java files who has controller.xml as a string inside it.
From the result, we can see some interesting files named ConfigXMLReader.java
and RequestHandler.java
which does parsing of controller.xml. Let’s look into these files deeper and see how it’s parsing the file and handle incoming requests.
File: ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java
So configXMLReader.java parses the /WEB-INF/controller.xml
and uses the RequestMap class which represents mapping for handling specific HTTP requests. As you can see, if auth=true
is set, then inside the RequestMap object, this.securityAuth
is set to true. Now let’s look at the RequestHandler.java:
File: ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
doRequest()
is the major function which handles the incoming HTTP Request.
-
The code starts with incoming request’s ServerName (host header) is in the hostHeadersAllowed list. If not, it logs an error and throws a RequestHandlerException.
-
It then parses the controller.xml
file to obtain the configuration (ControllerConfig
) and sets up variables, such as the application name (cname), default request URI, and paths. A great way to debug how all these variables play a role is to setup a breakpoint at the doRequest()
function and execute line by line printing variables and values.
- Further down, it checks if the resolved request maps (
rmaps
) collection is empty and then proceeds to handle the HTTP method.
We can continue down this path until we find securityAuth
which triggers the login flow:
File: ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
-
The code checks if requestMap.securityAuth
is true, if so, it invokes a security handler named checkLogin()
.
-
The return value from the checkLogin()
event is stored in checkLoginReturnString
and if the return value is not success
(case-insensitive), it indicates authentication failure.
This means if we are able to return success
for checkLogin(), our request will continue ! We have already seen how to bypass the authentication in the first part of this article :)
Let’s try and access the same path but this time using the authentication bypass we did above: https://localhost:8443/webtools/control/ping?USERNAME&PASSWORD=&requirePasswordChange=Y
This time, rather than going to login page, the application will return PONG
confirming that we have bypassed the authentication successfully !
Remote Code Execution
At the beginning of this article, we noticed groovyScripts
directory, let’s explore the directory and see if it can help us getting RCE.
File: ofbiz-framework-release18.12.05/framework/webtools/groovyScripts/entity/ProgramExport.groovy
The program starts with checking if a parameter named groovyProgram
exists, if not a default value is provided.
File: ofbiz-framework-release18.12.05/framework/webtools/groovyScripts/entity/ProgramExport.groovy
A groovyShell
is initialised and eventually the parameter groovyProgram
is passed onto groovy shell, effectively getting our code executed. An interesting thing to note is the usage of org.apache.ofbiz.security.SecuredUpload.isValidText(groovyProgram,["import"])
, which is essentially taking our parameter and along with a second argument which is an array containing the string “import”.
File: ofbiz-framework-release18.12.05/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
isValidText() internally called DENIEDWEBSHELLTOKENS.stream()
which is nothing but deniedWebShellTokens()
function which seems to be returning based on some pre-defined property value.
File: ofbiz-framework-release18.12.05/framework/security/config/security.properties
This is a simple blacklist based filtering, bypassing which will give us direct code execution.
POC
References:
-
packtpub
-
y4tacker.github.io
Anirudh Anand
Product Security ♥ | CTF - @teambi0s | Security Trainer - @7asecurity | certs - eWDP, OSCP, OSWE