本教程是将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
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
在这里,您将部署一个由两个微服务组成的简单电子商务应用程序:
执行以下步骤:
使用您选择的文本编辑器,创建一个名为1-app.yaml的 YAML 文件,其中包含以下内容(或从 GitHub 复制)。
apiVersion: apps/v1 kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 80
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db
部署应用程序和 API:
$ kubectl apply -f 1-app.yaml deployment.apps/app created
service/app created
deployment.apps/db created
service/db created
确认 Podinfo pods 已部署,如STATUS
列中的Running
值所示。 它们可能需要 30-40 秒才能完全部署,因此请等到两个 pod 的状态都变为“正在运行”
后再继续下一步(根据需要重新发出命令)。
$ kubectl get podsNAME READY STATUS RESTARTS AGE
app-d65d9b879-b65f2 1/1 Running 0 37s
db-7bbcdc75c-q2kt5 1/1 Running 0 37s
在浏览器中打开该应用程序:
$ minikube service app|-----------|------|-------------|--------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|--------------|
| default | app | | No node port |
|-----------|------|-------------|--------------|
😿 service default/app has no node port
🏃 Starting tunnel for service app.
|-----------|------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|------------------------|
| default | app | | http://127.0.0.1:55446 |
|-----------|------|-------------|------------------------|
🎉 Opening service default/app in default browser...
示例应用相当基础。 它包括一个列出物品(例如枕头)的主页和一组包含描述和价格等详细信息的产品页面。 数据存储在 MariaDB 数据库中。 每次请求页面时,都会向数据库发出 SQL 查询。
当您打开枕头产品页面时,您可能会注意到 URL 以/product/1结尾。 这1是产品的 ID。为防止将恶意代码直接插入 SQL 查询,最佳做法是在将请求转发到后端服务之前对用户输入进行清理。 但是,如果应用程序配置不正确,并且在将输入插入到针对数据库的 SQL 查询之前没有对其进行转义,该怎么办?
要了解应用程序是否正确地转义输入,请通过将 ID 更改为数据库中不存在的 ID 来运行一个简单的实验。
手动将 URL 中的最后一个元素从1到-1。 错误消息无效的
产品
ID
“-1”
表示产品 ID 没有被转义 - 相反,字符串直接插入到查询中。 除非您是黑客,否则这可不是什么好事!
假设数据库查询如下:
SELECT * FROM some_table WHERE id = "1"
要利用未转义输入导致的漏洞,请替换 1
和 -1”
<恶意查询>
--
//
例如:
"
)-1
完成第一个查询。--
//
序列丢弃查询的其余部分。例如,如果你将 URL 中的最后一个元素更改为‑1"
或1
--
//
,查询编译为:
SELECT * FROM some_table WHERE id = "-1" OR 1 -- //"
--------------
^ injected ^
这将从数据库中选择所有行,这在黑客攻击中很有用。 要确定是否确实如此,请将 URL 结尾更改为‑1"
。 生成的错误消息为您提供了有关数据库的更多有用信息:
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"-1""' at line 1 in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23
现在,您可以开始操作注入的代码,尝试按 ID 对数据库结果进行排序:
-1" OR 1 ORDER BY id DESC -- //
结果是数据库中最后一项的产品页面。
强制数据库对结果进行排序很有趣,但如果您的目标是黑客攻击,那么这并不是特别有用。 尝试从数据库中提取用户名和密码更值得。
可以安全地假设数据库中有一个包含用户名和密码的用户表。 但是,如何将访问权限从产品表扩展到用户表呢?
答案是通过注入这样的代码:
-1" UNION SELECT * FROM users -- //
在哪里
‑1”
强制从第一个查询返回一个空集。UNION
强制将两个数据库表(在本例中为产品和用户)合并在一起,这使您能够获取原始(产品)表中没有的信息(密码)。SELECT
*
FROM
users
选择用户表中的所有行。--
//
序列会丢弃恶意查询后的所有内容。当你修改 URL 以注入的代码结尾时,你会收到一条新的错误消息:
Fatal error: Uncaught mysqli_sql_exception: The used SELECT statements have a different number of columns in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23
此消息表明产品表和用户表的列数不同,因此无法执行UNION
指令。 但是,您可以通过反复试验,逐一将列(字段名称)作为参数添加到SELECT
指令中来发现列数。 对用户表中字段名称的一个好猜测是密码
,因此尝试一下:
# select 1 column-1" UNION SELECT password FROM users; -- //
# select 2 columns
-1" UNION SELECT password,password FROM users; -- //
# select 3 columns
-1" UNION SELECT password,password,password FROM users; -- /
# select 4 columns
-1" UNION SELECT password,password,password,password FROM users; -- //
# select 5 columns
-1" UNION SELECT password,password,password,password,password FROM users; -- //
最后一个查询成功(告诉您用户表中有五列)并且您看到用户密码:
此时您不知道与该密码对应的用户名。 但是知道用户表中的列数,您可以使用与之前相同类型的查询来显示该信息。 假设相关字段名称是用户名
。 事实证明是正确的——以下查询从用户表中公开了用户名和密码。 这很好——除非这个应用程序托管在你的基础设施上!
-1" UNION SELECT username,username,password,password,username 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
kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
- name: proxy # <-- sidecar
image: "nginx"
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
name: nginx-config
volumes:
- name: nginx-config
configMap:
name: sidecar
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 8080 # <-- the traffic is routed to the proxy
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sidecar
data:
nginx.conf: |-
events {}
http {
server {
listen 8080 default_server;
listen [::]:8080 default_server;
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
location / {
proxy_pass http://localhost:80/;
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db
部署 Sidecar:
$ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configured
service/app configured
configmap/sidecar created
deployment.apps/db unchanged
service/db unchanged
通过返回应用程序并再次尝试 SQL 注入来测试 Sidecar 是否正在过滤流量。 NGINX 在请求到达应用程序之前阻止了它!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //
像挑战 3中那样保护你的应用程序是一种有趣的教育体验,但我们不建议将其用于生产,因为:
一个更好的解决方案是使用 NGINX Ingress Controller 将相同的保护扩展到所有应用程序! 入口控制器可用于集中各种安全功能,从阻止像 Web应用防火墙 (WAF) 这样的请求到身份验证和授权。
在这个挑战中,您将部署 NGINX Ingress Controller ,配置流量路由,并验证过滤器是否阻止了 SQL 注入。
安装 NGINX Ingress Controller 最快的方法是使用Helm 。
将 NGINX 存储库添加到 Helm:
$ helm repo add 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
NAME: main
LAST DEPLOYED: Day Mon DD hh:mm:ss YYYY
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES: The NGINX Ingress Controller has been installed.
确认 NGINX Ingress Controller pod 已部署,如STATUS
列中的Running
值所示。
$ kubectl get pods NAME READY STATUS ...
main-nginx-ingress-779b74bb8b-mtdkr 1/1 Running ...
... RESTARTS AGE
... 0 18s
创建一个名为3-ingress.yaml的 YAML 文件,其中包含以下内容(或从 GitHub 复制)。 它定义了将流量路由到应用程序所需的 Ingress 清单(这次不是通过 sidecar 代理)。 请注意注释:
块中的代码片段用于定制 NGINX Ingress Controller 配置,其位置
块与挑战 3 中的 ConfigMap 定义相同:它拒绝任何包含(以及其他字符串) SELECT
或UNION 的
请求。
apiVersion: v1 kind: Service
metadata:
name: app-without-sidecar
spec:
ports:
- port: 80
targetPort: 80
selector:
app: app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: entry
annotations:
nginx.org/server-snippets: |
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
spec:
ingressClassName: nginx
rules:
- host: "example.com"
http:
paths:
- backend:
service:
name: app-without-sidecar
port:
number: 80
path: /
pathType: Prefix
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar created
ingress.networking.k8s.io/entry created
启动一次性BusyBox容器,以使用正确的主机名向 NGINX Ingress Controller pod 发出请求。
$ kubectl run -ti --rm=true busybox --image=busybox$ wget --header="Host: example.com" -qO- main-nginx-ingress
<!DOCTYPE html>
<html lang="en">
<head>
# ...
尝试 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: server returned error: HTTP/1.1 403 Forbidden
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 内容。”