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

NGINX | 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存储库复制这些文本。每个YAML文件的文本旁均提供GitHub链接。

本教程包含4项挑战:

  1. 部署集群和易受攻击的应用
  2. 入侵应用
  3. 使用NGINX边车容器阻止特定请求
  4. 配置NGINX Ingress Controller以筛选请求

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

在此挑战中,您将部署minikube集群,并安装Podinfo,将其用作存在安全漏洞的示例应用。

创建Minikube集群

部署一个minikube集群。几秒钟后,系统会显示一条消息,确认部署成功。

$ minikube start 
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

安装存在漏洞的应用

在此部署一个由两个微服务组成的简单电子商务应用:

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

执行以下步骤:

  1. 使用您选择的文本编辑器创建一个名为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 
  1. 部署应用和API:
$ kubectl apply -f 1-app.yaml deployment.apps/app created 
service/app created 
deployment.apps/db created 
service/db created 
  1. 确认Podinfo Pod已部署,判断依据是Running值显示在STATUS列中。Pod可能需要30–40秒才能完成部署,因此请等待两个Pod的状态均变为Running,再继续下一步(必要时可重新运行该命令)。
$ kubectl get podsNAME                  READY   STATUS    RESTARTS   AGE 
app-d65d9b879-b65f2   1/1     Running   0          37s 
db-7bbcdc75c-q2kt5    1/1     Running   0          37s 
  1. 在浏览器中打开应用:
$ 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... 

挑战2:入侵应用

该示例应用程序较为基础。它包含一个列出商品(例如枕头)的主页,以及一组显示商品描述和价格等详细信息的商品页面。数据存储在MariaDB数据库中。每次请求页面时,系统都会向数据库发出一条SQL查询。

  • 对于首页,系统会检索数据库中的所有项目。
  • 对于产品页面,按ID获取相应项目。

打开pillows产品页面时,您可能会注意到URL以/product/1结尾。其中,1是该产品的ID。为防止恶意代码直接插入SQL查询,建议先清理用户输入,再将请求转发到后端服务。但如果应用配置不当,输入在插入针对数据库的SQL查询之前未进行转义,会发生什么?

利用漏洞 1

要确定应用是否正确转义输入,可进行一个简单的实验:将ID更改为数据库中不存在的值。

手动将URL中的最后一个元素从1更改为-1。错误消息Invalid product id "-1"表明产品ID未经过转义,而是将该字符串直接插入查询中。除非您是黑客,否则这可不是好事!

假设数据库查询如下:

SELECT * FROM some_table WHERE id = "1"

要利用因未转义输入而导致的漏洞,请将1替换为-1" <malicious_query> -- //,如下所示:

  • 位于"之后的引号(-1)使第一个查询完整闭合。
  • 可以在引号后添加自己的恶意查询。
  • The -- // 序列会丢弃查询的其余部分。

例如,如果将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 -- //

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

漏洞利用 2

强制数据库对结果进行排序很有意思,但如果目标是实施入侵,这并没有太大用处。尝试从数据库中提取用户名和密码更值得投入精力。

可以合理假设,数据库中有一个包含用户名和密码的用户表。但如何将访问范围从产品表扩展到用户表?

答案是注入如下代码:

-1" UNION SELECT * FROM users -- //

其中

  • ‑1"会强制第一个查询返回空集。
  • UNION强制合并两个数据库表,在本例中为products和users,从而可以获取原始表(products)中没有的信息(密码)。
  • SELECT * FROM users 会选择 users 表中的所有行。
  • The -- // 序列会丢弃恶意查询之后的所有内容。

将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

此消息表明,products表和users表的列数不同,因此无法执行UNION指令。不过,可以通过反复尝试来确定列数:每次添加一列(字段名称),并将其作为参数传递给SELECT指令。可以合理猜测,users表中有一个名为password的字段,因此请尝试使用该字段:

# 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; -- //

最后一个查询成功(这表明users表中有5列),并且您会看到一个用户密码:

此时,您还不知道与此密码对应的用户名。但由于已知users表的列数,可以使用与之前相同类型的查询来获取该信息。假设相关字段名为username。事实证明这一假设是正确的,以下查询会同时暴露users表中的用户名和密码。这当然很好,除非此应用托管在您的基础设施上!

