博客 | NGINX

NGINX 教程: 保护 Kubernetes 应用免受 SQL 注入

NGINX-F5-horiz-black-type-RGB 的一部分
Daniele Polencic 缩略图
丹尼尔·波伦契奇
2022 年 3 月 22 日发布

本教程是将2022 年 3 月微服务中的概念付诸实践的四个教程之一: Kubernetes 网络

是否想要有关使用 NGINX 实现更多 Kubernetes 网络用例的详细指导? 下载我们的免费电子书《使用 NGINX 管理 Kubernetes 流量》: 实用指南

您在当地一家受欢迎的商店从事 IT 工作,该商店销售从枕头到自行车等各种商品。 他们即将推出第一家网上商店,但他们请一位安全专家在网站公开之前对其进行渗透测试。 不幸的是,安全专家发现了一个问题! 在线商店容易受到SQL 注入攻击。 安全专家能够利用该网站从您的数据库中获取敏感信息,包括用户名和密码。

您的团队来找您——Kubernetes 工程师——来拯救这一局面。 幸运的是,您知道可以使用 Kubernetes 流量管理工具来缓解 SQL 注入(以及其他漏洞)。 您已经部署了 Ingress 控制器来公开应用程序,并且在单一配置中,您能够确保漏洞不会被利用。 现在,网上商店可以按时上线了。 做得好!

实验室和教程概述

本博客是 2022 年 3 月微服务第 3 单元 – Kubernetes 中的微服务安全模式的附带实验室,演示了如何使用 NGINX 和 NGINX Ingress Controller 来阻止 SQL 注入。

要运行本教程,您需要一台具有以下配置的机器:

  • 2 个或更多 CPU
  • 2 GB 可用内存
  • 20 GB 可用磁盘空间
  • 互联网连接
  • 容器或虚拟机管理器,例如 Docker、Hyperkit、Hyper-V、KVM、Parallels、Podman、VirtualBox 或 VMware Fusion/Workstation
  • minikube安装
  • 安装Helm
  • 允许您启动浏览器窗口的配置。 如果不可能的话,您需要弄清楚如何通过浏览器获取相关服务。

为了充分利用实验室和教程,我们建议您在开始之前:

本教程使用了以下技术:

每个挑战的说明都包括用于配置应用程序的 YAML 文件的完整文本。 您也可以从我们的GitHub repo复制文本。 每个 YAML 文件的文本都附带有 GitHub 链接。

本教程包括四个挑战:

  1. 部署集群和易受攻击的应用程序
  2. 破解应用程序
  3. 使用 NGINX Sidecar 容器阻止某些请求
  4. 配置 NGINX Ingress Controller 来过滤请求

挑战1: 部署集群和易受攻击的应用程序

在本次挑战中,您将部署一个 minikube 集群安装 Podinfo作为存在安全漏洞的示例应用程序。

创建 Minikube 集群

部署一个minikube集群。 几秒钟后,会出现一条消息确认部署成功。

$ minikube start 🏄 完成!kubectl 现在配置为默认使用“minikube”集群和“default”命名空间 

安装有漏洞的应用程序

在这里,您将部署一个由两个微服务组成的简单电子商务应用程序:

  • MariaDB 数据库
  • 连接数据库并检索数据的 PHP 微服务

