Este artigo também está disponível para leitura on-line ou download em formato PDF.
Por Ulisses Castro | Conviso Security Labs
Todos que já buscaram saber como funcionam os ataques de SQL Injection sabem que é uma técnica antiga, porém sempre é bom ver novas ideias surgindo que acabam servindo para ampliar as possibilidades. Quero compartilhar aqui algumas técnicas não convencionais de extração de dados que normalmente são utilizadas por “seres humanos”, mas porque dizer assim?
Por mais acostumado a automatizar tudo utilizando as melhores e mais variadas ferramentas para pentest, você não pode esquecer que em algumas das muitas situações encontradas durante o processo, é manualmente e com aquele pensamento “fora da caixa” que vai conseguir um resultado inesperado e diferenciado de qualquer outra abordagem similar no mercado.
Por isso venho aqui compartilhar uma técnica semelhante a utilizada no MS-SQL Server que é bem conhecida para quem tem experiência em ataques neste tipo de banco de dados, entretanto pouco conhecida e utilizada em outros, em específico MySQL, Oracle, PostgreSQL e SyBase que é o Error Based Blind SQL Injection, nada mais do que a extração de dados baseada em erro.
Ferramentas fechadas de renome e também aquelas que tem seu código fonte aberto não abordam tal técnica, perdendo uma grande gama de possibilidades e acabam não suprindo um gap que nós pentesters encontramos. Apesar de não abordar neste artigo especificamente como utilizá-la em conjunto com outras técnicas para bypass em Web Application Firewalls (WAFs), deixo aqui algumas ideias que passam batido em muitos WAFs como HPP em conjunto com SQL Injection e existem até aqueles que deixam passar SQL Injection com erros boleanos pois na assinatura só conseguem se defender quando encontrarm o famoso UNION na URL, fica aqui um gancho para uma próxima pesquisa.
Introdução
Partindo do princípio que o leitor sabe do que se trata um SQL Injection, vamos deixar claro as seguintes técnicas de maneira resumida:
- Union Inband SQL Injection: Técnica onde ao injetar códigos SQL através de uma aplicação, buscamos utilizar a função UNION em conjunto com operadores boleanos para imprimir os dados solicitados na própria página da aplicação.
- Blind SQL Injection: Diversas formas diferentes, basicamente utilizando comparações boleanas em conjunto com funções de string do banco de dados, forçamos um verdadeiro ou falso na aplicação montando corretamente ou não a página.
- Error Based Blind SQL Injection: Este tipo ataque, o qual vou abordar neste artigo, é exatamente o tipo que é pouco abordado em ferramentas, onde muitas vezes não conseguimos trazer os dados utilizando o UNION, mas sim forçar que algum erro seja impresso na página.
Sendo assim existem algumas técnicas para extrair os dados destas mensagens. Importante frisar que as demonstradas utilizando este tipo de ataque podem facilmente ser adaptadas para o Blind SQL Injection e também o conhecido Time Based SQL Injection, que se baseia no tempo em que é retornada determinada consulta sem enxergar absolutamente nada na interface da aplicação.
Error Based Blind SQL Injection, como funciona?
MySQL
Vamos ver como poderíamos forçar um erro boleano no MySQL e extrair dados do mesmo? O pesquisador Dmitry Evteev, apresentou uma técnica muito interessante utilizando a função ExtractValue() do mysql versão 5.1 ou superior para demonstrar como extrair dados. No console do MySQL podemos ver o que ocorre ao forçar um erro utilizando a função apontada, repare que não é utilizado UNION, conforme apresentado na imagem abaixo.
Esta abordagem é excelente, porém só conseguimos extrair os dados em versões 5.1 ou mais recentes, a quantidade de dados extraída é limitada a 31 bytes e ainda alguns casos dependemos de um UNION o que é péssimo frente a defesas como WAFs. Abaixo, um exemplo demonstrando a limitação dos 31 bytes:
Para as limitações citadas existem diversas formas de amenizar estas dificuldades, primeiro é preciso eliminar a dependência de versão. Abaixo um insight feito por Qwazar que demonstra como é possível realizar esta técnica aplicada para versões do MySQL 4, 5.0, 5.1 e superiores. Tal abordagem além de mitigar a limitação da versão, amplia a quantidade de bytes extraídos passando de 31 para 64 bytes e mantém a mesma lógica demonstrada acima:
Porém nos traz mais uma desvantagem, neste caso devido a função rand(), não existe 100% de chance de retorno, ou seja, as vezes o resultado pode vir e as vezes não. Isso é péssimo, principalmente em casos onde escrevemos um script para automatizar mas como sempre existe uma solução, abaixo montei uma versão sem utilizar UNION e mantendo 99% de retorno do resultado. Não sou DBA e nem mesmo ultra expert em SQL, assim acredito que alguém com mais experiência possa ajudar a montar um consulta um pouco menor, mas a syntax ficou assim (seguindo o mesmo exemplo acima):
select ‘1 ‘ and if(row(0,0)> (select + count(*), concat((select concat_ws(0x3a,user,password) from mysql.user limit 1), 0x3a, floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row (0,0)> (select + count(*),concat((select concat_ws(0x3a,user,password) from mysql.user limit 1),0x3a,floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row(0,0)>(select + count(*),concat((select concat_ws(0x3a,user,password) from mysql.user limit 1),0x3a,floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row(0,0)>(select + count(*),concat((select concat_ws(0x3a,user,password) from mysql.user limit 1),0x3a,floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row(0,0)>(select + count(*),concat((select concat_ws(0x3a,user,password) from mysql.user limit 1),0x3a,floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row(0,0)>(select + count(*),concat((select concat_ws(0x3a,user,password) from mysql.user limit 1),0x3a,floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row(0,0)>(select + count(*), concat((select concat_ws(0x3a,user,password) from mysql.user limit 1), 0x3a, floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row(0,0)>(select + count(*),concat((select concat_ws(0x3a,user,password) from mysql.user limit 1),0x3a,floor(rand()*2)) x from mysql.user group by x limit 1),1,if(row(0,0)>(select + count(*),concat((select concat_ws(0x3a,user,password) from mysql.user limit 1), 0x3a,floor(rand()*2)) x from mysql.user group by x limit 1),1,(select 1 and 1=1))))))))));
Veja que o resultado apresentado na imagem abaixo não vem truncado em 31 bytes e conseguimos ver a hash completamente, pois conseguimos trazer 64 bytes. Essa talvez não seja nem de perto a melhor solução e seria preciso estudar um pouco mais algumas formas de melhorar os resultados.
Com a utilização desta técnica, em conjunto com outras funções específicas do MySQL, é possível ampliar ainda mais extração de dados. Utilizando a função compress() com a biblioteca zlib podemos compactar o resultado, conforme apresentado abaixo. Com isso, é possível diminuir o tamanho do resultado, o que nos testes que realizei chegaram a 50% de taxa na compressão. Porém, a desvantagem é que não seria possível utilizar técnica de Verdadeiro/Falso para extrair caractere por caractere, pois o tipo de dado de retorno impede que isso aconteça.
Porém a solução é simples, com a função hex() conseguimos transformar todo o resultado em hexadecimal, e com esta saída não só podemos transportar qualquer tipo de dados como ainda facilita a comparação Verdadeiro/Falso em casos de Blind SQL Injection, e pode preparar a consulta para trazer um “tira” única em hexadecimal de todos os dados solicitados independente do tamanho.
A única desvantagem perceptível é que o processamento do banco de dados será elevado em caso de automatização, mas isso pode ser ajustado através de delays e timers. Na prática para extrair os dados, seria necessário “caminhar” pela tira em hexadecimal, extraindo o resultado de 64 em 64 bytes ou 32 em 32 bytes dependento da técnica, utilizando funções específicas para lidar com strings. Na prática ficaria como na imagem abaixo:
E para potencializar tudo isto seria possível ainda utilizar “sub-querys” e trazer muitos dados com pouquíssimos “hits” no Servidor Web, técnica que demonstrei no OWASP AppSec Brasil do ano passado. Porém nem todas as empresas utilizam MySQL e é preciso estar preparado para encontrar os mais diversos tipos pela frente. Acima demonstrei como é feito com MySQL, porém cada um possui sua limitação e também vantagens, o que nos traz novas possibilidades a cada abordagem.
Aplicação em outras bases
Abaixo algumas idéias menos elaboradas do que esta feita no MySQL, porém mostrando a pontinha do iceberg e um caminho para aplicar a mesma técnica em diferentes cenários.
MSSQL Server / Sybase
Não é nada novo, a maioria das técnicas de SQL Injection demonstradas em diversos forums, how-tos, listas de discussão, demonstram esta técnica que força a conversão no tipo de dado para demonstrar o erro, no caso do MSSQL é possível trazer aproximadamente 1024 bytes e no Sybase a técnica é a mesma.
PostreSQL
Semelhante ao MSSQL/Sybase, porém utilizando a função CAST():
E seguindo a mesma linha de raciocínio aplicada no MySQL:
Oracle
Demonstrando esta técnica utilizando Oracle 9.0 ou superior, é possível extrair até 214 bytes de uma mensagem de erro no Oracle usando a função XMLType() em conjunto com outras para tratamento de string, seguindo lógica semelhante a demonstrada com o MySQL, transforma a consulta em hexadecimal, usa substr() para cortar a “tira” e trazer os resultados em partes. E ainda podemos utilizar um truque para trazer versão em uma única linha, isso porque o Oracle não possui offset ou limit:
SELECT banner FROM(SELECT banner,rownum rnum FROM v$version a)WHERE rnum=1;
Usar XMLType() para mostrar a versão na saída de erro, para isto foi necessário utilizar a função REPLACE() e substituir os espaços por “_”(underline). Repare que a todo momento utilizo a função CHR() visando evitar qualquer problema com caracteres de “‘”(aspas simples).
SELECT XMLTYPE(CHR(60)||CHR(58)||(SELECT REPLACE((SELECT banner FROM(SELECT banner,rownum rnum FROM v$version a)WHERE rnum=1),CHR(32),CHR(95))FROM dual)||CHR(62))FROM dual;
Abaixo uma versão mais apurada para burlar os problemas de tipos de dados, assim como replicar técnica semelhante à aplicada ao MySQL, utilizando conversão para hexa e removendo a função REPLACE() não precisamos dela já que estamos convertendo para hexadecimal.
SELECT UPPER(XMLTYPE(CHR(60)||CHR(58)||(SELECT RAWTOHEX((SELECT banner FROM(SELECT banner,rownum rnum FROM v$version a)WHERE rnum=1))FROM dual)||CHR(62)))FROM dual;
E finalmente a versão para ser utilizada em um Error Based Blind SQL Injection utilizando operador lógico simulando um ataque.
SELECT 1 FROM dual WHERE 1=1 AND(1)=(SELECT UPPER(XMLTYPE(CHR(60)||CHR(58)||(SELECT RAWTOHEX((SELECT banner FROM(SELECT banner,rownum rnum FROM v$version a)WHERE rnum=1))FROM dual)||CHR(62)))FROM dual);
Existem diferentes maneiras de se chegar ao mesmo objetivo, com um conhecimento maior em determinado banco de dados e mais pesquisas, com certeza é possível chegar a um resultado mais apurado, lembrando que em todos os casos sempre pensei em não utilizar o UNION para tentar evitar um possível match de assinatura com WAFs, assim como a utilização de função para representar caracteres ascii na consulta e evitar enviá-los através de URL.
Conclusão
SQL Injection é uma técnica que por mais antiga que seja a cada dia evolui, seja com lançamento de novas versões com novas funções, ou fazendo uma releitura de técnicas já conhecidas e aprimorando sempre é possível demonstrar algo novo.
E como sou um viciado convicto em Python nada melhor do que escrever scripts em python usando threads somado a técnicas novas, é sempre diversão garantida. Abaixo o video demonstrando na prática um script que aplica a técnica acima, atacando um banco de dados MySQL.
Em um futuro próximo a Conviso IT Security estará disponibilizando em formato Open Source algumas ferramentas criadas em laboratório, fique de olho! 😉
Referências