本教程是将2022 年 3 月微服务中的概念付诸实践的四个教程之一: Kubernetes 网络:
是否想要有关使用 NGINX 实现更多 Kubernetes 网络用例的详细指导? 下载我们的免费电子书《使用 NGINX 管理 Kubernetes 流量》: 实用指南。
您在当地一家受欢迎的商店从事 IT 工作,该商店销售从枕头到自行车等各种商品。 他们即将推出第一家网上商店,但他们请一位安全专家在网站公开之前对其进行渗透测试。 不幸的是,安全专家发现了一个问题! 在线商店容易受到SQL 注入攻击。 安全专家能够利用该网站从您的数据库中获取敏感信息,包括用户名和密码。
您的团队来找您——Kubernetes 工程师——来拯救这一局面。 幸运的是,您知道可以使用 Kubernetes 流量管理工具来缓解 SQL 注入(以及其他漏洞)。 您已经部署了 Ingress 控制器来公开应用程序,并且在单一配置中,您能够确保漏洞不会被利用。 现在,网上商店可以按时上线了。 做得好!
本博客是 2022 年 3 月微服务第 3 单元 – Kubernetes 中的微服务安全模式的附带实验室,演示了如何使用 NGINX 和 NGINX Ingress Controller 来阻止 SQL 注入。
要运行本教程,您需要一台具有以下配置的机器:
为了充分利用实验室和教程,我们建议您在开始之前:
本教程使用了以下技术:
每个挑战的说明都包括用于配置应用程序的 YAML 文件的完整文本。 您也可以从我们的GitHub repo复制文本。 每个 YAML 文件的文本都附带有 GitHub 链接。
本教程包括四个挑战:
在本次挑战中,您将部署一个 minikube 集群并安装 Podinfo作为存在安全漏洞的示例应用程序。
部署一个minikube集群。 几秒钟后,会出现一条消息确认部署成功。
$ minikube start 🏄 完成!kubectl 现在配置为默认使用“minikube”集群和“default”命名空间
在这里,您将部署一个由两个微服务组成的简单电子商务应用程序:
执行以下步骤:
使用您选择的文本编辑器,创建一个名为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
部署应用程序和 API:
$ kubectl apply -f 1-app.yaml deploy.apps/app 创建 service/app 创建 deploy.apps/db 创建 service/db
确认 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
在浏览器中打开该应用程序:
$ minikube 服务应用程序|-----------|------|-------------|--------------| | 命名空间 | 名称 | 目标端口 | URL | |-----------|------|-------------|-------------| | 默认 | 应用程序 | | 没有节点端口 | |-----------|------|----------|--------------| 😿 服务默认/应用程序没有节点端口 🏃 启动服务应用程序的隧道。|-----------|------|-------------|------------------------| | 命名空间 | 名称 | 目标端口 | URL | |-----------|------|-------------|------------------------| | 默认 | 应用程序 | | http://127.0.0.1:55446 | |-----------|------|-------------|------------------------| 🎉 在默认浏览器中打开服务默认/应用程序...
示例应用相当基础。 它包括一个列出物品(例如枕头)的主页和一组包含描述和价格等详细信息的产品页面。 数据存储在 MariaDB 数据库中。 每次请求页面时,都会向数据库发出 SQL 查询。
当您打开枕头产品页面时,您可能会注意到 URL 以/product/1结尾。 这1是产品的 ID。为防止将恶意代码直接插入 SQL 查询,最佳做法是在将请求转发到后端服务之前对用户输入进行清理。 但是,如果应用程序配置不正确,并且在将输入插入到针对数据库的 SQL 查询之前没有对其进行转义,该怎么办?
要了解应用程序是否正确地转义输入,请通过将 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 降序排序 -- //
结果是数据库中最后一项的产品页面。
强制数据库对结果进行排序很有趣,但如果您的目标是黑客攻击,那么这并不是特别有用。 尝试从数据库中提取用户名和密码更值得。
可以安全地假设数据库中有一个包含用户名和密码的用户表。 但是,如何将访问权限从产品表扩展到用户表呢?
答案是通过注入这样的代码:
-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 -- //
在线商店应用程序的开发人员显然需要更加注意清理用户输入(例如使用参数化查询),但作为 Kubernetes 工程师,您也可以通过阻止攻击到达应用程序来帮助防止 SQL 注入。这样一来,应用程序存在漏洞就不那么重要了。
有很多方法可以保护您的应用程序。 在本实验室的其余部分,我们将重点关注两点:
在这个挑战中,你在 pod 中注入一个sidecar 容器来代理所有流量并拒绝任何 URL 中包含UNION 的
请求。
首先部署 NGINX Open Source 作为 sidecar ,然后测试它是否过滤掉恶意查询。
笔记: 我们利用这项技术仅仅是为了说明目的。 实际上,手动部署代理作为边车并不是最好的解决方案(稍后会详细介绍)。
创建一个名为2-app-sidecar.yaml的 YAML 文件,其中包含以下内容(或从 GitHub 复制)。 配置的重要方面包括:
SELECT
或UNION 的
请求都会被拒绝(请参阅ConfigMap
部分中的第一个位置
块)。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
部署 Sidecar:
$ kubectl apply -f 2-app-sidecar.yaml deploy.apps/app 配置 service/app 配置 configmap/sidecar 创建 deploy.apps/db 不变 service/db 不变
通过返回应用程序并再次尝试 SQL 注入来测试 Sidecar 是否正在过滤流量。 NGINX 在请求到达应用程序之前阻止了它!
-1” UNION SELECT 用户名,用户名,密码,密码,用户名 FROM users where id=1 -- //
像挑战 3中那样保护你的应用程序是一种有趣的教育体验,但我们不建议将其用于生产,因为:
一个更好的解决方案是使用 NGINX Ingress Controller 将相同的保护扩展到所有应用程序! 入口控制器可用于集中各种安全功能,从阻止像 Web应用防火墙 (WAF) 这样的请求到身份验证和授权。
在这个挑战中,您将部署 NGINX Ingress Controller ,配置流量路由,并验证过滤器是否阻止了 SQL 注入。
安装 NGINX Ingress Controller 最快的方法是使用Helm 。
将 NGINX 存储库添加到 Helm:
$ helm repo 添加 nginx-stable https://helm.nginx.com/stable
下载并安装基于 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 已安装。
确认 NGINX Ingress Controller pod 已部署,如STATUS
列中的Running
值所示。
$ kubectl get pods名称 就绪状态 ... main-nginx-ingress-779b74bb8b-mtdkr 1/1 正在运行... ... 重新开始年龄... 0 18秒
创建一个名为3-ingress.yaml的 YAML 文件,其中包含以下内容(或从 GitHub 复制)。 它定义了将流量路由到应用程序所需的 Ingress 清单(这次不是通过 sidecar 代理)。 请注意注释:
块中的代码片段用于定制 NGINX Ingress Controller 配置,其位置
块与挑战 3 中的 ConfigMap 定义相同:它拒绝任何包含(以及其他字符串) SELECT
或UNION 的
请求。
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
路径: /
路径类型: 前缀
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar 创建了 ingress.networking.k8s.io/entry
启动一次性BusyBox容器,以使用正确的主机名向 NGINX Ingress Controller pod 发出请求。
$ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Host: example.com" -qO- main-nginx-ingress #...
尝试 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 内容。”