October 29, 2024

Paranoids’ Vulnerability Research: NetIQ iManager Security Alerts

Note: Verizon Media is now known as Yahoo.

Last Summer, the Paranoid’s Vulnerability Research Team (VRT) identified a series of vulnerabilities in OpenText NetIQ iManager, an enterprise directory management tool. Some of the vulnerabilities can be chained together by an attacker to achieve pre-authentication remote code execution. In other cases, an attacker with any valid credentials can escalate their privileges within the platform and ultimately achieve post-auth code execution. 

  • CVE-2024-4429 - Multiple CSRF Validation Bypasses
  • CVE-2024-3970 - Create eGuide Blind SSRF
  • CVE-2024-3969 - Unsafe Stylesheet Parsing Leads to Remote Code Execution
  • CVE-2024-3968 - Plugin Studio Installer RCE
  • CVE-2024-3967 - Email Configuration Unsafe Deserialization May Lead to Remote Code Execution
  • CVE-2024-3488 - Autoparse Arbitrary File Upload
  • CVE-2024-3487 - fw_authState Authentication Bypass
  • CVE-2024-3486 - ModulesToInstall XXE Leads to SSRF or File Disclosure
  • CVE-2024-3485 - Multiple Data Handler Directory Traversals Leads to File Disclosure
  • CVE-2024-3484 - OctetStringUpload Path Traversal Leads to Privilege Escalation or File Disclosure
  • CVE-2024-3483 - checkForLocaleDirectory Command Injection Leads to Remote Code Execution

Fixes for these vulnerabilities were released with version 3.2.6.0300 as of April 2024. More information can be found in the product release notes. If you have not yet upgraded your iManager installations to the latest version by now, you should do so now before moving on to read the rest of this post. 

On the Docket

While we’d love to cover all the aforementioned vulnerabilities, some of which are quite intriguing in their own right, we’re going to keep things focused on a handful of vulnerabilities in this post: CVE-2024-4429, CVE-2024-3488, CVE-2024-3487, and CVE-2024-3483. Individually, each of these bugs are quite tame. However, when chained together, they can be leveraged to achieve full compromise of the iManager server. 

For each, we’ll briefly examine the root cause, see a proof of concept, and discuss any constraints for exploitation. Once we have all the pieces together, we’ll see a full exploit in action. Finally, we’ll wrap up by taking our foothold one step deeper. We’ll reflect (ahem) on how Java Agents can be used in post-exploitation workflows to achieve fine grained instrumentation and control over the compromised application. 

What is iManager?

iManager is an enterprise directory management tool maintained by OpenText. It provides a graphical web console for ergonomic creation, modification, and deletion of directory objects. It can manage directory implementations such as LDAP or their very own eDirectory. It allows for customizable role based access control and can be extended via a plugin system. 

Why does iManager end up being such a good target for attackers? iManager, like many other enterprise administrative consoles, sits in a highly privileged position, administering  downstream directory services. These directory services maintain user account information, such as usernames, passwords, attributes, and group memberships. An attacker with this level of control over user accounts can fool downstream applications that rely on it as a source of truth. 

For example, consider a downstream application that relies on lightweight directory access protocol (LDAP) to control who can login, and which features they can access. This may be implemented via user attributes and assignment to groups. An attacker who compromises iManager can set the user’s password in the directory, while also assigning them to the relevant access groups. Then, the attacker can proceed to simply login to the target application with newly minted credentials. Alternatively, they could create a new account for themselves. The impact of attacks that can be pulled off from this vantage point is bountiful. 

CVE-2024-3487 - Authentication Bypass

When peering into the web.xml to map out entry points into the application, we find that the ingress paths are fairly condensed. Most of the routes map to a single servlet known simply as “portal”, which corresponds to the com.novell.nps.PortalServlet class.

In the PortalServlet class, we see the logic for routing requests to their handler destinations clearly laid out. However, what we don’t see is validation of authentication or authorization yet. 

It’s possible that these checks are implemented as middleware, but we notice up top that PortalServlet extends the AuthenticatorServlet class. Diving into the AuthenticatorServlet class quickly reveals where authentication checks are happening. 

In the service method of AuthenticatorServlet, the HttpServletRequest gets wrapped in a utility type FwRequest shortly before the login state is checked. Here we observe that isLoggedIn is called on the request object, which accordingly sets the bLoggedIn property. 

