社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Django

DjangoORM注入分享

船山信安 • 1 年前 • 449 次点击  

DjangoORM注入

简介

这篇文章中,分享一些关于django orm相关的技术积累和如果orm注入相关的安全问题讨论。

攻击效果同数据库注入

从Django-Orm开始

开发角度

Django ORM(Object-Relational Mapping)是Django框架中用于处理数据库操作的一种机制。它允许开发者使用Python代码来描述数据库模式和执行数据库查询,进行数据库操作,如创建、读取、更新和删除数据等操作,而不需要直接编写SQL语句。通过ORM,开发者可以更直观和方便地进行数据库操作,同时保持代码的可读性和可维护性。

  • 如果没有ORM,作为后端开发的需要写如下代码

# 假设需求背景 Python 开发人员想要编写一个博客网站,供人们发布文章,并希望向其应用程序添加搜索功能
def search_articles(search_term: str) -> list[dict]:
results = []
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT title, body FROM articles WHERE title LIKE %s", (f"%{search_term}%",))
rows = cursor.fetchall()

for row in rows:
results.append({
"title": row[0],
"body": row[1]
})
return results
  • 换成ORM模式开发如下

# models/article.py
from django.db import models

class Article(models.Model):
"""
The data model for Articles
"""
title = models.CharField(max_length=255)
body = models.TextField()

class Meta:
ordering = ["title"]

# serializers/article.py
class ArticleSerializer(serializers.ModelSerializer):
"""
How objects of the Article model are serialized into other data types (e.g. JSON)
"""
class Meta:
model = Article
fields = ('title', 'body')

# views/article.py
class ArticleView(APIView):
"""
Some basic API view that users send requests to for searching for articles
"""
def post(self, request: Request, format=None):
# Returns the search URL parameter if present otherwise it is set to None
search_term = request.data.get("search", None)
if search_term is not None:
articles = Article.objects.filter(title__contains=search_term)
else:
articles Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)

安全角度

Django ORM通常可以防止SQL注入问题,因为它会自动对查询参数进行适当的转义和处理。不过,如果在使用Django ORM时不小心使用了原生的SQL查询或手动构建了SQL语句,也是有SQL注入的问题,不过这个不是这篇文章讨论的主要方向。仅作概述:

  • 使用ORM的过滤器方法: 始终使用Django ORM的过滤器方法,而不是手动构建SQL查询。例如:

# 安全的查询方式
users = User.objects.filter(username=username)
  • 避免使用raw()方法: raw()方法允许你编写原生SQL查询,但如果不正确处理输入,可能会导致SQL注入。

# 不安全的方式
users = User.objects.raw("SELECT * FROM auth_user WHERE username = '%s'" % username)

# 安全的方式
users = User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [username])
  • 使用Django的QuerySet API: 尽量避免使用低级别的数据库API,Django的QuerySet API提供了足够的功能来执行大多数查询,而不需要直接编写SQL

# 安全的方式
users = User.objects.filter(email__icontains='example.com')
  • 使用参数化查询: 如果必须使用自定义的SQL查询,确保使用参数化查询。

from django.db import connection

def get_user_by_username(username):
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM auth_user WHERE username = %s", [username])
row = cursor.fetchone()
return row
  • 变量效验: 在处理用户输入时,始终进行变量效验,以确保输入数据的安全和有效性。

from django.db import connection

def get_user_by_username(username):
with connection.cursor() as cursor:
# 使用参数化查询防止SQL注入
cursor.execute("SELECT * FROM auth_user WHERE username = %s", [username])
user = cursor.fetchone()
return user

Django-Orm注入

首先创建一个django应用,能够获取book信息

  • 此时的view逻辑为:

  • 假设此时后端开发的领导有了如下的要求:

    • 需要一个强大的 API 来允许用户按 book 模型中的任何字段进行过滤

    • 后续会不断新增表中的字段,并且希望 API 无需修改任何代码即可兼容这些更改

    • 任务十分紧急

    • ....

  • 这个时候开发十分容易写出如下代码

盲注获取敏感字段

写在前面,修改部分代码模拟一个靶场环境:

  • 新增flagbook,其中isbn为敏感字段flag,真是环境flag可能为密码、手机号、token等敏感字段

Book.objects.create(title='flagbook', author='flag book', published_date=date(2020, 4, 15), isbn='flag{secret}')
  • 修改后端view代码,不返回isbn字段&调整下代码

  • 通过django filter startwith 进行注入获取flag

