注:本教程是Microservices June 2022 微服务之月项目第四单元“微服务的安全防护模式”的配套实验手册,您可点此报名参与这一免费线上教学项目以获取更多学习资源。
- 第一单元:通过自动扩展减少 Kubernetes 延迟
- 第二单元:通过速率限制保护 Kubernetes API
- 第三单元:通过灰度部署改善正常运行时间和弹性
- 第四单元:保护 Kubernetes 应用免遭 SQL 注入攻击(本文)
您在当地一家商店工作,商店销售从枕头到自行车在内的各类商品,您所在部门是 IT 部门。这家商店十分受欢迎,他们即将推出首个在线商店,但在向大众推出之前,他们请安全专家对网站进行了渗透测试。很不幸,安全专家发现了一个问题!在线商店容易遭到 SQL 注入攻击。安全专家能够利用该网站获取数据库中的敏感信息,包括用户名和密码。
您的团队找到了您 —— Kubernetes工程师来解决这个问题。幸运的是,您知道可以通过 Kubernetes 流量管理工具来规避 SQL 注入及其他漏洞。您部署了一个 Ingress controller 来暴露应用,能够确保此漏洞将不会被攻击者利用了。现在,在线商店可以准时上线了。干得漂亮!
实验和教程概述
本文是“Microservices June 2022 微服务之月”第四单元“微服务的安全防护模式”的配套文档,但您也可以在自己的环境中将其当作教程使用(从我们的 GitHub 仓库中获取示例)。它演示了如何使用 NGINX 和 NGINX Ingress Controller 拦截 SQL 注入攻击。
为了完成实验,您需要一台具有以下配置的电脑:
- 至少 2 个 CPU
- 2GB 可用内存
- 20GB 可用磁盘空间
- 互联网连接
- 容器或虚拟机管理器,例如 Docker、HyperKit、Hyper-V、KVM、Parallels、Podman、VirtualBox 或 VMware Fusion/Workstation
- 装有minikube
根据您的环境,您可能需要设置代理以便安装能够正常进行。具体可参考https://minikube.sigs.k8s.io/docs/handbook/vpn_and_proxy/ - 装有 Helm
注:本文提到的 minikube 需在可以启动浏览器窗口的台式/笔记本电脑上运行。如果您所处的环境做不到这一点,那么您需要解决如何通过浏览器访问服务的问题。
为了充分利用实验室和教程,我们建议在您开始实验之前:
- 观看直播回放
- 阅读相关的博客文章和其他学习资源
- 观看答疑课视频回放
本教程使用了以下技术:
- NGINX 开源版
- NGINX Ingress Controller (基于 NGINX 开源版)
- Helm
- minikube
- 存在安全漏洞的简单应用
本教程涉及到四个挑战:
挑战 1:部署集群和易受攻击的应用
在这项挑战中,您需要部署 minikube 集群并安装 Podinfo 作为示例应用和 API。
创建 Minikube 集群
部署 minikube 集群。几秒钟后将出现一条确认部署成功的消息。
$ minikube start Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
创建易受攻击的应用
步骤 1:创建 Deployment
您将部署一个简单的在线商店应用,其中包括两个微服务:
- MariaDB 数据库
- 连接到数据库并检索数据的 PHP 应用
- 使用您选择的文本编辑器,创建一个名为 1-app.yaml 的 YAML 文件,该文件应包含以下内容:
- 部署应用和 API:
- 确认 pods 已经部署成功,如
STATUS
列中的值Running
所示。它们可能需要 30-40 秒才能完全部署完成,所以在继续下一步之前,有必要再次运行该命令,以确认所有 pod 都在运行。 - 对于主页,其页面将检索数据库中的所有商品。
- 对于产品页面,商品将由 ID 获取。
- 修改 URL:
- 第一个引用 ” 完成了第一个查询。
- 我们在该引用之后添加自己的查询。
-- //
序列丢弃查询的其余部分。- 提取用户数据:
-1"
强制从第一个查询返回一个空集。UNION
将两个数据表(如产品表和用户表)强制放在一起,这样黑客便能够获取原本不应与原表(产品)关联的信息(密码)。UNION SELECT * FROM users
从用户表中选择所有行。-- //
序列丢弃后面的所有内容。- 代理所有前往 pod 中应用的流量。
- 使用 Ingress controller 过滤所有进入集群的流量。
- 创建一个名为 2-app-sidecar.yaml 的 YAML 文件(包含以下内容),并检查这些重要组件:
- 运行 NGINX 的 sidecar 容器在端口 8080 上启动。
- NGINX 进程将所有流量转发到应用。
- 任何包含
SELECT
或UNION
关键字的请求都会被拒绝。 - 应用的服务首先将所有流量路由到 NGINX 容器。
- 部署 sidecar:
- 它不是一个完整的安全防护解决方案。
- 它不支持扩展(您无法轻松将此防护措施应用到多个应用)。
- 更新过程复杂低效。
- 将 NGINX 仓库添加到 Helm:
- 下载并安装基于 NGINX 开源版的 NGINX Ingress Controller(由 F5 NGINX 维护)。请注意,此命令包括
enableSnippets=true
。代码段将用于配置 NGINX 以阻止 SQL 注入。最后一行输出结果确认安装成功。 - 确认 NGINX Ingress Controller pod 已经部署成功,如
STATUS
列中的值Running
所示。 - 创建一个名为 3-ingress.yaml 的 YAML 文件,该文件应包含以下内容。它定义了将流量路由到应用所需的 Ingress manifest(这一次,流量将不会通过 sidecar 代理)。请注意,NGINX Ingress Controller 使用定义为注释的代码段进行了自定义,该代码段包含与最后一项挑战相同的注入到 sidecar 容器中的代码行。
- 部署 Ingress 资源:
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
$ kubectl apply -f 1-app.yaml deployment.apps/app created
service/app created
deployment.apps/db created
service/db created
$ kubectl get pods NAME 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...
注:如果指定app参数无法显示,可使用 minikube service --all
显示
挑战 2:入侵应用
示例应用非常简单。它包含了一个包含商品列表(例如枕头)的主页和一组提供详细信息(例如描述和价格)的产品页面。数据存储在 MariaDB 数据库中。每次请求页面时,都会向数据库发出 SQL 查询。
如果打开“枕头”产品页面,您可能会注意到 URL 以 /product/1 结尾。URL 末尾的“1”用于识别产品的 ID。为了防止将恶意代码直接插入到 SQL 查询中,最好的做法是在处理请求之前对用户输入进行筛查。但是,如果该应用未正确配置,并且在将用户输入插入 SQL 查询和数据库之前未转义,会发生什么?
我们将通过一个简单的实验确定输入是否被正确转义:将 ID 更改为数据库中不存在的 ID。
将 URL 结尾从 1 手动更改为 -1。这将返回错误消息 Invalid product id
“-1”,说明产品的 ID 没有转义,原因是字符串直接插入到了查询中。大事不妙! (除非你是黑客。)
我们可以假设数据库查询类似于 SELECT * FROM some_table WHERE id = "1"
。如果想要利用它,我们可以将 1 替换为 -1″ -- //
,那么:
如果您要将 URL 末尾更改为 -1"
or 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
现在,我们可以操作结果了,我们可以使用 -1" OR 1 ORDER BY id DESC -- //
按 ID 对数据库结果进行排序。这将打开产品页面,其中包含了数据库中的最后一件商品。
强制数据库对结果进行排序十分有趣,但如果我们想干坏事的话就没什么用了。也许我们还可以从数据库中提取更多信息,例如用户数据。
我们可以假设数据库中有一个包含用户名和密码的用户表。但是,我们如何从产品表跨越到用户表呢?
我们可以通过 -1″ UNION SELECT \* FROM users -- //
来实现。
当我们将 URL 结尾修改为 -1" UNION SELECT * FROM users -- //
,我们收到新的错误消息:
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
添加列,反复进行试验,从而确定列数。用户表中可能有一个密码字段,因此我们可以尝试以下排列(注意,每个版本都会添加一个列):
-1" UNION SELECT password FROM users; -- // <-- 1 column -1" UNION SELECT password,password FROM users; -- // <-- 2 columns
-1" UNION SELECT password,password,password FROM users; -- // <-- 3 columns
-1" UNION SELECT password,password,password,password FROM users; -- // <-- 4 columns
-1" UNION SELECT password,password,password,password,password FROM users; -- // <-- 5 columns
成功!当我们使用五列的语句时就成功了。此响应显示了用户的密码。
现在,我们知道用户表中共包含五列,所以我们可以使用相同的策略继续尝试查找其他列名。获取与您公开的密码相对应的用户名是不是很有用?以下查询暴露了用户表中的用户名和密码。这太好了 —— 除非该应用托管在您的基础架构上!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //
挑战 3:使用 NGINX Sidecar 容器阻止某些请求
当然,该应用的开发人员更应注意转义用户输入(例如使用参数化查询),但是您 —— Kubernetes工程师也可以通过防止此攻击到达应用来帮助避免 SQL 注入。这样,即使应用容易遭受攻击,也可以及时将攻击拦截下来。
保护应用的选项非常多。在接下来的实验中,我们将重点关注两个选项:
这项挑战探讨了第一个选项的实现方式,即通过注入 sidecar 容器来过滤流量。我们将 NGINX 开源版用作 pod 内的 sidecar 容器 来代理所有流量,并拒绝 URL 中包含 UNION
语句的任何请求。
注:本教程使用该技术仅仅是为了方便说明。实际上,将代理手动部署为 sidecar 不是最好的解决方案(稍后将详细介绍)。
将 NGINX 开源版部署为 sidecar
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
$ 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 -- //
挑战 4:配置 NGINX Ingress Controller 以过滤请求
最后一项挑战中介绍的应用保护方式不仅有趣,而且具有教育意义,但不建议在生产环境中使用,原因如下:
使用 Ingress controller 将相同的功能扩展到所有应用是一种更好的解决方案!Ingress controller 可用于集中管理从 Web 应用防火墙 (WAF) 到身份验证和授权在内的各种安全功能。
部署 NGINX Ingress Controller
安装 NGINX Ingress Controller 最快的方法是使用 Helm。
$ helm repo add nginx-stable https://helm.nginx.com/stable
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: Tue Feb 22 19:49:17 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES: The NGINX Ingress Controller has been installed.
$ kubectl get pods NAME READY STATUS RESTARTS AGE
main-nginx-ingress-779b74bb8b-mtdkr 1/1 Running 0 18s
将流量路由到您的应用
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
测试过滤器
您需要一个新的 URL 将流量路由到 Ingress Controller 侦听的端口。要获取 URL,请启动临时的 busybox
容器,该容器使用正确的主机名向 NGINX Ingress pod 发出请求。
$ kubectl run -ti --rm=true busybox --image=busybox $ wget --header="Host: example.com" -qO- main-nginx-ingress
<!DOCTYPE html>
<html lang="en">
<head>
# truncated output
现在,尝试实施 SQL 注入。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 controller 可以规避 SQL(和许多其他)漏洞。但是请记住:即使您刚刚实施了类似 WAF 的功能,Ingress controller 既不能取代 Web 应用防火墙 (WAF),也不能替代安全架构应用。狡诈的黑客仍然可以通过对 UNION
代码做一些手脚来实施攻击。
也就是说,Ingress controller 是一款强大的工具,能够集中管理您的大部分安全防护功能,为您带来更出色的效率和安全性,包括身份验证和授权用例(mTLS,单点登录),甚至稳健的 WAF(如 NGINX App Protect WAF)。
如欲继续深入了解和本实验相关的知识技能,您可以加入到 Microservices June 2022 微服务之月项目中来,并继续浏览本项目第四单元“微服务的安全防护模式” 的其他学习资源。
如欲试用适用于 Kubernetes 且基于 NGINX Plus 的 NGINX Ingress Controller 和 NGINX App Protect,请立即下载 30 天免费试用版或者联系我们讨论您的用例。
如欲试用 NGINX 开源版 NGINX Ingress Controller,您可以获取源代码进行编译,或者从 DockerHub 下载预构建的容器。