The isLoggedIn function is fairly straightforward: it iterates through the “required” authenticators and calls the isAuthenticated implementation for each. It turns out there are only two authenticators available, NullAuthenticator and DirAuthenticator. On this path, only DirAuthenticator is loaded. If the user is not yet logged in, execution continues through to login, which similarly iterates through authenticators, this time calling handleLogin on each, which just drops through to the authenticator’s login implementation. 

Here’s where the interesting bit is. 

A query parameter named fw_authState is compared against a few cases. If the state is “dontChangePwd”, the session is marked as logged in! This appears to be a very powerful authentication bypass, as it happens early in the flow before requests are routed to their ultimate handlers. 

The proof of concept is simple enough:

When testing our auth bypass, we notice an important caveat: it does not work on all routes. In fact, it apparently doesn’t work on most routes. What’s blocking us here? Metrics. When dispatching requests to their handlers, the GadgetManager will attempt to locate the responsible class, instantiate it, and pass back the registered instance. This handler instance, wrapped in a special container class, is called a “gadget” in iManager terminology. User registered plugins can create their own gadgets, introducing new core functionality to the application. More in-depth information is available via the iManager Developer Kit Documentation. 

iManager Application Architecturefrom the developer kit documentation

In most cases, this filters down into two methods for handing off execution to the gadget, processInstanceRequest and processRequest, each with different semantics. Both of them make a call to postGadgetHitEvent, which is where the problem lies. postGadgetHitEvent will attempt to extract the user id associated with the session while emitting an event for the request.

The problem is, there isn’t  a properly set up login session with all the required attributes. Our request is simply marked as “logged in”. This means that any paths attempting to access session attributes will result in a NullPointerException! This unfortunately sabotages the majority of routes for us. However, there is a silver lining, a narrow escape for us to clutch onto: routes destined for /mtaskservice are dispatched through processMTaskRequest, which makes no such dangerous property accesses or function calls. This means that we can only use this auth bypass to trigger code in gadgets which extend the MTask type. While this is a significant restriction, we’ll later see how to make this work for us. 

Injection Interlude

While exploring available MTask gadgets via the previous bug, we happened upon MiscTask located in the com.novell.emframe.debug package. This gadget, as its name suggests, contains miscellaneous debug tests. 

However, like the previous bug, we again see powerful functionality hidden within a state check. In this case, an attacker can supply a key/value pair to be applied to the application’s running configuration. This is gated behind FwUtils.canConfigureiManager(), however. What constitutes this check? See below (some code blocks collapsed for brevity). 

Normally, a configuration file is consulted. This file, configiman.properties, typically located at /var/opt/novell/iManager/nps/WEB-INF/configiman.properties, contains a list of users and groups authorized to configure iManager. This file can be modified to grant and revoke this permission for a given user and group. If the configiman.properties file does not exist, true will be returned, and thus this check would pass for all users. It’s important to note that this file does not exist by default on a new installation. On a fresh install, we can leverage our authentication bypass previously discovered to modify configuration values. Now the question is, what value do we modify?

CVE-2024-3483 - Command Injection

To evaluate the impact of controllable configuration values on the application, we first identified which class was responsible for loading and exposing configuration values. This appeared to be com.novell.emframe.dev.config.SystemConfig, which exposed the getSetting method. getSetting was also wrapped via getSystemConfig, a helper method of com.novell.admin.ns.nds.jclient.NDSNamespaceImpl. Searching for references to these, we stumble on an interesting case inside of checkForLocaleDirectory(). This function appears to be intended to identify the existence of a locally running instance of NetIQ eDirectory by probing well-known configuration paths.

Within this function, the value of the configuration option EDir_Instance_File_Path is loaded. The file at the configured path is opened, and the contents read. 

The contents read are used as a second filename, which is opened and read. The contents of the second file are then appended to a command line string without sanitization and subsequently executed. This appears to be a clear cut command injection vulnerability. However, note that the second file open operation must succeed, using the contents of that first file as the second filename. This means that our command injection string must also be a valid file path. This is not difficult to do in practice, but must be taken into account. 

From here, we work backward to identify a place to trigger the functionality from. Fortunately for us, the code to trigger it is easily reachable: checkForLocaleDirectory is called from NDSNamespaceImpl.authenticate(). This function is called early on during standard login flows! 

Once we have our command injection string configured, we  make a login attempt (valid credentials not needed) to achieve code execution. This path is not bound by the limits of our authentication bypass if we don't apply it for this request. 

An obvious problem presents itself, though: the injection string is loaded from a file on disk. We control the path to the file via the configuration value, but we don’t have a way to plant a file with a controlled name and controlled contents. What now?

CVE-2024-3488 - Arbitrary File Upload