执行以下步骤:

  1. 使用您选择的文本编辑器,创建一个名为1-app.yaml的 YAML 文件,其中包含以下内容(或从 GitHub 复制)。

    api版本:apps/v1 种类: 部署 
    元数据: 
    名称:应用程序 
    规格: 
    选择器: 
    匹配标签: 
    应用程序:应用程序 
    模板: 
    元数据: 
    标签: 
    应用程序:应用程序 
    规格: 
    容器: 
    -名称:应用程序 
    图像:f5devcentral/microservicesmarch:1.0.3 
    端口: 
    -容器端口: 80 
    环境: 
    - 名称: MYSQL_USER 
    值:dan 
    - 名称: MYSQL_PASSWORD 
    值:dan 
    - 名称: MYSQL_DATABASE 
    值:sqlitraining 
    - 名称: DATABASE_HOSTNAME 
    值:db.default.svc.cluster.local 
    --- 
    apiVersion:v1 
    种类: 服务 
    元数据: 
    名称:应用程序 
    规格: 
    端口: 
    - 端口: 80 
    目标端口: 80 
    节点端口: 30001 
    选择器:
    应用程序:应用程序
    类型: NodePort 
    --- 
    apiVersion:apps/v1 
    kind: 部署 
    元数据: 
    名称:db 
    规格: 
    选择器: 
    匹配标签: 
    应用程序:db 
    模板: 
    元数据: 
    标签: 
    应用程序:db 
    规格: 
    容器: 
    -名称:db 
    图像:mariadb:10.3.32-focal 
    端口: 
    -容器端口: 3306 
    环境: 
    - 名称: MYSQL_ROOT_PASSWORD 
    值:root 
    - 名称: MYSQL_USER 
    值:dan 
    - 名称: MYSQL_PASSWORD 
    值:dan 
    - 名称: MYSQL_DATABASE 
    值:sqlitraining 
    
    --- 
    apiVersion:v1 
    种类: 服务 
    元数据: 
    名称:db 
    规格: 
    端口: 
    - 端口: 3306 
    目标端口: 3306 
    选择器:
    应用程序:db 
    
  2. 部署应用程序和 API:

    $ kubectl apply -f 1-app.yaml deploy.apps/app 创建 service/app 创建 deploy.apps/db 创建 service/db 
    
  3. 确认 Podinfo pods 已部署,如STATUS列中的Running值所示。 它们可能需要 30-40 秒才能完全部署,因此请等到两个 pod 的状态都变为“正在运行”后再继续下一步(根据需要重新发出命令)。

    $ kubectl get pods NAME READY STATUS RESTARTS AGE app-d65d9b879-b65f2 1/1 运行 0 37s db-7bbcdc75c-q2kt5 1/1 运行 0 37s 
    
  4. 在浏览器中打开该应用程序:

    $ minikube 服务应用程序|-----------|------|-------------|--------------| | 命名空间 | 名称 | 目标端口 | URL | |-----------|------|-------------|-------------| | 默认 | 应用程序 | | 没有节点端口 | |-----------|------|----------|--------------| 😿 服务默认/应用程序没有节点端口 🏃 启动服务应用程序的隧道。|-----------|------|-------------|------------------------| | 命名空间 | 名称 | 目标端口 | URL | |-----------|------|-------------|------------------------| | 默认 | 应用程序 | | http://127.0.0.1:55446 | |-----------|------|-------------|------------------------| 🎉 在默认浏览器中打开服务默认/应用程序... 
    

挑战2: 破解应用程序

示例应用相当基础。 它包括一个列出物品(例如枕头)的主页和一组包含描述和价格等详细信息的产品页面。 数据存储在 MariaDB 数据库中。 每次请求页面时,都会向数据库发出 SQL 查询。

  • 对于主页,将检索数据库中的所有项目。
  • 对于产品页面,通过 ID 获取产品。

当您打开枕头产品页面时,您可能会注意到 URL 以/product/1结尾。 这1是产品的 ID。为防止将恶意代码直接插入 SQL 查询,最佳做法是在将请求转发到后端服务之前对用户输入进行清理。 但是,如果应用程序配置不正确,并且在将输入插入到针对数据库的 SQL 查询之前没有对其进行转义,该怎么办?

漏洞 1

要了解应用程序是否正确地转义输入,请通过将 ID 更改为数据库中不存在的 ID 来运行一个简单的实验。

手动将 URL 中的最后一个元素从1-1。 错误消息无效的产品ID “-1”表示产品 ID 没有被转义 - 相反,字符串直接插入到查询中。 除非您是黑客,否则这可不是什么好事!

假设数据库查询如下:

从 some_table 中选择 * 其中 id =“1”

