BLOG | NGINX

NGINX 实操教程:保护 Kubernetes 应用免遭 SQL 注入攻击

NGINX-Part-of-F5-horiz-black-type-RGB
Daniele Polencic 缩略图
Daniele Polencic
Published

注:本教程是Microservices June 2022 微服务之月项目第四单元“微服务的安全防护模式”的配套实验手册,您可点此报名参与这一免费线上教学项目以获取更多学习资源。

您在当地一家商店工作,商店销售从枕头到自行车在内的各类商品,您所在部门是 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 需在可以启动浏览器窗口的台式/笔记本电脑上运行。如果您所处的环境做不到这一点,那么您需要解决如何通过浏览器访问服务的问题。

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

本教程使用了以下技术:

本教程涉及到四个挑战:

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

 

挑战 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. 使用您选择的文本编辑器,创建一个名为 1-app.yaml 的 YAML 文件,该文件应包含以下内容:
  2. 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 
    
  3. 部署应用和 API:
  4. $ kubectl apply -f 1-app.yaml deployment.apps/app created 
    service/app created 
    deployment.apps/db created 
    service/db created 
    
  5. 确认 pods 已经部署成功,如 STATUS 列中的值 Running 所示。它们可能需要 30-40 秒才能完全部署完成,所以在继续下一步之前,有必要再次运行该命令,以确认所有 pod 都在运行。
  6. $ 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 查询。

    • 对于主页,其页面将检索数据库中的所有商品。
    • 对于产品页面,商品将由 ID 获取。

    如果打开“枕头”产品页面,您可能会注意到 URL 以 /product/1 结尾。URL 末尾的“1”用于识别产品的 ID。为了防止将恶意代码直接插入到 SQL 查询中,最好的做法是在处理请求之前对用户输入进行筛查。但是,如果该应用未正确配置,并且在将用户输入插入 SQL 查询和数据库之前未转义,会发生什么?

    我们将通过一个简单的实验确定输入是否被正确转义:将 ID 更改为数据库中不存在的 ID。

    1. 修改 URL:
    2. 将 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 对数据库结果进行排序。这将打开产品页面,其中包含了数据库中的最后一件商品。

      强制数据库对结果进行排序十分有趣,但如果我们想干坏事的话就没什么用了。也许我们还可以从数据库中提取更多信息,例如用户数据。

    3. 提取用户数据:
    4. 我们可以假设数据库中有一个包含用户名和密码的用户表。但是,我们如何从产品表跨越到用户表呢?

      我们可以通过 -1″ UNION SELECT \* FROM users -- //来实现。

      • -1" 强制从第一个查询返回一个空集。
      • UNION 将两个数据表(如产品表和用户表)强制放在一起,这样黑客便能够获取原本不应与原表(产品)关联的信息(密码)。
      • 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 注入。这样,即使应用容易遭受攻击,也可以及时将攻击拦截下来。

    保护应用的选项非常多。在接下来的实验中,我们将重点关注两个选项:

    1. 代理所有前往 pod 中应用的流量。
    2. 使用 Ingress controller 过滤所有进入集群的流量。

    这项挑战探讨了第一个选项的实现方式,即通过注入 sidecar 容器来过滤流量。我们将 NGINX 开源版用作 pod 内的 sidecar 容器 来代理所有流量,并拒绝 URL 中包含 UNION 语句的任何请求。

    注:本教程使用该技术仅仅是为了方便说明。实际上,将代理手动部署为 sidecar 不是最好的解决方案(稍后将详细介绍)。

    将 NGINX 开源版部署为 sidecar

    1. 创建一个名为 2-app-sidecar.yaml 的 YAML 文件(包含以下内容),并检查这些重要组件:
      • 运行 NGINX 的 sidecar 容器在端口 8080 上启动。
      • NGINX 进程将所有流量转发到应用。
      • 任何包含 SELECTUNION 关键字的请求都会被拒绝。
      • 应用的服务首先将所有流量路由到 NGINX 容器。
      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
      
    2. 部署 sidecar:
    3. $ 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

    1. 将 NGINX 仓库添加到 Helm:
    2. $ helm repo add nginx-stable https://helm.nginx.com/stable  
    3. 下载并安装基于 NGINX 开源版的 NGINX Ingress Controller(由 F5 NGINX 维护)。请注意,此命令包括 enableSnippets=true。代码段将用于配置 NGINX 以阻止 SQL 注入。最后一行输出结果确认安装成功。
    4. 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.  
      
    5. 确认 NGINX Ingress Controller pod 已经部署成功,如 STATUS 列中的值 Running 所示。
    6. $ kubectl get pods   NAME                                  READY   STATUS    RESTARTS   AGE  
      main-nginx-ingress-779b74bb8b-mtdkr   1/1     Running   0          18s  
      

    将流量路由到您的应用

    1. 创建一个名为 3-ingress.yaml 的 YAML 文件,该文件应包含以下内容。它定义了将流量路由到应用所需的 Ingress manifest(这一次,流量将不会通过 sidecar 代理)。请注意,NGINX Ingress Controller 使用定义为注释的代码段进行了自定义,该代码段包含与最后一项挑战相同的注入到 sidecar 容器中的代码行。
    2. 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 
      
    3. 部署 Ingress 资源:
    4. $ 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 下载预构建的容器。


"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."