A file upload primitive was identified early in the auditing process, but was recorded and sidelined for later due to not being able to convert it to RCE directly. With the command injection opportunity clear in our sights, this primitive becomes much more powerful. 

Remember when examining CVE-2024-3487 (authentication bypass) that we noted the incoming request object was wrapped in the FwRequest type before routing? As it turns out, the constructor for this wrapper can perform a very important task for us. 

If the incoming request has the Content-Type header set to multipart/form-data, the constructor will additionally check for the presence of a query parameter named Autoparse. If that parameter contains any value, we can reach the call to parseMultipartRequest.

parseMultipartRequest does essentially what it sounds like. It will parse out the submitted form data as a multipart upload. A destination directory is chosen based on the saveDirectory parameter, but since the caller passes in null, it just uses the default here, which is /var/opt/novell/tempFiles.

Once an output directory is chosen, the values extracted from the request are passed into the constructor for com.oreilly.servlet.MultipartRequest, which handles the actual parsing and file saving logic. The most important detail in the implementation of MultipartRequest is that while an attacker can’t necessarily perform a path traversal out of the tempFile upload directory with a malicious filename field, the submitted file is still stored using the filename the attacker supplied. This means that the prerequisite of multiple on-disk files with controlled names and contents for our command injection bug is now satisfied! While MultipartRequest filters the filename and disallows a few characters, our command injection string is still plenty viable. An injection string (doubling as a filename) can just smuggle the “bad” characters inside of base64 encoding like so:

/var/opt/novell/tempFiles/a ; echo aWQgPiAvdG1wL3B3bmVkCg== | base64 -d | sh #

Prepping 

Combining the first three bugs, we can now achieve pre-authentication RCE against iManager. Let’s break down the game plan so that we have a clear idea of what steps our exploit must implement:

  1. Using the arbitrary file upload primitive, we must upload two filessome text
    1. File A can have any name, but its contents must contain a full path to a second file. This second filepath will also be our command injection string. 
    2. File B can have any contents, but its name must be the file name portion of the command injection string that was stored in file A. 
  2. Once the files are uploaded, we must quickly leverage our authentication bypass to trigger the debug.Misc task, setting the configuration value of EDir_Instance_File_Path to the path to file A. 
  3. Finally, we must trigger the command injection itself by attempting a login to the application. This does not require any valid credentials. This attempt will also trigger a cleanup of the files read during checkForLocaleDirectory! This means that we must make our login attempt before another user does. In practice, this is fairly easy to do, and the exploit can be re-attempted as many times as needed.
  4. Establish a more permanent foothold in the target. Leveraging the command injection, we drop a JSP webshell on the target, allowing us to retain access after the vulnerability is patched.

With our full chain mapped out, we can achieve command execution on iManager. However, this requires us to be on a network segment where we can issue requests to the iManager server. Best practice suggests that this administration console not be exposed to the internet, meaning  that in an ideal scenario (for the defender), an attacker would have to already have an internal foothold somewhere, by compromising a workstation, corp VPN credentials, or other appliance or server.

Let’s take a look at one final bug which should grant us the network pivoting capability we need to make this exploit sing. 

CVE-2024-4429 - CSRF Bypass

As an attacker, we need to identify a way to perform a sort of network pivot in order to send our exploit requests to the iManager server, which we assume sits within the private network perimeters of a target. One way that we can achieve this is by coercing a user who already sits within that perimeter to send the requests for us. None of the requests we issue in our exploit should require us reading the response body. Additionally, despite the primary session cookie being marked SameSite=strict, we do not need to submit a session cookie with our requests, as we wield an authentication bypass! These factors are in our favor, making a webpage based exploit attractive. We’d simply serve the page up to a victim who is signed into the corporate VPN, and our exploit would fire against the internal iManager instance.

However, there is still a kink in the plan. iManager still requires CSRF tokens to be submitted with requests, which get validated. If no valid anti-CSRF token is supplied, the request is rejected. If we are performing this attack cross-domain through a webpage served to the victim, we can’t issue a GET request to extract valid tokens from a page’s form fields either, as we won’t be able to read the response body. 

Luckily, there are some flaws in the anti-CSRF token validation. 

Anti-CSRF functionality is implemented as middleware through a servlet filter, in the com.novell.emframe.fw.filter.AntiCsrfServletFilter class. In the doFilter method, we quickly see a number of escape hatches available to us. The filter provides exemptions based on the suffix or path of the URL for things like images, css, html pages, javascript files, and the help route. Even better, one of the escape hatches is just a hidden parameter that explicitly disables anti-CSRF token validation.