-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //

挑战3:使用NGINX边车容器阻止特定请求

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

保护应用的方法有很多。在本实验的其余部分,我们将重点介绍以下两种:

  • 在本挑战中,需要在Pod中注入一个边车容器,以代理所有流量,并拒绝URL中包含UNION的任何请求。首先,将NGINX Open Source部署为边车,然后测试它能否过滤恶意查询。注意:此处采用这项技术仅用于演示。实际上,手动将代理部署为边车并非理想的解决方案(稍后将详细介绍)。
  • 在挑战 4中,我们使用NGINX Ingress Controller过滤进入集群的所有流量。

将NGINX Open Source部署为边车容器

  1. 创建一个名为2-app-sidecar.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 
        - 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
  • 在端口8080上启动一个运行NGINX Open Source的边车容器。
  • NGINX将所有流量转发到应用。
  • 任何包含以下字符(以及其他字符)的请求都会被拒绝:SELECT或UNION(请参阅ConfigMap部分中的第一个location块)。
  • 该应用的服务会先将所有流量路由到NGINX容器。
  1. 部署边车:
$ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configured 
service/app configured 
configmap/sidecar created 
deployment.apps/db unchanged 
service/db unchanged 

测试作为过滤器的Sidecar

返回应用并再次尝试SQL注入,以测试Sidecar是否正在过滤流量。NGINX会在请求到达应用之前将其阻止!

-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- // 

挑战4:配置NGINX Ingress Controller以筛选请求

按照挑战 3中的方式保护应用,是一次有趣的学习实践,但不建议在生产环境中使用,原因如下:

  • 这并非完整的安全解决方案。
  • 它不具备可扩展性(无法轻松将这种保护应用于多个应用)。
  • 更新过程复杂且效率低下。

更好的解决方案是使用NGINX Ingress Controller,将同样的保护扩展到所有应用!Ingress控制器可用于集中管理各种安全功能,包括像Web应用防火墙(WAF)一样阻止请求,以及进行身份验证和授权。

在本挑战中,您将部署NGINX Ingress Controller、配置流量路由,并验证过滤器能否阻止SQL注入攻击。

部署NGINX Ingress Controller

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

  1. 将NGINX仓库添加到Helm:
$ helm repo add nginx-stable https://helm.nginx.com/stable 
  1. 下载并安装基于NGINX Open Source的NGINX Ingress Controller,该控制器由F5 NGINX维护。请注意enableSnippets=true参数:代码片段用于配置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.  
  1. 确认NGINX Ingress Controller Pod已部署,判断依据是Running值显示在STATUS列中。
$ kubectl get pods   NAME                                READY   STATUS  ...
main-nginx-ingress-779b74bb8b-mtdkr 1/1     Running ...

      ... RESTARTS   AGE 
      ... 0          18s

将流量路由到您的应用

  1. 创建一个名为3-ingress.yaml的YAML文件,并添加以下内容(也可以从GitHub复制)。该文件定义了将流量路由到应用所需的Ingress清单(这次不经过边车代理)。请注意annotations:块,其中使用代码片段来自定义NGINX Ingress Controller配置,并采用与挑战3的ConfigMap定义中相同的location块:它会拒绝包含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 
  1. 部署Ingress资源:
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar created 
ingress.networking.k8s.io/entry created 

验证过滤器运行情况

  1. 启动一个一次性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> 
# ...
  1. 尝试进行SQL注入。403 Forbidden状态码表明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、单点登录),还可集成强大的WAF,例如F5 NGINX App Protect WAF。

应用和架构的复杂性可能需要更精细的控制。如果贵组织需要零信任和端到端加密,可以考虑使用永久免费提供的服务网格,例如F5 NGINX Service Mesh,以控制Kubernetes集群中各服务之间的通信(东西向流量)。我们将在第4单元高级Kubernetes部署策略中探讨服务网格。

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

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

要试用基于NGINX Open Source的NGINX Ingress Controller,请在我们的GitHub存储库中查看“NGINX Ingress Controller版本”,或从“DockerHub”下载预构建容器。


分享

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。