要利用未转义输入导致的漏洞,请替换 1-1” <恶意查询> -- // 例如:

  • 后面的引号 ( " )-1完成第一个查询。
  • 您可以在引号后添加自己的恶意查询。
  • -- //序列丢弃查询的其余部分。

例如,如果你将 URL 中的最后一个元素更改为‑1"1-- // ,查询编译为:

从 some_table 中选择 *,其中 id =“-1”或 1 -- //“ -------------- ^ 注入 ^ 

这将从数据库中选择所有行,这在黑客攻击中很有用。 要确定是否确实如此,请将 URL 结尾更改为‑1" 。 生成的错误消息为您提供了有关数据库的更多有用信息:

严重错误: 未捕获的 mysqli_sql_exception: 您的 SQL 语法有误;请查阅与您的 MariaDB 服务器版本相对应的手册,以了解在 /var/www/html/product.php:23 第 1 行 '"-1""' 附近使用的正确语法 堆栈跟踪:#0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} 在 /var/www/html/product.php 第 23 行抛出

现在,您可以开始操作注入的代码,尝试按 ID 对数据库结果进行排序:

-1" 或 1 按 id 降序排序 -- //

结果是数据库中最后一项的产品页面。

漏洞2

强制数据库对结果进行排序很有趣,但如果您的目标是黑客攻击,那么这并不是特别有用。 尝试从数据库中提取用户名和密码更值得。

可以安全地假设数据库中有一个包含用户名和密码的用户表。 但是,如何将访问权限从产品表扩展到用户表呢?

答案是通过注入这样的代码:

-1” UNION SELECT * FROM 用户 -- //

在哪里

  • ‑1”强制从第一个查询返回一个空集。
  • UNION强制将两个数据库表(在本例中为产品用户)合并在一起,这使您能够获取原始(产品)表中没有的信息(密码)。
  • SELECT * FROM users选择用户表中的所有行。
  • -- //序列会丢弃恶意查询后的所有内容。

当你修改 URL 以注入的代码结尾时,你会收到一条新的错误消息:

严重错误: 未捕获的 mysqli_sql_exception: 使用的 SELECT 语句在 /var/www/html/product.php:23 中的列数不同 堆栈跟踪:#0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} 在 /var/www/html/product.php 第 23 行抛出

此消息表明产品表和用户表的列数不同,因此无法执行UNION指令。 但是,您可以通过反复试验,逐一将列(字段名称)作为参数添加到SELECT指令中来发现列数。 对用户表中字段名称的一个好猜测是密码,因此尝试一下:

# 选择 1 列-1" UNION SELECT password FROM users; -- // # 选择 2 列-1" UNION SELECT password,password FROM users; -- // # 选择 3 列-1" UNION SELECT password,password,password FROM users; -- / # 选择 4 列-1" UNION SELECT password,password,password,password FROM users; -- // # 选择 5 列-1" UNION SELECT password,password,password,password,password FROM users; -- //

最后一个查询成功(告诉您用户表中有五列)并且您看到用户密码:

此时您不知道与该密码对应的用户名。 但是知道用户表中的列数,您可以使用与之前相同类型的查询来显示该信息。 假设相关字段名称是用户名。 事实证明是正确的——以下查询从用户表中公开了用户名和密码。 这很好——除非这个应用程序托管在你的基础设施上!

-1” UNION SELECT 用户名,用户名,密码,密码,用户名 FROM users where id=1 -- //

挑战3: 使用 NGINX Sidecar 容器阻止某些请求

在线商店应用程序的开发人员显然需要更加注意清理用户输入(例如使用参数化查询),但作为 Kubernetes 工程师,您也可以通过阻止攻击到达应用程序来帮助防止 SQL 注入。这样一来,应用程序存在漏洞就不那么重要了。

有很多方法可以保护您的应用程序。 在本实验室的其余部分,我们将重点关注两点:

  • 在这个挑战中,你在 pod 中注入一个sidecar 容器来代理所有流量并拒绝任何 URL 中包含UNION 的请求。

    首先部署 NGINX Open Source 作为 sidecar ,然后测试它是否过滤掉恶意查询

    笔记: 我们利用这项技术仅仅是为了说明目的。 实际上,手动部署代理作为边车并不是最好的解决方案(稍后会详细介绍)。

  • 挑战 4中,我们使用 NGINX Ingress Controller 来过滤进入集群的所有流量。

将 NGINX 开源版本部署为 Sidecar

  1. 创建一个名为2-app-sidecar.yaml的 YAML 文件,其中包含以下内容(或从 GitHub 复制)。 配置的重要方面包括:

    • 运行 NGINX Open Source 的 sidecar 容器在端口 8080 上启动。
    • NGINX 将所有流量转发到应用程序。
    • 任何包含(以及其他字符串) SELECTUNION 的请求都会被拒绝(请参阅ConfigMap部分中的第一个位置块)。
    • 应用程序的服务首先将所有流量路由到 NGINX 容器。
    apiVersion:apps/v1
    种类: 部署 
    元数据: 
    名称:应用程序 
    规格: 
    选择器: 
    匹配标签: 
    应用程序:应用程序 
    模板: 
    元数据: 
    标签: 
    应用程序:应用程序 
    规格: 
    容器: 
    -名称:应用程序 
    图像:f5devcentral/microservicesmarch:1.0.3 
    端口: 
    -容器端口: 80 
    环境: 
    - 名称: MYSQL_USER 
    值:dan 
    - 名称: MYSQL_PASSWORD 
    值:dan 
    - 名称: MYSQL_DATABASE 
    值:sqlitraining 
    - 名称: DATABASE_HOSTNAME 
    值:db.default.svc.cluster.local 
    - 名称:代理 # <-- sidecar 
    图像:“nginx” 
    端口: 
    - 容器端口: 8080 
    volumeMounts: 
    - mountPath: /etc/nginx 
    名称: nginx-config 
    volumes: 
    - 名称: nginx-config 
    configMap: 
    名称: sidecar 
    --- 
    apiVersion: v1 
    kind: 服务 
    元数据: 
    名称:应用程序 
    规格: 
    端口: 
    - 端口: 80 
    目标端口: 8080 # <-- 流量被路由到代理 
    nodePort: 30001 
    选择器:
    应用程序:应用程序
    类型: NodePort 
    --- 
    apiVersion:v1 
    种类: ConfigMap 
    元数据: 
    名称:sidecar 
    数据: 
    nginx.conf:|- 
    事件{} 
    http { 
    服务器{ 
    监听8080 default_server; 
    监听[::]:8080 default_server; 
    
    位置~*“(\'|\”)(.*)(drop|insert|md5|select|union)”{ 
    拒绝所有; 
    } 
    
    位置/{ 
    proxy_pass http://localhost:80/; 
    } 
    } 
    } 
    --- 
    apiVersion:apps/v1 
    种类: 部署 
    元数据: 
    名称:db 
    规格: 
    选择器: 
    匹配标签: 
    应用程序:db 
    模板: 
    元数据: 
    标签: 
    应用程序:db 
    规格: 
    容器: 
    -名称:db 
    图像:mariadb:10.3.32-focal 
    端口: 
    -容器端口: 3306 
    环境: 
    - 名称: MYSQL_ROOT_PASSWORD 
    值:root 
    - 名称: MYSQL_USER 
    值:dan 
    - 名称: MYSQL_PASSWORD 
    值:dan 
    - 名称: MYSQL_DATABASE 
    值:sqlitraining 
    
    --- 
    apiVersion:v1 
    种类: 服务 
    元数据: 
    名称:db 
    规格: 
    端口: 
    - 端口: 3306 
    目标端口: 3306 
    选择器:
    应用程序:db
    
  2. 部署 Sidecar:

    $ kubectl apply -f 2-app-sidecar.yaml deploy.apps/app 配置 service/app 配置 configmap/sidecar 创建 deploy.apps/db 不变 service/db 不变 
    

将 Sidecar 作为过滤器进行测试

通过返回应用程序并再次尝试 SQL 注入来测试 Sidecar 是否正在过滤流量。 NGINX 在请求到达应用程序之前阻止了它!

-1” UNION SELECT 用户名,用户名,密码,密码,用户名 FROM users where id=1 -- //

挑战4: 配置 NGINX Ingress Controller 来过滤请求

挑战 3中那样保护你的应用程序是一种有趣的教育体验,但我们不建议将其用于生产,因为:

  • 这不是一个完整的安全解决方案。
  • 它不可扩展(您无法轻松地将此保护应用于多个应用程序)。
  • 更新它很复杂并且效率低下。

一个更好的解决方案是使用 NGINX Ingress Controller 将相同的保护扩展到所有应用程序! 入口控制器可用于集中各种安全功能,从阻止像 Web应用防火墙 (WAF) 这样的请求到身份验证和授权。

在这个挑战中,您将部署 NGINX Ingress Controller配置流量路由,并验证过滤器是否阻止了 SQL 注入

部署 NGINX Ingress 控制器 

安装 NGINX Ingress Controller 最快的方法是使用Helm 。  

  1. 将 NGINX 存储库添加到 Helm: 

    $ helm repo 添加 nginx-stable https://helm.nginx.com/stable  
    
  2. 下载并安装基于 NGINX 开源的NGINX Ingress Controller ,由 F5 NGINX 维护。注意enableSnippets=true参数:snippets 用于配置 NGINX 以阻止 SQL 注入。 最后一行输出确认安装成功。

    $ helm install main nginx-stable/nginx-ingress \ --set controller.watchIngressWithoutClass=true --set controller.service.type=NodePort \ --set controller.service.httpPort.nodePort=30005 \ --set controller.enableSnippets=true名称:main 最后部署: 日 星期一 DD hh:mm:ss YYYY命名空间:默认 状态:已部署 修订: 1 测试套件: 无 注意: NGINX Ingress Controller 已安装。  
    
  3. 确认 NGINX Ingress Controller pod 已部署,如STATUS列中的Running值所示。 

    $ kubectl get pods名称 就绪状态 ... main-nginx-ingress-779b74bb8b-mtdkr 1/1 正在运行... ... 重新开始年龄... 0 18秒
    

将流量路由到您的应用

  1. 创建一个名为3-ingress.yaml的 YAML 文件,其中包含以下内容(或从 GitHub 复制)。 它定义了将流量路由到应用程序所需的 Ingress 清单(这次不是通过 sidecar 代理)。 请注意注释:块中的代码片段用于定制 NGINX Ingress Controller 配置,其位置块与挑战 3 中的 ConfigMap 定义相同:它拒绝任何包含(以及其他字符串) SELECTUNION 的请求。

    api版本: v1 种类: 服务 
    元数据: 
    名称:app-without-sidecar 
    规格: 
    端口: 
    - 端口: 80 
    目标端口: 80 
    选择器:
    应用程序:应用程序 
    --- 
    apiVersion:networking.k8s.io/v1 
    种类: 入口 
    元数据: 
    名称:entry 
    注释: 
    nginx.org/server-snippets:| 
    位置 ~*“(\'|\”)(.*)(drop|insert|md5|select|union)”{ 
    全部拒绝; 
    } 
    规范: 
    入口类名称:nginx 
    规则: 
    -主机:“example.com” 
    http: 
    路径: 
    -后端: 
    服务: 
    名称:app-without-sidecar 
    端口: 
    号码: 80 
    路径: / 
    路径类型: 前缀 
    
  2. 部署 Ingress 资源: 
  3. $ kubectl apply -f 3-ingress.yaml service/app-without-sidecar 创建了 ingress.networking.k8s.io/entry 
    

验证过滤器运行情况

  1. 启动一次性BusyBox容器,以使用正确的主机名向 NGINX Ingress Controller pod 发出请求。

    $ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Host: example.com" -qO- main-nginx-ingress    #...
    
  2. 尝试 SQL 注入。 这403禁止状态代码确认 NGINX 阻止了攻击!

     

    $ wget --header="Host: example.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20username,username,password,password,username%20FROM%20users%20where%2 0id=1%20--%20//' wget:服务器返回错误: HTTP/1.1 403 禁止 
    

下一步

Kubernetes 默认是不安全的。 Ingress 控制器可以缓解 SQL 注入(以及许多其他)漏洞。 但请记住,您刚刚使用 NGINX Ingress Controller 实现的类似 WAF 的功能并不能替代实际的 WAF,也不能替代安全架构应用程序。 精明的黑客只需对代码进行一些小的修改就可以使UNION黑客程序发挥作用。 有关该主题的更多信息,请参阅渗透测试人员的 SQL 注入 (SQLi) 指南

也就是说,Ingress 控制器仍然是集中管理大部分安全性的强大工具,从而提高效率和安全性,包括集中式身份验证和授权用例(mTLS、单点登录)以及像F5 NGINX App Protect WAF这样的强大 WAF。

您的应用程序和架构的复杂性可能需要更细粒度的控制。 如果您的组织需要零信任和端到端加密,请考虑使用始终免费的F5 NGINX 服务网格等服务网格来控制 Kubernetes 集群中服务之间的通信(东西向流量)。 我们在第 4 单元“高级 Kubernetes 部署策略”中探索服务网格。

有关获取和部署 NGINX 开源的详细信息,请访问nginx.org

要尝试基于 NGINX Plus 和 NGINX App Protect 的 NGINX Ingress Controller,请立即开始30 天免费试用联系我们讨论您的用例。 

要尝试基于 NGINX 开源的 NGINX Ingress Controller,请参阅我们的 GitHub 存储库中的NGINX Ingress Controller 发布或从DockerHub下载预构建的容器。 


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”