A request meeting any of the following conditions bypasses anti-CSRF token validation:

  • Contains the parameter iManAntiCSRF.NoValidation=true
  • Request path ends with .jpeg, .png, .gif, .css, or .tld
  • Request path ends with /help/
  • Request path ends with tiny_mce.js

Some proof of concepts are shown below:

Exploiting paths with magic extensions (leveraging path parameters delimited by semicolon):

Exploiting paths using the magic iManAntiCSRF.NoValidation flag:

With this final piece, our exploit chain can now be fully run through the browser of a victim connected to their corporate network. The web page it is served on can be linked to the victim in a number of typical phishing pretexts, or via watering hole attacks.

0(day) to 💯

With all the hard work out of the way, we finally have the satisfaction of testing out a full exploit! Below is a javascript implementation which can be served up via a webpage. 

pwn.js

trigger.html

Secret Agents and Spy Gadgets

Once command execution is achieved on the target, we likely want to escalate our hold on the application in order to accomplish actual operational objectives. As we discussed earlier, users manage objects in downstream directories through iManager. We’d like to gain this same capability in a straightforward manner. What if we could lift passwords straight out of the application itself as administrative users log in?

One approach we can take is to instrument the application, which would allow us to inspect the values of certain variables at runtime. Java has support for such runtime instrumentation already, in the form of Java Agents. Agents can be injected into the application to run at either application startup, or after the application has launched. 

Introducing method hooks, or otherwise changing the behavior of class implementations involves generating new binary bytecode, which is replaced on the fly (a class Transformation)  in order to redefine the class within the application at runtime. While Java does not directly solve the bytecode generation problem for us, some effective libraries have already been developed for this purpose, each with varying levels of abstraction. When working lower level, we can leverage libraries like ASM. If we want more of the ugly parts abstracted away for us, we can lean on something like ByteBuddy

Let’s take a look at code for a simple Java Agent which modifies a class used as part of the authentication flow. We’ll install a method hook that allows us to inspect the incoming parameters, which we can record and exfiltrate.

Main.java

LoginAdvice.java

Agent.java

MANIFEST.MF

Within Main.java, we see our entrypoint into the target. The call to ByteBuddyAgent.attach() is responsible for attaching to the running JVM specified by a PID and loading the Java Agent specified by a path into it. In this case, we load the currently executing Jar. We simply do this to keep our payload monolithic and easy to move around. 

Once the agent is loaded in the target JVM, the agent class’ agentmain method is invoked by the loader. The agentmain method is the entrypoint for agents which are loaded after the application has started. In cases where the agent would be loaded with application startup, then we’d populate a premain method.  But how does the loader know which class contains the agent implementation, including the agentmain method that we want? It is specified in the manifest, using the field Agent-Class. Note how we also specify the field Main-Class. This allows us to invoke the jar from the command line, and have the jar bootstrap itself into the target JVM without needing separate dependencies for the loader and agent itself. 

In the agentmain method, we call into doInstrumentation, which uses the Java Instrumentation object to construct an agent to install. We specify a strategy used for class redefinition, in this case RETRANSFORMATION, which allows us to modify classes after they have been loaded. We supply a matcher, which helps the class visitor determine when it has found the correct class to modify. 

A transformer is constructed, with a second matcher and an Advice specifier.  In their own words

Advice wrappers copy the code of blueprint methods to be executed before and/or after a matched method. To achieve this, a static method of a class is annotated by Advice.OnMethodEnter and/or Advice.OnMethodExit and provided to an instance of this class.

In Agent.java we’re specifying that we want to supply an Advice hook for methods matching the name createAuthenticationBroker, with the advice implementation supplied in the LoginAdvice class. The AuthenticationBroker.createAuthenticationBroker method is called along standard login flow paths, and accepts parameters that contain crucial information for us: the username, password, and target directory. A nice freebie is that this happens after credentials have been validated, so we shouldn’t accidentally record any invalid entries. 

Finally, in the onEnter method of our LoginAdvice class, we annotate the parameters by position using Advice.Argument. We can then access these parameters by name within our hook. In this case, for the sake of example, we’re recording the stolen credential values out to a text file. In a real exploit, one might consider exfiltrating these off-target to login with later. 

To install our malicious Java Agent, we can launch the Jar directly and allow it to invoke the main class, which kicks things off. It will attach itself to the JVM and load the agent class into iManager, installing our hook. Once an administrator logs in, we check our exfil log and see the results: the administrative credentials were lifted directly out of the normal login flow. 

With these, we can log directly into the web console and perform administrative actions on the directory as a normal user would!