具体 django filter语法可参考 https://docs.djangoproject.com/en/5.0/ref/models/querysets/#id4

  • 基础语法,查询flagbook数据

  • 盲注获取flag poc

startswith正确时如下

startswith错误时如下

  • 至此,我们就可以写脚本获取完整的flag

泄露的条件总结

  • 可以控制filter过滤列

  • ORM支持正则、startswith类似操作

  • 表中存在一个隐藏的敏感字段

多表关联的情况

在 Django 中,OneToOneFieldManyToManyField 和 ForeignKey 是用来定义模型之间关系的字段类型。每种字段类型表示不同的数据库关系。

OneToOneField

OneToOneField 表示一对一关系。一个模型实例与另一个模型实例之间有且仅有一个关联。

from django.db import models

class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField()

在这个例子中,每个 UserProfile 实例与一个 User 实例有且只有一个关联。

ManyToManyField

ManyToManyField 表示多对多关系。一个模型实例可以与多个另一个模型实例关联,反之亦然。

class Author(models.Model):
name = models.CharField(max_length=100)

class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)

在这个例子中,一本书可以有多个作者,一个作者也可以写多本书。

ForeignKey

ForeignKey 表示多对一关系。一个模型实例可以与多个另一个模型实例关联,但反过来每个模型实例只能与一个实例关联。

class Publisher(models.Model):
name = models.CharField(max_length=100 )

class Book(models.Model):
title = models.CharField(max_length=100)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

在这个例子中,一本书只能有一个出版社,但一个出版社可以出版多本书。

关系总结

  • OneToOneField: 一对一关系

  • ManyToManyField: 多对多关系

  • ForeignKey: 多对一关系

demo演示

我们修改model代码如下
from django.db import models

class Publisher(models.Model):
name = models.CharField(max_length=100)
address = models.TextField()

def __str__(self):
return self.name


class Category(models.Model):
name = models.CharField(max_length=100)

def __str__(self):
return self.name


class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
published_date = models.DateField()
isbn = models.CharField(max_length= 13, unique=True)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
categories = models.ManyToManyField(Category)

def __str__(self):
return self.title

class BookDetail(models.Model):
book = models.OneToOneField(Book, on_delete=models.CASCADE)
summary = models.TextField()
number_of_pages = models.IntegerField()

def __str__(self):
return self.book.title
  • demo数据如下

import os
import django
import datetime
import random

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'MyProject.settings')
django.setup()

from BookStore.models import Book, Publisher, Category, BookDetail

# 清空旧数据
Publisher.objects.all().delete()
Category.objects.all().delete()
Book.objects.all().delete()
BookDetail.objects.all().delete()

# 创建 Publisher 示例数据
publishers = [
Publisher.objects.create (name=f'Publisher {i}', address=f'{i} Main St') for i in range(1, 6)
]

# 创建 Category 示例数据
categories = [
Category.objects.create(name=f'Category {i}') for i in range(1, 6)
]

# 创建 Book 示例数据
books = [
Book.objects.create(
title=f'Book {i}',
author=f'Author {i}',
published_date=datetime.date(2021, 1, i),
isbn=f'{1234567890123 + i}',
publisher=random.choice(publishers)
) for i in range(1, 6)
]

# 添加 Book 到 Category
for book in books:
book.categories.add(*random.sample(categories, k=2)) # 随机选择两个分类

# 创建 BookDetail 示例数据
for book in books:
BookDetail.objects.create(
book=book,
summary=f'This is the summary for {book.title}.',
number_of_pages=random.randint(100, 500)
)

print("Demo data created successfully.")
  • 依旧是这个接口逻辑不变

  • 一对一的方式,通过bookdetail关联 book 的isbn列数据包

  • 多对一的方式,通过book 关联 publishers 的address地址列数据包

  • 多对多的方式,通过book 关联 Category的name列数据包

写在最后

其余的一点思考:created_by__user__password__regex 类似这种会不会造成数据库redos攻击!因为之前学习过redos,答案很明显:几乎不大可能会。数据库的正则引擎为有限状态向量机。https://xz.aliyun.com/t/14653?time__1311=GqAhYKBIqIxmx05DKARoxgDforneQ4fqx

参考

ThinkPHP架构设计不合理极易导致SQL注入 | PHITHON的公开漏洞 (leavesongs.com)

Pwnhub Web题Classroom题解与分析 | 离别歌 (leavesongs.com)

https://www.elttam.com/blog/plormbing-your-django-orm/


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/173744
 
449 次点击